ecretVault WP:HTTP Connection 头绕过 JWT 反代身份校验

📅 2026/7/6 21:02:59
ecretVault WP:HTTP Connection 头绕过 JWT 反代身份校验
一、yml 文件分析题目给了源码并且给了docker-compose.yml文件我们可以直接在本地起环境。在运行容器之前先看看.yml文件中的内容services: secretvault: build: context: . container_name: secretvault environment: ICQ_FLAG: flag{test} ports: - 5555:5555得到的信息本地 flag 为flag容器内部的 5555 端口映射到宿主机的 5555端口二、登入界面分析1、简单浏览把容器跑起来# 在 docker-compose.yml 文件所在目录运行 docker compose up由于端口仅映射了 5555 端口那么直接浏览器访问http://127.0.0.1:5555看到的是一个登入界面并且含注册功能CTRL U查看页面源码没有发现“注释信息泄露关键信息”的情况。弱密码尝试登入 admin 账户无果打算尝试注册一个 admin发现用户已经存在说明 admin 就很有可能是管理员账户并且是我们最终的目标。正常情况下这里可以尝试做 SQL Fuzz 的操作但是这题给了网站的源码可以直接进行白盒审计。2、反代根据 5555 端口这个线索我们可以定位到这段代码main.golog.Println(Authorizer middleware service is running at :5555) if err : http.ListenAndServe(:5555, authorizer); err ! nil { log.Fatal(err) }http.ListenAndServe是 Go 标准库net/http里的一个函数用来启动一个 HTTP 服务器它有两个参数参数类型作用addrstring指定监听的地址和端口handlerhttp.Handler指定收到请求后由谁处理因此我们可以分析得到服务器监听任意网卡的 5555 端口并且由authorizer这个对象处理请求。据此线索去找authorizer还是在main.go文件中authorizer : httputil.ReverseProxy{Director: func(req *http.Request) { req.URL.Scheme http req.URL.Host 127.0.0.1:5000 uid : GetUIDFromRequest(req) log.Printf(Request UID: %s, URL: %s, uid, req.URL.String()) req.Header.Del(Authorization) req.Header.Del(X-User) req.Header.Del(X-Forwarded-For) req.Header.Del(Cookie) if uid { req.Header.Set(X-User, anonymous) } else { req.Header.Set(X-User, uid) } }}httputil.ReverseProxy是 Go 中专门用来实现反向代理的结构体用于处理反代服务器和后端服务器之间的请求交互。authorizer实例化了这个结构体并作为指向该实例地址的指针存在。req.URL.Scheme http req.URL.Host 127.0.0.1:5000这代表真正的后端服务是开在127.0.0.1:5000上的。指定了127.0.0.1说明不允许非本地网络访问。接着看uid : GetUIDFromRequest(req)调用了GetUIDFromRequest()函数func GetUIDFromRequest(r *http.Request) string { authHeader : r.Header.Get(Authorization) if authHeader { cookie, err : r.Cookie(token) if err nil { authHeader Bearer cookie.Value } else { return } } if len(authHeader) 7 || !strings.HasPrefix(authHeader, Bearer ) { return } tokenString : strings.TrimSpace(authHeader[7:]) if tokenString { return } token, err : jwt.ParseWithClaims(tokenString, AuthClaims{}, func(token *jwt.Token) (interface{}, error) { if _, ok : token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf(unexpected signing method: %v, token.Header[alg]) } return []byte(SecretKey), nil }) if err ! nil { log.Printf(failed to parse token: %v, err) return } claims, ok : token.Claims.(*AuthClaims) if !ok || !token.Valid { log.Printf(invalid token claims) return } return claims.UID }该函数首先会查看 Http Header 中是否有 Authorization 字段如果没有则继续查看 Cookie 字段中的 token如果有值则写入 authHeader 变量中无值则直接函数返回空字符。后续一连串的if都是用于判断这个 Token 是否是一个合法的 JWT Token格式、长度等是否正确并且判断签名是否正确。若合法且签名正确则会接收jwt.ParseWithClaims函数的返回值。这个函数可以在 Go 的官方文档https://pkg.go.dev/github.com/golang-jwt/jwt/v5#section-readme中看到其返回值*Token, error同意文档中可以找到type Token struct { Raw string // Raw contains the raw token. Populated when you [Parse] a token Method SigningMethod // Method is the signing method used or to be used Header map[string]any // Header is the first segment of the token in decoded form Claims Claims // Claims is the second segment of the token in decoded form Signature []byte // Signature is the third segment of the token in decoded form. Populated when you [Parse] or sign a token Valid bool // Valid specifies if the token is valid. Populated when you [Parse] a token }简单来说token会获得 JWT 的一些关键元素。最后claims, ok : token.Claims.(*AuthClaims) …… return claims.UID这回返回 payload 中的 UID 字段。回到之前看到的uid : GetUIDFromRequest(req)现在我们知道这一步会检查 jwt 的合法性和签名情况如果正常则返回 jwt payload 中的 uid 字段值并赋值给uid变量。继续看req.Header.Del(Authorization) req.Header.Del(X-User) req.Header.Del(X-Forwarded-For) req.Header.Del(Cookie)这说明反代传给后端服务器的 http 请求并非与用户原始请求相同而是删减了一些 http 请求头AuthorizationX-UserX-Forwarded-ForCookie最后是一个if-else判断if uid { req.Header.Set(X-User, anonymous) } else { req.Header.Set(X-User, uid) }如果 uid 没有值则在请求头中加上X-User: anonymous如果 uid 有值则在请求头中加上X-User: uid_value3、真正的后端既然知道了 5555 是反代当然要去找到真正的后端服务也就是我们上面分析出来的 5000 端口在app.py中就能看到监听信息if __name__ __main__: flask_app create_app() flask_app.run(host127.0.0.1, port5000, debugFalse)通过之前页面的简单浏览我们知道访问127.0.0.1:5555就会重定向到127.0.0.1:5555/sign对应的代码逻辑app.route(/) def index(): uid request.headers.get(X-User, 0) if not uid or uid anonymous: return redirect(url_for(login)) return redirect(url_for(dashboard))这里有个小发现如果请求头中的X-User字段有值且不为“anonymous”则可以绕过登入直接到/dashboard界面。正常的请求包中是没有该字段的我们若手动添加该字段则会被反代给删除上面我们分析过的req.Header.Del(X-User)删除后又会根据 uid 的值添加上了X-User字段并赋予新值if uid { req.Header.Set(X-User, anonymous) } else { req.Header.Set(X-User, uid) }而 uid 的值来自 jwt若不知道签名密钥则无法伪造正常 jwt。换言之我们无法在请求中伪造X-User字段来绕过登入判断。但是这有个非常重要的细节聚焦request.headers.get(X-User, 0)当请求头中没有X-User字段的时候则会添加上X-User: 0而 0 刚好是能绕过登入判断的。那么我们就得想如何让反代不传该请求头呢想到 http 请求头中的字段ConnectionConnection里列出的字段名表示这些头只对当前连接有效代理在转发时应把它们去掉。也就是说当我们请求127.0.0.1:5555的时候带上请求头…… …… Connection: X-User反代看到该字段则在转发给后端服务器的时候会将该字段从请求头中去除。5000 API 接到请求后发现并没有检测到X-User请求头字段则默认将其设置为X-User: 0绕过登入成功。三、获取 Flag尝试上述分析到的结果改包从响应中可以看出直接重定向到/dashboard去了而不是/sign说明成功了。开启 burp 的跟随重定向功能