ProxyAsAService
3 minutes to read
We are given a website that redirects to /r/catvideos
:
We are also given the source code of the web application, which is build with Flask (Python).
Source code analysis
There are some endpoints for a proxy and debugging options (application/index.py
):
from flask import Flask, jsonify
from application.blueprints.routes import proxy_api, debug
app = Flask(__name__)
app.config.from_object('application.config.Config')
app.register_blueprint(proxy_api, url_prefix='/')
app.register_blueprint(debug, url_prefix='/debug')
@app.errorhandler(404)
def not_found(error):
return jsonify({'error': 'Not Found'}), 404
@app.errorhandler(403)
def forbidden(error):
return jsonify({'error': 'Not Allowed'}), 403
@app.errorhandler(400)
def bad_request(error):
return jsonify({'error': 'Bad Request'}), 400
These are the routes (application/blueprints/routes.py
):
from flask import Blueprint, request, Response, jsonify, redirect, url_for
from application.util import is_from_localhost, proxy_req
import random, os
SITE_NAME = 'reddit.com'
proxy_api = Blueprint('proxy_api', __name__)
debug = Blueprint('debug', __name__)
@proxy_api.route('/', methods=['GET', 'POST'])
def proxy():
url = request.args.get('url')
if not url:
cat_meme_subreddits = [
'/r/cats/',
'/r/catpictures',
'/r/catvideos/'
]
random_subreddit = random.choice(cat_meme_subreddits)
return redirect(url_for('.proxy', url=random_subreddit))
target_url = f'http://{SITE_NAME}{url}'
response, headers = proxy_req(target_url)
return Response(response.content, response.status_code, headers.items())
@debug.route('/environment', methods=['GET'])
@is_from_localhost
def debug_environment():
environment_info = {
'Environment variables': dict(os.environ),
'Request headers': dict(request.headers)
}
return jsonify(environment_info)
As can be seen, the root endpoint (/
) takes one random subreddit and redirects there if there is no url
parameter. Otherwise, it adds the url
parameter to the HTTP Request URI:
target_url = f'http://{SITE_NAME}{url}'
The function proxy_req
appears in application/util.py
:
from flask import request, abort
import functools, requests
RESTRICTED_URLS = ['localhost', '127.', '192.168.', '10.', '172.']
def is_safe_url(url):
for restricted_url in RESTRICTED_URLS:
if restricted_url in url:
return False
return True
def is_from_localhost(func):
@functools.wraps(func)
def check_ip(*args, **kwargs):
if request.remote_addr != '127.0.0.1':
return abort(403)
return func(*args, **kwargs)
return check_ip
def proxy_req(url):
method = request.method
headers = {
key: value for key, value in request.headers if key.lower() in ['x-csrf-token', 'cookie', 'referer']
}
data = request.get_data()
response = requests.request(
method,
url,
headers=headers,
data=data,
verify=False
)
if not is_safe_url(url) or not is_safe_url(response.url):
return abort(403)
return response, headers
This function checks that we are not trying to access local websites, among other things. However, the verification is not exhaustive because we still can pass other IP address representations (decimal, hexadecimal…).
It is clear that we need to access localhost
to be able to call /debug
and read environment variables:
@debug.route('/environment', methods=['GET'])
@is_from_localhost
def debug_environment():
environment_info = {
'Environment variables': dict(os.environ),
'Request headers': dict(request.headers)
}
return jsonify(environment_info)
In the Dockerfile
, the flag is added as an environment variable:
# Place flag in environ
ENV FLAG=HTB{f4k3_fl4g_f0r_t3st1ng}
Nevertheless, the function is_from_localhost
will always fail because we are outside the server and request.remote_addr
will always be different from 127.0.0.1
.
HTTP Request URI
The idea of this challenge is to somehow trick the server to redirect to 127.0.0.1
. We can’t simply add this to url
because we are not performing the request from localhost
.
The problem here is that url
is appended to SITE_NAME
(reddit.com
) without any slash (/
). As a result, we can add a special @
sign. With this, we will be able to specify the host to perform the request. This happens because the HTTP Request URI has this format (more information at Wikipedia):
http[s]://[username[:password]@]domain-or-ip[:port]
Therefore, we can enter something like @127.0.0.1:1337/debug/environment
, and the HTTP Request URI will be:
http://reddit.com@127.0.0.1:1337/debug/environment
The web server will think reddit.com
is the username, which is useless since HTTP Basic Authentication is not used in this web application. Then, the request will go to 127.0.0.1:1337/debug/environment
from localhost
, so we bypass the is_from_localhost
function.
In addition, we need to use another value for 127.0.0.1
, for instance, 0x7f000001
(hexadecimal format).
Flag
Now, if we go set url=@0x7f000001:1337/debug/environment
, we will see the flag:
HTB{fl4gs_4s_4_S3rv1c3}