We are allowed to register and then add new domain. I tried injecting PHP code inside, but it got replaced by comments.
Writeup-1.png
Gitea
Going back to home page for enumeration I noticed Contribute here! link leading to port 3000, which wasn't discovered by RustScan
Writeup-2.png
Application lives in /var/www/microblob
Writeup-3.png
order.txt records all the files that are in content directory.
Writeup-4.png
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.
We are able to write to any file, but no code execution.
Writeup-5.png
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.
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.
We can't write to the file, but because it's added in orders.txt that's why we get LFI.
Writeup-6.png
To pretty print:
Controlling Proxied Host
/etc/nginx/nginx.conf is not helpful, but /etc/nginx/sites-enabled/default was
This clearly smells like SSRF, but I wasn't able to get it to work with domain name in the URL.
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
Note: The space at the end of Redis command is very important as it's the delimiter for proper HTTP request!
Writeup-7.png
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
Pro user's have third option of uploading images.
Writeup-8.png
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
Writeup-9.png
Current user can't do much, root owns most of the stuff. .git has same stuff as Gitea.
We might as well check Redis as that was the database.
Reverse Shell (Automated)
The shell died 💀 automated the process...
Redis Enumeration
SSH (22)
Creds: cooper:zooperdoopercooper
User.txt
Privilege Escalation
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.
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("")