]> git.somenet.org - pub/jan/ctf-seminar.git/blob - writeups/Raffy23/asis2019.md
Add OTW Advent Bonanza write-up, update older writeups with additional information
[pub/jan/ctf-seminar.git] / writeups / Raffy23 / asis2019.md
1 ASIS CTF 2019 - Protected Area 1 & 2
2 ============================
3 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. 
4
5
6 ## Overview
7 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.
8
9 ## Protected Area 1 (Solved)
10 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: 
11 - GET `/check_perm/readable/?file=public.txt`
12 - GET `/read_file/?file=public.txt`
13
14 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. 
15
16 ### Directory traversal
17 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: 
18 - `../../static/app.js`
19 - `../../../etc/passwd`
20 - `../../../usr/lib/python2.7/LICENSE.txt`
21 - `../../../usr/lib/python3.5/LICENSE.txt`
22
23 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:
24 - Simply adding `../` in front of the file name doesn't work 
25 - Choosing any other extension then `.txt` leads to a "security" error
26
27 ### Arbitrary file read
28 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: 
29 ```
30 $ curl http://$SERVER/read_file/?file=....//....//....//usr/lib/python2.7/LICENSE.txt
31 ```
32
33 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: 
34 - `/read_file/?file=private.txt&debug=true` => security
35 - `/read_file/?debug=true&file=private.txt` => file contents of private.txt
36
37 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.
38
39 ### Inspecting the server code
40 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: 
41
42 ```python
43 from flask import current_app as app
44 from flask import request, render_template, send_file
45 from .functions import *
46 from config import *
47 import os
48
49 [...]
50
51 @app.route('/protected_area_0098', methods=['GET'])
52 @check_login
53 def app_protected_area() -> str:
54         return Config.FLAG
55
56 [...]
57 ```
58
59 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: 
60
61 ```python
62 import os
63
64 class Config:
65     """Set Flask configuration vars from .env file."""
66
67     # general config
68     FLAG       = os.environ.get('FLAG')
69     SECRET     = "s3cr3t"
70     ADMIN_PASS = "b5ec168843f71c6f6c30808c78b9f55d"
71 ```
72
73 ### Dumping the flag
74 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: 
75 ```python
76 [...]
77
78 ah = request.headers.get('ah')
79
80 if ah == hashlib.md5((Config.ADMIN_PASS + Config.SECRET).encode("utf-8")).hexdigest():
81         return f(*args, **kwds)
82 else:
83         return abort(403)
84 ```
85
86 So with a simple curl we can get the flag: 
87 ```
88 $ curl -H "ah: cbd54a3499ba0f4b221218af1958e281" http://$SERVER/protected_area_0098
89 ASIS{f70a0203d638a0c90a490ad46a94e394}
90 ```
91
92
93 ## Protected Area 2 (Not solved)
94 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: 
95 ```
96 $ curl "http://$SERVER/read_file/?file=t../x../t"
97 [Errno 2] No such file or directory: '/files/txt'
98 ```
99
100 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). 
101
102 ### Arbitrary file read
103 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: 
104 ```
105 $ curl http://$SERVER/check_perm/wtf/?file=public.txt
106 '_io.TextIOWrapper' object has no attribute 'wtf'
107 ```
108
109 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.
110
111 ### Finding the application source
112 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: 
113 ```
114 server { 
115     listen 80; 
116     location / { 
117         try_files $uri @app; 
118     } 
119
120     location @app { 
121         include uwsgi_params; 
122         uwsgi_pass unix:///tmp/uwsgi.sock; 
123     } 
124
125     location /static { 
126         alias /opt/py/app/static; 
127     } 
128 }
129 ```
130
131 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`:
132 ```python
133 import osclass Config:
134     """Set Flask configuration vars from .env file."""# general config
135     FLAG      = "flag/flag"
136     FLAG_SEND = "../flag/flag"
137 ```
138
139 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.
140
141 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.