7 minutes
FE-CTF 2023 - Brannigan’s Law
The Task
We get the following task:
Captain Zip Brannigan at your service!
Stardate... April. The 13th. Point two.
We've made it to Kompromaise 7, the have-it-both-ways world.
Everyone here, from young to old, and in between (..especially in
between) are used to compromising.
It. Disgusts. Me.
But fear not! I have assigned my most trusted
chump^H^H^H^H^Hdiplomatic envoy to help make contact.
That's YOU!
You need to send a zip file to initiate first contact.
Now remember, the younger seyvaan-siip people, and the older, more
conservative ohns-ehp people don't get along.
Please send a single file that makes everybody happy. And no excuses!
What are you waiting for, cadet? Fly over to
brannigans-law.hack.fe-ctf.dk:1337 right away.
Brannigan out!
And if we connect to the remote with some random data we get:
$ nc brannigans-law.hack.fe-ctf.dk 1337
Okay, Cadet. Listen up.
The situation. Is. Tense.
We need a pair of aces to win this chess match.
The seyvaan-siipians expect a file called 'DoJfLXXWvNUTHKNZnQfadKcV.txt' containing 'RdxHWQMsdUrwtylMdNXVFgCB'
But the proud ohns-ehp tribe demand a file called 'jpyvQGymBaisaRjtcoj.txt' containing 'nwyaPQZrcvjhRSQInVXTqiSd'
We cannot afford to take sides. At least not on your salary.
So it's one file.
You can make one file in two versions in one file, right?
..right?
Ugh. Kif! Clear my desk. No, not all of it!
Cadet. How large is this file of yours going to be?
1
Very well. Kif, clear space for 1B, there in the corner. Be careful
with my portrait!
At it Cadet. We haven't got all day! Let's see this file.
Receiving file.. 1
OK
Systems check:
unzip: UnZip 6.00 of 20 April 2009, by Debian. Original by Info-ZIP.
7z: 7-Zip [64] 16.02 : Copyright (c) 1999-2016 Igor Pavlov : 2016-05-21
zipdetails:
Running unzip test.. Bad news, cadet! It seems unzip failed:
Command '['unzip', '-t', '/tmp/tmpq9jn5jk2.zip']' returned
non-zero exit status 9.
No flag for you!
Polyglotting
So it seems that we need to create a compressed file where 7-zip and unzip both only see their own file in.
We can solve this with a polyglot file, which is a file that is actually two different files, so the plan is to make a 7-zip archive and a normal zip archive and append their data together, such that 7-zip sees the 7-zip header and extracts that, while unzip skips over the 7-zip file to find the zip file header in the second half of the file.
So lets test this:
$ echo "Content of file 1" > file1.txt
$ 7zz a file1.7z file1.txt
7-Zip (z) 23.01 (arm64) : Copyright (c) 1999-2023 Igor Pavlov : 2023-06-20
64-bit arm_v:8 locale=en_US.UTF-8 Threads:8 OPEN_MAX:2560, ASM
Scanning the drive:
1 file, 18 bytes (1 KiB)
Creating archive: file1.7z
Add new data to archive: 1 file, 18 bytes (1 KiB)
Files read from disk: 1
Archive size: 144 bytes (1 KiB)
Everything is Ok
$ echo "Content of file 2" > file2.txt
$ zip file2.zip file2.txt
adding: file2.txt (stored 0%)
$ cat file1.7z file2.zip > polyglot.zip
$ file --keep-going polyglot.zip
polyglot.zip: 7-zip archive data, version 0.4
- data
$ 7zz x polyglot.zip
7-Zip (z) 23.01 (arm64) : Copyright (c) 1999-2023 Igor Pavlov : 2023-06-20
64-bit arm_v:8 locale=en_US.UTF-8 Threads:8 OPEN_MAX:2560, ASM
Scanning the drive for archives:
1 file, 331 bytes (1 KiB)
Extracting archive: polyglot.zip
WARNINGS:
There are data after the end of archive
WARNING:
polyglot.zip
Cannot open the file as [zip] archive
The file is open as [7z] archive
--
Path = polyglot.zip
Open WARNING: Cannot open the file as [zip] archive
Type = 7z
WARNINGS:
There are data after the end of archive
Physical Size = 144
Tail Size = 187
Headers Size = 122
Method = LZMA2:12
Solid = -
Blocks = 1
Would you like to replace the existing file:
Path: ./file1.txt
Size: 18 bytes (1 KiB)
Modified: 2023-11-01 09:12:02
with the file from archive:
Path: file1.txt
Size: 18 bytes (1 KiB)
Modified: 2023-11-01 09:12:02
? (Y)es / (N)o / (A)lways / (S)kip all / A(u)to rename all / (Q)uit? n
Everything is Ok
Archives with Warnings: 1
Warnings: 1
Size: 18
Compressed: 331
$ unzip polyglot.zip
Archive: polyglot.zip
warning [polyglot.zip]: 144 extra bytes at beginning or within zipfile
(attempting to process anyway)
replace file2.txt? [y]es, [n]o, [A]ll, [N]one, [r]ename: n
$ echo $?
1
So it seems to work, the only problem is that unzip throws a warning that makes the return value non-zero, luckily we can fix this with zip -F
to fix the offset:
$ zip -F polyglot.zip --out fixedpolyglot.zip
Fix archive (-F) - assume mostly intact archive
Zip entry offsets appear off by 144 bytes - correcting...
copying: file2.txt
$ 7zz x fixedpolyglot.zip
7-Zip (z) 23.01 (arm64) : Copyright (c) 1999-2023 Igor Pavlov : 2023-06-20
64-bit arm_v:8 locale=en_US.UTF-8 Threads:8 OPEN_MAX:2560, ASM
Scanning the drive for archives:
1 file, 331 bytes (1 KiB)
Extracting archive: fixedpolyglot.zip
WARNINGS:
There are data after the end of archive
WARNING:
fixedpolyglot.zip
Cannot open the file as [zip] archive
The file is open as [7z] archive
--
Path = fixedpolyglot.zip
Open WARNING: Cannot open the file as [zip] archive
Type = 7z
WARNINGS:
There are data after the end of archive
Physical Size = 144
Tail Size = 187
Headers Size = 122
Method = LZMA2:12
Solid = -
Blocks = 1
Would you like to replace the existing file:
Path: ./file1.txt
Size: 18 bytes (1 KiB)
Modified: 2023-11-01 09:12:02
with the file from archive:
Path: file1.txt
Size: 18 bytes (1 KiB)
Modified: 2023-11-01 09:12:02
? (Y)es / (N)o / (A)lways / (S)kip all / A(u)to rename all / (Q)uit? n
Everything is Ok
Archives with Warnings: 1
Warnings: 1
Size: 18
Compressed: 331
$ unzip fixedpolyglot.zip
Archive: fixedpolyglot.zip
replace file2.txt? [y]es, [n]o, [A]ll, [N]one, [r]ename: n
Solving and flag
We can solve the challenge on the remote by automating the polyglot generation and sending the polyglot to the server.
For this I wrote the following solve script:
from pwn import remote
from os import system, path
conn = remote("brannigans-law.hack.fe-ctf.dk", 1337)
conn.recvuntil(b"'")
file1_name = conn.recvuntil(b"'").decode()[:-1]
conn.recvuntil(b"'")
file1_contents = conn.recvuntil(b"'").decode()[:-1]
conn.recvuntil(b"'")
file2_name = conn.recvuntil(b"'").decode()[:-1]
conn.recvuntil(b"'")
file2_contents = conn.recvuntil(b"'").decode()[:-1]
conn.recv()
with open(file1_name, 'w') as fp:
fp.write(file1_contents)
with open(file2_name, 'w') as fp:
fp.write(file2_contents)
system("7zz a file1.7z " + file1_name)
system("zip file2.zip " + file2_name)
with open("file1.7z", "rb") as fp:
zip1_data = fp.read()
with open("file2.zip", "rb") as fp:
zip2_data = fp.read()
parasite_data = zip1_data + zip2_data
with open("parasite.zip", "wb") as fp:
fp.write(parasite_data)
system("zip -F parasite.zip --out solve.zip")
psize = path.getsize("solve.zip")
print(psize)
conn.sendline(str(psize).encode())
conn.recvuntil(b'file..')
with open("solve.zip", "rb") as fp:
conn.sendline(fp.read())
conn.interactive()
And in action:
$ python3 solve.py
[+] Opening connection to brannigans-law.hack.fe-ctf.dk on port 1337: Done
7-Zip (z) 23.01 (arm64) : Copyright (c) 1999-2023 Igor Pavlov : 2023-06-20
64-bit arm_v:8 locale=en_US.UTF-8 Threads:8 OPEN_MAX:2560, ASM
Open archive: file1.7z
--
Path = file1.7z
Type = 7z
Physical Size = 144
Headers Size = 122
Method = LZMA2:12
Solid = -
Blocks = 1
Scanning the drive:
1 file, 12 bytes (1 KiB)
Updating archive: file1.7z
Keep old data in archive: 1 file, 18 bytes (1 KiB)
Add new data to archive: 1 file, 12 bytes (1 KiB)
Files read from disk: 1
Archive size: 225 bytes (1 KiB)
Everything is Ok
adding: tuCpTnMSbzWwyTMWrUcmPspKN.txt (stored 0%)
Fix archive (-F) - assume mostly intact archive
Zip entry offsets appear off by 225 bytes - correcting...
copying: file2.txt
copying: tuCpTnMSbzWwyTMWrUcmPspKN.txt
612
[*] Switching to interactive mode
OK
Systems check:
unzip: UnZip 6.00 of 20 April 2009, by Debian. Original by Info-ZIP.
7z: 7-Zip [64] 16.02 : Copyright (c) 1999-2016 Igor Pavlov : 2016-05-21
zipdetails:
Running unzip test.. OK
Running 7z test.. OK
Running zipdetails test.. OK
Extracting with 7z.. OK
Trying wrong file with 7z.. OK
Extracting with unzip.. OK
Extracting wrong file with unzip.. OK
You did well! Here, have a flag. It's made of *velour*:
flag{i_dont_pretend_to_understand_brannigans_law_i_merely_enforce_it}
[*] Got EOF while reading in interactive
$
[*] Closed connection to brannigans-law.hack.fe-ctf.dk port 1337
Thrus the flag is flag{i_dont_pretend_to_understand_brannigans_law_i_merely_enforce_it}