From 665c01ccc6100176e173b6fdd4723d789f5a7f6f Mon Sep 17 00:00:00 2001 From: Raphael Ludwig Date: Tue, 7 Jan 2020 16:42:03 +0100 Subject: [PATCH] Raffy23: Add asis2019 writeup --- writeups/Raffy23/asis2019.md | 141 +++++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 writeups/Raffy23/asis2019.md diff --git a/writeups/Raffy23/asis2019.md b/writeups/Raffy23/asis2019.md new file mode 100644 index 0000000..f680b9c --- /dev/null +++ b/writeups/Raffy23/asis2019.md @@ -0,0 +1,141 @@ +ASIS CTF 2019 - Protected Area 1 & 2 +============================ +To participate at the ASIS CTF was more or less a last-minute decision and in retrospective I should have invested more time at the beginning, since at the end I was not only out of ideas on how I can proceed further, but there was also little time left to actually try something else. The Mattermost discussions really helped, since as a team we were more efficient and it was easy to share knowledge about the following challenges. I often used "we" in the writeups below, which does refer to everybody that participated in the Mattermost discussion and does also underlines that solving the first challenge was a real team effort. + + +## Overview +Protected Area 1 and 2 are both web-services which have rather unimpressive UI. There is no usage of any CSS or inline styling. It only consists of a single HTML page and a single JavaScript file called `app.js`. The services are providing access to files in a certain directory. Protective Area 2 is the second stage of Protected Area 1, which is "more" secure than it's predecessor and was released after Protected Area 1. In both challenges only the `public.txt` file is presented to the user and there is no input field to choose which file is presented to the user. + +## Protected Area 1 (Solved) +First I looked at the JavaScript file since there wasn't really anything else to analyze. This file contains 3 functions and a document ready listener, which does invoke the `file_check` function with "public.txt" as parameter. When looking at the network tab in the chrome developer window we can see two requests when the page is loaded: +- GET `/check_perm/readable/?file=public.txt` +- GET `/read_file/?file=public.txt` + +The `/check_perm/readable/` endpoint did return "True" while the contents of `/read_file` endpoint are displayed on the page. Since there isn't any flag in the frontend UI code I assumed that we can utilize a directory traversal to read a flag.txt file in a directory on the server. + +### Directory traversal +To check if a directory traversal works I added `../` in front of the file name `public.txt`. For the `/check_perm/readable` endpoint the result was not "True" but "0" while the `/read_file` endpoint does still return the contents of the public.txt file. Therefore we can assume that the string `../` is most likely filtered in the `/read_file` endpoint. While we currently can't get the file contents we still can enumerate the file system with the `/check_perm/readable` endpoint. So after a few tries I was able to find some files which might be able to give us a hint of which service might be used: +- `../../static/app.js` +- `../../../etc/passwd` +- `../../../usr/lib/python2.7/LICENSE.txt` +- `../../../usr/lib/python3.5/LICENSE.txt` + +Since now we know that there are two versions of python installed and the result of the `/check_perm/readable` endpoint returns "True" we can assume that the service is written in python. With this knowledge I started to search for typical python files like `main.py` and `app.py`. While I was able to find such files it turned out that reading the contents of the files is not as trivial as I thought: +- Simply adding `../` in front of the file name doesn't work +- Choosing any other extension then `.txt` leads to a "security" error + +### Arbitrary file read +Assuming that a regex or a simple string replace was used to filter out `../` from the URL parameter we could replace `../` with `....//`, which would result in the target string after a "bad" string is filtered out. If the filtering is applied as long as something is is found that method would not work. But nevertheless I tried it since I was out of ideas on how I could approach this problem in any other way. To test my assumption I tried to read the LICENSE files of python since they do they the `.txt` file extension and therefore don't trigger the security error. This resulted in the following curl statement: +``` +$ curl http://$SERVER/read_file/?file=....//....//....//usr/lib/python2.7/LICENSE.txt +``` + +Now we only have to find out why the security exception is triggered when trying to read files that do not end with `.txt`. Since I was already out of ideas I started to fuzz the URL, including the `file` parameter, by hand to maybe find a parameter which could disable the security check. At some point I simply swapped the occurrences of two parameters and was able to read a file or even get the access denied to the file: +- `/read_file/?file=private.txt&debug=true` => security +- `/read_file/?debug=true&file=private.txt` => file contents of private.txt + +After a closer inspection it looks like as if the server would check the whole URL with the following regex: `^.*\.txt$` or would use a simple `endsWith('.txt')` to "enforce" the security rule that only text files could be read. + +### Inspecting the server code +With that trick we can craft a URL that would allow us to simply dump all files, which are readable to the application. So by simply by appending `&a=.txt` we can bypass the "security" filter. The `main.py` and `app.py` files reveal to us that this is indeed a flask application and a closer inspection of the `api.py` file shows that we can access the flag simply by accessing the `/protected_area_0098` endpoint: + +```python +from flask import current_app as app +from flask import request, render_template, send_file +from .functions import * +from config import * +import os + +[...] + +@app.route('/protected_area_0098', methods=['GET']) +@check_login +def app_protected_area() -> str: + return Config.FLAG + +[...] +``` + +A quick look into the `config.py` revealed also that this would be the only way since the flag is loaded via an environment variable: + +```python +import os + +class Config: + """Set Flask configuration vars from .env file.""" + + # general config + FLAG = os.environ.get('FLAG') + SECRET = "s3cr3t" + ADMIN_PASS = "b5ec168843f71c6f6c30808c78b9f55d" +``` + +### Dumping the flag +Since we know the `SECRET` and `ADMIN_PASS` from the config file and know that the `check_login` function uses a MD5 hash for verification we can simply generate the hash and pretend to be the admin and have access to the flag: +```python +[...] + +ah = request.headers.get('ah') + +if ah == hashlib.md5((Config.ADMIN_PASS + Config.SECRET).encode("utf-8")).hexdigest(): + return f(*args, **kwds) +else: + return abort(403) +``` + +So with a simple curl we can get the flag: +``` +$ curl -H "ah: cbd54a3499ba0f4b221218af1958e281" http://$SERVER/protected_area_0098 +ASIS{f70a0203d638a0c90a490ad46a94e394} +``` + + +## Protected Area 2 (Not solved) +The Protected Area 2 challenge is very similar to the first one. Even the description does only mention some more "security" measures to protect the flag against our attacks. So naturally the first thing I did was to try if any of the previous methods worked, but it seems that the "security" check at the `/read_file` endpoint is fixed and therefore we where unable to read any file, which does not have the extension `.txt`. Also very interesting is that we do not only get an error code, but an actual exception from the web server when accessing a file that does not exist: +``` +$ curl "http://$SERVER/read_file/?file=t../x../t" +[Errno 2] No such file or directory: '/files/txt' +``` + +From that exception we can assume that more security through obscurity was used since the files are not hosted anymore in `/app/application/files` but directly from `/files`. Also while it does seem very helpful for the attacker to get the reason why a request could not be processed correctly, it did not help us much since we where already out of ideas most of the time. But there was also another difference to the previous challenge: The contents of the file `private.txt` did change to give us an additional hint, since the following git repository was linked: [uwsgi-nginx-flask-docker](https://github.com/tiangolo/uwsgi-nginx-flask-docker). + +### Arbitrary file read +Since we didn't know how to proceed and we could not read arbitrary files anymore the only thing left to do was to try again and fuzz the URL in a way that would enable us to read files. After a closer inspection of the `/check_perm/readable` endpoint we discovered that the path segment `readable` actually revers to the readable function of the `TextIOWrapper` object in python: +``` +$ curl http://$SERVER/check_perm/wtf/?file=public.txt +'_io.TextIOWrapper' object has no attribute 'wtf' +``` + +A quick look into the [python 3 documentation](https://docs.python.org/3/library/io.html) revealed that there is also a `read` function available in the object, so we could just use `/check_perm/read/?file=public.txt` to read the file contents of any file. + +### Finding the application source +At first we tried to look in `/app/` for the python sources, but there where only the sources of the standard template project available. So I proceeded to look at the `nginx.conf` file: +``` +server { + listen 80; + location / { + try_files $uri @app; + } + + location @app { + include uwsgi_params; + uwsgi_pass unix:///tmp/uwsgi.sock; + } + + location /static { + alias /opt/py/app/static; + } +} +``` + +This shows us that we have to search in `/opt/py/app/` for the sources of the application. So since we know from the previous challenge that the flag details were stored in a `config.py` we simply try to read it in the path we extracted from the `nginx.conf`: +```python +import osclass Config: + """Set Flask configuration vars from .env file."""# general config + FLAG = "flag/flag" + FLAG_SEND = "../flag/flag" +``` + +This was the point where we got stuck. I tried to read the file `/opt/py/app/flag/flag` but was not able to and somehow not only I managed to misspell `uwsgi.ini`. This file did contain contain information about the file names of the main entry point of the application. + +But not long after the CTF has ended a [writeup](https://medium.com/bugbountywriteup/asis-ctf-protected-area-1-2-walkthrough-5e6db7869658?) was published where the author managed to get the flag of this challenge. \ No newline at end of file -- 2.43.0