Insomnia
4 minutos de lectura
Se nos proporciona un sitio web como este:
También disponemos del código fuente en PHP.
Análisis del código fuente
Hay un controlador en Controllers/UserController.php
que gestiona las acciones de “signin” y “signup”:
<?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);
}
}
}
}
La función vulnerable es login
, porque podemos controlar $json_data
, que se pasa directamente a $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();
// ...
}
Queremos iniciar sesión como administrator
porque de esa manera podemos obtener la flag, como se muestra en 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,
]);
}
}
}
}
Y este usuario administrator
ya existe porque se genera en 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
Solución
Por lo tanto, debemos encontrar un payload en JSON que nos permita iniciar sesión como administrator
dado que controlamos la entrada a la función getWhere
.
De acuerdo a la documentación de CodeIgniter (el framework de PHP que se está utilizando en este reto), debemos indicar las columnas y valores que queremos seleccionar de la tabla de la base de datos SQL.
Obsérvese que el servidor verifica que el número total de parámetros en el documento JSON sea 2 (deberían ser username
y password
). Sin embargo, podemos usar id
y username
.
Como resultado, un payload como este funcionará:
{"id": 1, "username": "administrator"}
Implementación
Para implementar el ataque, podemos intentar iniciar sesión así:
Ahora podemos editar la petición y reemplazar password
por id
:
Con esto, tenemos un token JWT válido:
Solo necesitamos ponerlo como una cookie, y listo:
De acuerdo a Config/Routes.php
, debemos acceder a /index.php/profile
para ver la 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
Y aquí tenemos la flag:
HTB{I_just_want_to_sleep_a_little_bit!!!!!}