Google CTF 2022 - LOG4J ( WEB CHALLANGES )
LOG4J⌗
CHALLENGE INFO : Talk with the most advanced AI. https://log4j-web.2022.ctfcompetition.com
After visiting the challenge URL, we have a chatbot interface with an input field.
Looking into the source code, we see the following:
Python 3.8.10
running aFlask web server
in the frontend.Java 11.0.15
in the backend withLog4j 2.17.2
. In this versionJNDI lookups
are disabled by default andLog4Shell is not exploitable
. The versions by running the Docker container and looking intopom.xml
.
Python code serves frontend.
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)
The backend is called by starting a Java process like this:
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')
Looking at JAVA code,
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);
}
Flag is hidden in the environment variable FLAG
.
The LOGGER.info
call is the only one calling Log.
The command is fetched from the system property Commands must start with a / - except for help. Next, the command and the arguments are passed tCommand(cmd.substring(1), args)
We finally see the available commands in app.java
:
- help or /help just prints a help message
- /repeat will print the first (and only) argument to stdout
- /time will print the current time
- /wc will print the number of arguments passed
log4j2.xml
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="INFO">
<Appenders>
<Console name="Console" target="SYSTEM_ERR">
<PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %logger{36} executing ${sys:cmd} - %msg %n">
</PatternLayout>
</Console>
</Appenders>
<Loggers>
<Root level="debug">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>
This configures how Log4j will behave: Logs are written to stderr (remember, we don’t collect that in the Python frontend) The system properties lookup ${sys:cmd} prints the cmd property in every log line Afterwards, %msg holds the message, followed by a line break %n
Our cmd is passed as-is to the Java process and injected into the pattern layout:
%d{HH:mm:ss.SSS} %-5level %logger{36} executing ${sys:cmd} - %msg %n
Log4j still has environment lookups which could enable us to read the flag via ${env:FLAG}. A first try would be to inject this in the cmd command. This will print the FLAG to stderr on the backend, but sadly this is not captured by the frontend.
%formatstringhereeee⌗
After trying alot, suddenly I came across %formatstringhereeee
command, that raise an exception
Log4j will resolve patterns recursively so that we can combine conversion patterns and environment lookups. This will also trigger an exception, but the exception does not leak the resolved environment lookup. Yet.
To encounter that problem we can use java lookup with env
lookup to extract the environment variable FLAG
.
Combining this with raising exception
, we can leak the flag.
%formatstringhereeeeeeee${java:${env:FLAG}}⌗
Flag : CTF{d95528534d14dc6eb6aeb81c994ce8bd}