[HTB] Management 渗透测试笔记
这次打的是 Management,一台 Linux Easy 靶机。我从首页的 SSO 入口开始,先折腾了前端资源解密、JATO 参数和匿名账户权限,后来通过 OpenAM 拿到 Shell,再沿着 GLPI 凭据和备份权限拿到了两个 Flag。中间也走了些弯路,一起记在这里。
目标靶机信息
| 项目 | 内容 |
|---|---|
| 平台 | Hack The Box |
| 靶机 | Management |
| 难度 | Easy |
| 目标 IP | 10.129.159.168(初始扫描) |
| 实战日期 | 2026-11-01 至 2026-11-02(北京时间) |
| 完成情况 | 已取得 user flag 和 root flag,未建立交互式 root shell |

图中的实例 IP 为 10.129.160.183,本文首次扫描使用的是 10.129.159.168。
路径概览
打完后回头看,主要过程可以串成下面这条线:
已验证的关键节点
01 · 从 Web 入口定位 OpenAM
扫描发现 Web 与 LDAP 服务,Client Login 跳转到 SSO;先后检查前端资源、JATO 参数和匿名用户权限。
02 · 获得 openam Shell
修正 PoC 的目标入口后取得命令执行回显,再建立服务账户 Shell。
03 · 从 GLPI 凭据切换到 owen
读取 GLPI 配置和 LDAP 服务账户密文,解密后验证密码复用,取得 user flag。
04 · 通过备份读取 root flag
利用 owen 的 sudo 备份规则绕过路径限制,将 /root 复制到可读取的位置,取得 root flag。
信息收集
端口扫描
先用 Nmap 看一下开放了哪些服务:
1 | sudo nmap -sS -sV -sC -T4 -n --reason -O -oA nmap_10.129.159.168 10.129.159.168 |
| 端口 | 服务 | 当时获得的线索 |
|---|---|---|
| 22 | SSH | OpenSSH 9.6p1,Ubuntu |
| 80 / 443 | HTTP / HTTPS | nginx 1.24.0;证书包含 management.htb 与通配域名 |
| 4444 | SSL / LDAP | 证书指向 sso.management.htb |
| 50389 | LDAP | Nmap 提示 Anonymous bind OK |
展开 Nmap 原始输出摘录
关键输出:
1 | 22/tcp open ssh syn-ack ttl 63 OpenSSH 9.6p1 Ubuntu 3ubuntu13.19 (Ubuntu Linux; protocol 2.0) |

发现 SSH、HTTP、HTTPS 和 LDAP 服务。
其中 LDAP 扫描结果提示允许匿名绑定,但当时我还不了解 LDAP 协议和具体利用方法。
Web 页面与 SSO 入口
LDAP 暂时没什么思路,我先去看看 Web 页面。
在 management.htb 首页看到统一身份认证、资产管理、监控和备份等介绍,先保留这些信息。
首页文字与状态信息(原始摘录)
1 | The platform |

点击 Client Login 后跳转到:
1 | https://sso.management.htb/openam/XUI/#login/ |

OpenAM 版本线索与漏洞检索
发现这是 OpenAM 登录页面,接下来使用 Burp Suite 查看前端请求中的版本线索。
1 | GET /openam/XUI/templates/common/NavigationTemplate.html?v=16.0.5 |
请求里带着 v=16.0.5,我先把它当成版本线索,试着搜索对应漏洞。
展开 SearchSploit 检索记录
使用 SearchSploit 搜索对应版本:
1 | ┌─[✗]─[meng@parrot]─[~/labs/htb/Management] |
没有找到对应版本的结果,接下来查看页面源代码是否留有信息。
页面源码与加密前端资源

OpenAM 登录页 HTML 源码
1 |
|
看到 base64-1.0.0-min.js 时,我一开始以为前后端通信做了什么加密。后来才理清:Base64 只是编码,单凭加载这个库还看不出它具体处理了什么。
接下来查看 https://management.htb/ 的源代码,发现页面会下载 assets/app.enc,再使用脚本内给出的密钥执行 AES-GCM 解密。
展开前端资源加载脚本
1 | (function(){ |
这里提供了密钥、加密文件位置和解密函数。接下来尝试将文件下载到本地。
1 | ┌─[meng@parrot]─[~/labs/htb/Management] |
当时看到下载完成,误以为已经取得加密文件;后续检查确认这次仅下载了 178 字节的 HTTP 重定向页面。
接下来卡在解密步骤,不知道如何继续。
解密前端资源
页面脚本说明 assets/app.enc 使用 Base64 包装,按这段加载脚本的格式,解码后前 12 字节为 AES-GCM IV,其余为密文和 authentication tag;页面中给出了 Base64 编码的 AES key。
第一次使用 HTTP 下载时得到 Nginx 301 Moved Permanently 页面。改用 HTTPS 并跟随重定向后,成功取得 Base64 文件,并使用 Node.js crypto 按 AES-256-GCM 解密,拆出前 12 字节 IV 和末尾 16 字节 authentication tag,生成 app.dec.js。
1 | 解密完成:63863 bytes,已写入 app.dec.js |
文件解出来了,但里面还是一堆 _0x... 形式的混淆代码。我当时没有从这里继续找到可用的接口或域名线索。

我也在浏览器 Console 中查看了解码后的内容,并复制保存为 app.dec.runtime.js。
OpenAM 漏洞探测与权限枚举
转向 JATO 参数验证
前端这条线暂时卡住后,我参考资料把方向转到了 CVE-2026-33439,关注点是 jato.clientSession 的 Java 反序列化处理。接下来先找目标上的对应入口,看看请求和响应有什么变化。
JATO 密码重置入口确认
在访问 /openam/ui/PWResetUserValidation 的正常密码重置流程中捕获到:
- 方法:
POST - Content-Type:
application/x-www-form-urlencoded - 表单字段:
PWResetUserValidation.fldUserAttr、PWResetUserValidation.tfUserAttr、PWResetUserValidation.btnNext、jato.defaultCommand - 当前请求包含
jato.pageSession,其值以rO0AB开头,符合 Java 序列化数据经过 Base64 编码后的特征。
这里能看到 JATO 表单和序列化会话参数,不过正常请求带的是 jato.pageSession,还不是我要检查的 jato.clientSession。两个参数不能混为一谈。
这一阶段按顺序做了五次参数验证。最明显的差异是:普通字符串被清空,而 HashMap 值会被保留。
| 顺序 | 输入与位置 | 观察结果 |
|---|---|---|
| 1 | POST 表单中的空值 | HTTP 200,密码重置错误与基线一致 |
| 2 | POST 表单中的字符串 A | 页面重新渲染空的 hidden input |
| 3 | POST 表单中的序列化 Java String | 相同错误,hidden input 仍为空 |
| 4 | Query 中的序列化 HashMap | hidden input 原样保留该值 |
| 5 | 本地自定义 Comparator 的 PriorityQueue | 参数被清空,未复现本地触发输出 |
这一阶段尚不能确认 RCE。 参数被保留只是对象处理行为的线索;HTTP 200、页面长度变化和本地 Comparator 输出,都不能代替目标上的命令执行证据。
展开五次验证的请求与响应记录
随后在同一个 POST 表单中加入空的 jato.clientSession:
1 | ...&jato.pageSession=<原值>&jato.clientSession= |
服务端返回 HTTP 200,并显示 The password cannot be reset for this user.,响应仍是密码重置页面。这次只是把参数放进 POST 表单发了出去;响应与基线一致,单凭 HTTP 200 看不出服务端是否处理了这个参数。
随后将 jato.clientSession 改为普通字符串 A。服务端仍返回 HTTP 200 和相同的密码重置错误,但响应长度增加,并重新渲染了空的 jato.clientSession hidden input。我能确认的是响应中该字段为空;究竟是输入被忽略、处理失败还是被归一化,仅凭页面还分不清。
随后使用合法 Java String 序列化值 rO0ABXQAD21hbmFnZW1lbnQtdGVzdA 作为 jato.clientSession 的 POST 字段。服务端仍返回 HTTP 200、相同的密码重置错误和空的 hidden input;当前结果未出现解析异常或命令执行。该测试仍携带了登录 Cookie,且参数位于 POST 表单中,不能作为预认证漏洞的最终判断。
随后将正常 jato.pageSession 的 HashMap 序列化值放入 Query 参数 jato.clientSession,并保持 POST 表单中的 jato.pageSession。服务端返回 HTTP 200,响应中的 jato.clientSession hidden input 原样保留该 HashMap 值,而普通字符串测试会被清空。该差异与目标接受符合预期结构的对象这一推测相符,但这次也改变了参数位置,尚不能隔离判断差异的原因;目前仍只是无害对象验证,没有执行命令。
随后将本地 PQTest$DemoComparator 序列化的 PriorityQueue 放入 jato.clientSession。服务端仍返回 HTTP 200,但响应把 jato.clientSession 清空,页面流程未改变。可能是目标缺少 PQTest$DemoComparator,也可能是类型检查或其他处理失败;没有服务端异常信息,还不能确定原因。这次没有在目标上复现本地 Comparator 的触发效果。
请求关键字段对照
我主要对比了下面两种传参方式,这里只列关键部分,会话值用占位符表示。
POST 表单中的普通字符串:
1 | POST /openam/ui/PWResetUserValidation |
返回 HTTP 200,页面提示 The password cannot be reset for this user.,响应里的 jato.clientSession hidden input 为空。
Query 中的 HashMap:
1 | POST /openam/ui/PWResetUserValidation?jato.clientSession=<HashMap 序列化值> |
这次把正常 jato.pageSession 中的 HashMap 值放进 Query,原来的 POST 表单仍然保留。响应也是 HTTP 200,但 hidden input 留下了传入的值。
这里有个问题:我同时改了参数内容和位置,所以还不能确定是哪一个因素造成了差异。到这一步也没有命令执行回显,不能算利用成功。
匿名会话权限枚举
使用已建立的匿名会话观察 Dashboard 请求,记录到以下结果(会话 Cookie 与 token 未写入):
GET /openam/json/serverinfo/*返回实例配置:realm 为/,域名为.management.htb,secureCookie=false、cookieSameSite=null、allowedWithoutReferer=true,自助注册和忘记密码功能关闭。GET /openam/json/realms/root/users/anonymous返回匿名用户资料。角色仅为ui-self-service-user,状态为Active,LDAP DN 为uid=anonymous,ou=people,dc=management,dc=htb,universal ID 为id=anonymous,ou=user,dc=management,dc=htb。GET /openam/json/realms/root/dashboard/assigned返回空分配结果:{"_id":"0","_rev":"0"}。GET /openam/json/realms/root/users/anonymous/oauth2/applications?_queryFilter=true返回result: [],当前用户没有 OAuth2 应用。GET /openam/json/users?_action=idFromSession返回 400Unrecognized request parameter '_action',该请求尚未证明可用,需结合实际方法、路径和 API 版本继续核对。
匿名账户能进入 Dashboard,也能看到自己的资料和部分实例配置,但没有分配应用。我还没找到能从这里进入管理功能的办法。

API 文档权限观察
在匿名会话的 OpenAM Dashboard 帮助菜单中发现 API Explorer 和 API Docs,访问 API Docs 时显示 403 页面。我的账户只有 ui-self-service-user 角色,接下来就去看看这个 403 是在哪里拦下来的。
API Docs 路由权限
OpenAM XUI main.js 中的路由配置如下(整理展示时的转义):
1 | apiDocView: { |
这里要求 ui-global-admin,而我的匿名账户只有 ui-self-service-user。结合 Network 来看,这次是 XUI 的路由检查先拦住了页面,加载的是 403.html 模板,并没有发出 API Docs 的后端请求。
进一步在 XUI 已加载模块中观察到权限分层:GlobalRoutes.js 主要使用 ui-global-admin,RealmsRoutes.js 使用 ui-realm-admin,AMRoutesConfig.js 和 UserRoutesConfig.js 包含 ui-self-service-user/ui-user 路由;AppConfiguration.js 也按这些角色控制菜单可见性。既然当前只有 ui-self-service-user,我就继续检查 self-service 路由和它们调用的 API。
各路由文件的具体观察如下,按当时的审阅顺序排列:
路由审阅结果:
ui-global-admin:全局认证设置(configure/authentication)、全局服务(configure/global-services)、部署站点(deployment/sites)、部署服务器(deployment/servers),以及服务器编辑/服务器默认配置的general、security、session、sdk、cts、uma、advanced、directoryConfiguration等树节点。ui-realm-admin:全局服务下的子 Schema 新建、编辑和嵌套编辑(configure/global-services/.../new、.../edit/...)。- 文件中没有未设置
role的管理路由;所有路由均属于navGroup: "admin"。
看完这个文件,没有找到匿名账户可以直接进入的管理路由。
路由权限分析:
dashboard:要求ui-self-service-user,匿名账户具备该角色,已实际访问。oauth2Tokens:要求ui-user,匿名账户当前没有该角色,预计会被拒绝,需以实际响应验证。confirmLogin:要求ui-user,属于登录确认流程。continuePasswordReset、continueSelfRegister、loggedOut、loginFailure、sessionExpired:未声明role,但它们是认证生命周期辅助页面,不是管理功能。实例serverinfo已显示忘记密码和自助注册均关闭,因此不能仅因缺少role就推断可利用。
当时我打算继续看 Dashboard 的 self-service API;OAuth2、密码重置和注册这几条线还没有验证出可用结果。
路由权限分析:
profile:要求ui-self-service-user,匿名账户可以访问个人资料页面。forgotUsername、passwordReset、selfRegistration:未声明role,属于找回用户名、密码重置和注册流程;结合实例配置中的forgotUsername=false、forgotPassword=false、selfRegistration=false,它们不是当前可用的管理入口。
个人资料页可以打开,我当时主要查看它调用的 API,没有提交资料或密码修改。

漏洞利用与获取 Shell(2026-11-02)
前面的参数测试和权限枚举暂时没有继续推进,接下来我用 PoC 验证命令执行。
OpenAM 命令执行与 openam Shell
一开始,我把 PoC 的 --url 指向 https://sso.management.htb/openam/XUI/#login/,只得到 OpenAM 的前端 HTML。该 URL 中 #login/ 是浏览器端 fragment,不会发送给服务器,且 XUI 登录页不是候选漏洞的后端入口。
将目标改为:
1 | https://sso.management.htb/openam/ui/PWResetUserValidation |
执行 id 后回显:
1 | uid=996(openam) gid=987(openam) groups=987(openam) |
随后建立 reverse shell,whoami 回显 openam。/home/owen 存在,但权限为 drwxr-x--- owen owen,openam 无法进入。


凭据获取与用户切换
GLPI 配置与数据库访问
OpenAM Java 进程以 openam 运行,启动参数给出 /opt/openam/config。手工枚举中没有发现直接可用的 SUID、capability 或普通 cron 提权点;但发现 /opt/glpi/config/config_db.php 对 openam 可读。

配置里有本地 MariaDB 的 GLPI 账户。我用它连上了 glpidb,随后从 glpi_authldaps 读取到:
1 | rootdn: cn=svc-glpi,ou=services,dc=management,dc=htb |
解密 LDAP 服务账户密码
GLPI 配置目录中的 glpicrypt.key 同样可读。我在 /opt/glpi 下调用项目自带的 GLPIKey()->decrypt() 解密该密文,解出了一条 12 字符的明文密码。

看到 WP 中提到密码复用,我就拿这条密码试着切换到 owen,结果成功了。
密码复用取得 owen 与 user flag
切换用户时,输入刚才解出的密码:
1 | su - owen |
切换成功后,id 返回:
1 | uid=1000(owen) gid=1000(owen) groups=1000(owen) |
进入 /home/owen 后读到了 user.txt。拿到 user flag,接下来继续看 sudo 权限。

备份权限利用与 root flag
owen 的 sudo 备份权限
以 owen 执行 sudo -l 得到:
1 | (root) NOPASSWD: /usr/bin/rdiff-backup --server --restrict-path /opt/backup |

该规则原意是只读访问 /opt/backup,但末尾 * 允许附加参数。WP 提到,这台靶机上的重复 --restrict-path 参数可以覆盖前面的路径限制。我接下来按这个方向尝试备份 /root。
绕过备份路径限制并复制 /root
我以 owen 身份运行带 --remote-schema 的 rdiff-backup 客户端命令,输出里出现了:
1 | NOTE: Starting mirror from source path /root to destination path /tmp/root_backup |

随后 ls -la /tmp/root_backup 显示了 root 的家目录副本,包括 .ssh、rdiff-backup-data 及 root.txt。这次确实把 /root 的内容复制出来了。截图中的命令还触发了旧版命令行兼容提示,所以这里记录的是靶机上实际验证的行为,不把它泛化到所有 rdiff-backup 版本。
获取 root flag
最后读取备份里的 Flag:
1 | cat /tmp/root_backup/root.txt |

成功拿到 root flag,到这里两个 Flag 都齐了。
两个 Flag 都已取得。 最后我是通过备份副本读到 root flag 的,没有拿到交互式 root Shell。
总结反思
这次最值得保留的,是几处判断发生变化的过程:
| 当时的观察 | 这次记录留下的提醒 |
|---|---|
| curl 显示下载完成 | 下载成功不等于内容正确;第一次得到的是 301 页面 |
| 前端加载了 Base64 库 | 库的存在不能证明通信加密,需要继续检查实际逻辑 |
| JATO 参数响应出现差异 | 先记录差异,再区分对象处理与命令执行证据 |
| API Docs 显示 403 | 结合 Network 区分前端路由拦截与后端接口拒绝 |
| 成功读取 root.txt | 准确描述为读取 root 文件,不能写成已获得 root Shell |
参考与后续复习
这次前端解密脚本、OpenAM PoC、回连以及 GLPI 查询和解密命令用到了 AI 帮助;GLPI 凭据链、密码复用和备份绕过也参考了公开 WP,再由我实际验证。
虽然两个 Flag 都拿到了,但 OpenAM 入口怎么选、可逆加密和哈希有什么区别,以及 rdiff-backup 的 remote schema 怎么工作,我还需要脱离原来的命令再梳理一遍。
知识点与阅读索引
这些资料用于补原理。复习时先合上文章自己解释,再按卡住的部分查阅,不必一次全部看完。
| 主题 | 复习时关注的问题 | 资料 |
|---|---|---|
| URL 与前后端路由 | 浏览器中的 URL、发出的请求和页面路由如何对应? | MDN:URI fragment |
| Base64 与 AES-GCM | 文件的编码层、IV、密文和认证标签分别是什么?Node.js 如何验证解密结果? | Node.js 20:Crypto / setAuthTag |
| Java 反序列化 | 如何识别序列化数据?格式特征、对象处理与可利用性之间还缺哪些证据? | OWASP:Deserialization Cheat Sheet |
| OpenAM 漏洞范围 | 公告中的产品、受影响版本和修复版本如何与目标证据对应? | OpenIdentityPlatform 官方公告:CVE-2026-33439 |
| 密码存储与服务凭据 | 登录口令校验与应用访问另一个系统时,对凭据的需求有什么区别? | OWASP:Password Storage / Hashing vs Encryption |
| 备份的权限边界 | 客户端、服务端、remote schema 和限制参数分别在哪一层起作用? | rdiff-backup 官方手册(重点看 REMOTE OPERATION、RESTRICT OPTIONS,并对照实际版本) |
补充笔记:参数测试如何做对照
这次 JATO 测试同时改了对象内容和参数位置,难以分清响应差异的来源。后续做类似验证,可以先固定入口、请求方法、会话和参数位置,每轮只改变一个因素;再单独比较 Query 与 POST。这样得到的差异才更容易解释。
除了状态码,还应比较响应体和错误信息,并保留正常请求作基线。某个字段被回填或被清空,不能直接推出内部调用成功,也不能替代命令执行证据。这是对本次测试方法的复盘,不代表当时已经完成了全部控制变量测试。


![[HTB] Cap 渗透测试笔记](https://img.nuyoah.space/post/postcover/cce3e506-fa7d-457f-ba65-d3b74d503510.png)
![[HTB] Management 渗透测试笔记](https://img.nuyoah.space/post/postcover/0a83fd14-514e-4d04-8781-46cd4c7939c9.png)