Insomnia
4 minutes to read
We are given a website like this:
We also have the source code in PHP.
Source code analysis
There is a controller in Controllers/UserController.php
that handles both “signin” and “signup” actions:
<?php
namespace App\Controllers;
use CodeIgniter\Controller;
use CodeIgniter\API\ResponseTrait;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
class UserController extends Controller
{
use ResponseTrait;
public function LoginIndex()
{
return View("LoginPage");
}
public function login()
{
$db = db_connect();
$json_data = request()->getJSON(true);
if (!count($json_data) == 2) {
return $this->respond("Please provide username and password", 404);
}
$query = $db->table("users")->getWhere($json_data, 1, 0);
$result = $query->getRowArray();
if (!$result) {
return $this->respond("User not found", 404);
} else {
$key = (string) getenv("JWT_SECRET");
$iat = time();
$exp = $iat + 36000;
$headers = [
"alg" => "HS256",
"typ" => "JWT",
];
$payload = [
"iat" => $iat,
"exp" => $exp,
"username" => $result["username"],
];
$token = JWT::encode($payload, $key, "HS256");
$response = [
"message" => "Login Succesful",
"token" => $token,
];
return $this->respond($response, 200);
}
}
public function RegisterIndex()
{
return View("RegisterPage");
}
public function register()
{
$db = db_connect();
$json_data = request()->getJSON(true);
$username = $json_data["username"] ?? null;
$password = $json_data["password"] ?? null;
if (!($username && $password)) {
return $this->respond("Empty username or password", 404);
} else {
// Check if the username already exists
$existingUser = $db
->table("users")
->where("username", $username)
->get()
->getRow();
if ($existingUser) {
return $this->respond("Username already exists", 400);
}
// Insert the new user if the username is unique
$db->table("users")->insert([
"username" => $username,
"password" => $password,
]);
if ($db->affectedRows() > 0) {
return $this->respond(
"Registration successful for user: " . $username,
200
);
} else {
return $this->respond("Registration failed", 404);
}
}
}
}
The vulnerable function is login
, because we can control $json_data
, which is passed directly to $db->table("users")->getWhere
:
public function login()
{
$db = db_connect();
$json_data = request()->getJSON(true);
if (!count($json_data) == 2) {
return $this->respond("Please provide username and password", 404);
}
$query = $db->table("users")->getWhere($json_data, 1, 0);
$result = $query->getRowArray();
// ...
}
We want to log in as administrator
because that way we can get the flag, as shown in Controllers/ProfileController.php
:
<?php
namespace App\Controllers;
use App\Controllers\BaseController;
use CodeIgniter\HTTP\ResponseInterface;
use Config\Paths;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
class ProfileController extends BaseController
{
public function index()
{
$token = (string) $_COOKIE["token"] ?? null;
$flag = file_get_contents(APPPATH . "/../flag.txt");
if (isset($token)) {
$key = (string) getenv("JWT_SECRET");
$jwt_decode = JWT::decode($token, new Key($key, "HS256"));
$username = $jwt_decode->username;
if ($username == "administrator") {
return view("ProfilePage", [
"username" => $username,
"content" => $flag,
]);
} else {
$content = "Haven't seen you for a while";
return view("ProfilePage", [
"username" => $username,
"content" => $content,
]);
}
}
}
}
And this administrator
user already exists because it is generated inside entrypoint.sh
:
#!/bin/bash
# Initialize SQLite database with a table and an initial user
touch /var/www/html/Insomnia/database/insomnia.db
chmod 666 /var/www/html/Insomnia/database/insomnia.db
sqlite3 /var/www/html/Insomnia/database/insomnia.db <<'EOF'
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
username TEXT NOT NULL,
password TEXT NOT NULL
);
INSERT INTO users (username, password) VALUES ('administrator', LOWER(hex(randomblob(16))));
EOF
# Create JWT secret key
echo "JWT_SECRET='$(openssl rand -hex 32)'" >> /var/www/html/Insomnia/.env
# Start Apache server
apache2-foreground
Solution
Therefore, we must come up with a JSON payload that allows us to log in as administrator
given that we control the input to getWhere
function.
According to CodeIgniter’s documentation (the PHP framework that is being used in this challenge), we must enter the fields and values we want to select from the table of the SQL database.
Notice that the server checks that the total number of parameters in the JSON document is 2 (they should be username
and password
). However, we can use id
and username
instead.
As a result, a payload like this will work:
{"id": 1, "username": "administrator"}
Implementation
To implement the attack, we can try to log in like this:
Now, we can edit the request and replace password
by id
:
Now, we have a valid JWT token:
We only need to put it as a cookie, and we are done:
According to Config/Routes.php
, we must go to /index.php/profile
to read the flag:
<?php
use App\Controllers\ProfileController;
use CodeIgniter\Router\RouteCollection;
use App\Controllers\UserController;
/**
* @var RouteCollection $routes
*/
$routes->get('/', 'Home::index');
$routes->get('/login',[UserController::class,'LoginIndex']);
$routes->post('/login',[UserController::class,'login']);
$routes->get('/register',[UserController::class,'RegisterIndex']);
$routes->post('/register',[UserController::class,'register']);
$routes->get('/profile',[ProfileController::class,'index'],['filter' => 'authenticated' ]);
Flag
And here we have the flag:
HTB{I_just_want_to_sleep_a_little_bit!!!!!}