Twelve Days of ZAPmas - Day 3 - CYA (Cover Your Auth)

Twelve Days of ZAPmas - Day 3 - CYA (Cover Your Auth)
Mic Whitehorn
Author: Mic Whitehorn

Access control is one of the crucial elements to application security. The vast majority of applications have some data or functionality that’s intended to be limited to a particular user. For example, you can change your own password. In most apps, you’re not allowed to change another user’s password. We would describe that as an authorization boundary between users. Some apps might have the concept of teams, where there’s a permission boundary between teams. Only users on a given team can access certain resources or functions related to that team. In a multi-tenant system, there are going to be boundaries between tenants, where Organization A’s users shouldn’t be able to see Organization B’s stuff.  When it comes to looking at actual HTTP request traffic, we can (and should) check that the authorization is being enforced properly. We can leverage ZAP’s Request Editor to help us analyze that behavior.

Sessions and Auth Tokens of all kinds

The classic example of a session is a cookie containing a strong, generated identifier which is included in requests to the server, and that the server then uses to retrieve your session information for some sort of fast temporary storage.

But really, these same checks can be applied as long as there’s a value in the request representing the user’s identity and (directly or indirectly) level of access. This means in addition to session cookies, this applies to OAuth JWTs, SAML 2.0 tokens and API keys and fully-encrypted tokens that you can’t read. These principles remain the same. Signed tokens that the client can read have additional considerations in the next section.

With that in-mind, below is a sample request, followed by the main points I want to prove when evaluating any given operation in the app.

An HTTP request with the session cookie highlighted.

Is the session ID different each time I receive it?

If I find the request where I received the session, and replay it (unmodified), will I get a different value? In this example, it will be a POST containing the username and password, and I’m looking at the Set-Cookie header in the response. Generally I want to see a different session ID each time. If I get the same one back, I’ll want to explore that further to better understand the behavior.

  1. What if I login on a different machine or in an incognito window?
  2. What if the session has expired?

Is the server actually checking for the presence of the session ID in the request at all? 

Test this by removing the cookie from the request completely and reissuing it with the Request Editor.

The same HTTP request as previously pictured, with a highlight showing where the cookie header has been removed.

If authorization is being enforced, I should receive a response that is different from the original response. In most cases, this should be a 4xx error or a redirect. In my sample app, it was a 303 redirect, sending the user to /login

An HTTP response with a 303 See Other status for a redirect to /login.

Is the server validating the session ID or just checking for its presence?

It’s unlikely that the app is just checking if the cookie is there, but not doing anything with it. However, I’m hired to do penetration testing, not penetration assuming. I’ll prove the hypothesis by replacing the cookie value with another value that I’m certain isn’t a valid session, as pictured.

An HTTP request with a session cookie holding a value of "my fake session value".

In my app, this produced the same 303 response as when the cookie was missing outright.  In most apps, this will be the case, because an invalid session and an expired session are often indistinguishable to the server-side process performing the lookup.

Is the app enforcing authorization correctly based on that session’s rights? 

There are a few setup steps for this test, but I need to start by identifying a transaction that’s a good candidate for it.  Here are the main qualities I’m looking for:

  1. I need to be able to target a resource I’m not supposed to have access to. The request from the previous examples was simply loading a page that required authentication, and it was at the root of the application so would most likely be a landing page that everyone would have. This wouldn’t be a good candidate, since every user would have access to, even though it might present different data depending on whose session was active. Ideally I’m looking for a transaction that takes a resource ID as a parameter. For example, /updateUser?userid=123 or /employee/62abadc36/compensation. The parameter could certainly be in the request body as well, but the point is that I want to be able to target a specific resource that one of my user accounts is allowed to access, and another user account I have should not be able to. 
  2. UI-level authorization isn’t necessarily a good candidate. This depends on the application architecture and technology stack. Pages can work as well, if they’re for privileged access, e.g. if a non-admin can go to an admin view at /userAdmin.aspx and use that interface, that’s a serious issue. On the other hand, if you navigate there, and are presented with a broken view because all of the server calls that retrieve data are enforcing authorization correctly, and all the interactions to create/edit/delete data are also enforcing authorization, there may not be much tangible risk. Similarly, single page apps (SPAs) can pretty universally be manipulated into showing privileged parts of the UI, there’s usually not really any feasible way to prevent it, but the risk is negligible if the authorization control on the API is done correctly.

For this example, let’s assume I see this request structure to add a user to a team. The current session showing belongs to a user who is a member of the team and has those permissions.

An HTTP request highlighting the team ID and user ID locations in both the route parameters and the JSON payload.

Once I decide I want to test this request, I would use the browser’s Developer Tools to delete the cookies.

The browser's developer tools, showing the cookies for the current domain. The context menu is open, with the option highlighted for Delete All From "wayfarer.test".

Note that I wouldn’t want to use the Logout functionality in the application, because that should invalidate the session. This will allow me to return to the login and use another account, while leaving the previous session alive until the server cleans it up as part of its session timeout policy.

Once I login with my other account, this one on the other side of the authorization barrier, I can find that request to add a team member, and open it in the Request Editor.

A POST request to add a user to a team. Highlighted areas for the userId, the session cookie, and the role being equal to owner.

I’ll definitely replace the original session ID with the one belonging to my current user, who is not supposed to have access to team 3. I’ve also changed the userId from 1 to 3 in both the route and the request body, and changed the role to owner.

When I send this request, the correct behavior would be some sort of authorization error or perhaps even a 404. On the other hand, if it returns a 200, and I can verify that user 3 (my current user) has been added to the team that I’m not supposed to have rights on, then the authorization is not correctly enforced. In this case, the auth was broken and the user made himself an owner of that team, as shown in the response below.

The HTTP response to a Get Team API request for team 3. Highlighted areas show the team ID is 3, and the user with an ID of 3 has been added as a team owner.

Other Considerations

  • Resource IDs affect the exploitability of the broken auth. In my example with Team 3 above, all the IDs were obviously sequential integers. Even if I didn’t have an account that could see the team, I could have reasonably guessed it or even used the Fuzzer to enumerate a large range of sequential integers and add my account to all the teams. On the other hand, if all the IDs were cryptographic-strength GUIDs, it’s statistically unlikely that I could generate even a single collision. Meaning that I could only truly exploit this flaw if the IDs were disclosed to me in the application (either by design or due to another flaw).
  • There are other observable things that could be wrong with a session identifier. If it’s a guessable value like a sequential ID or an MD5 hash of the user’s email address, that’s a problem. Another example is session fixation. If there’s some way of forcing a session ID on a user, such as supplying it in a request parameter, then an attacker could leverage that to give a target user a session ID that the attacker knows.
  • Because all these examples use cookies, it’s worth noting that cookie security would impact them. This means using the flags correctly:
    • Secure - to ensure the cookie is only sent over TLS
    • HTTPOnly - if possible, to keep JavaScript on the page for accessing the cookie, although I’ve argued before that the security added by this flag is frequently overstated.
    • Samesite=Strict or Samesite=Lax - these aren’t perfect, but they’re good. However, there are still a few common browsers around that support the flag but don’t default to Lax. So this should be explicitly set.

Client-Readable Bearer Tokens

There are specific attacks and common implementation flaws for different token types. My examples here will use JWTs and cover a couple of common attacks, but this sort of process is applicable to any token that:

  1. Identifies the user and/or permissions through claims within the token itself, and these claims can be read by the client. E.g. JWTs are Base64-encoded, these are trivial to decode back into their JSON structures.
  2. Relies on a signature or similar mechanism for tamper-proofing.

Also, to reiterate, every test in the previous section applies to these as well. Below is a sample request with a JWT bearer token, followed by additional checks to make against these tokens. 

HTTP request with the Authorization header highlighted, showing a Bearer token in JWT format.

What signing algorithm is being used?

While some token types don’t contain it, JWTs specifically do. And it’s useful information to have, in order to inform how you approach the other parts. If it’s your app, you may just know it. However, you may just want to decode the JWT and verify it.

Highlight the first section of the token (up to the first period character), right-click, and select Encode/Decode/Hash… from the context menu.

The Bearer token in the Authorization header, with the first section of the token highlighted, up to the period character. The context menu is open with Encode/Decode/Hash highlighted.

This will open a dialog with tabs for different encodings. Choose the Decode tab, and read the alg claim from the Base64 Decode section.

The Encode/Decode/Hash tool in ZAP, with highlights showing the Decode tab is selected, and the alg claim is HS256.

My example is HS256, meaning the signature is a message digest for the rest of the token, hashed with a secret.

If it was a signing algorithm based on asymmetric encryption, such as RS256, that would be worth noting. Either one can be done securely, but there are minor differences in the most efficient approaches to attacking them.

Is the signature being checked?

If we use ZAP’s encoding/decoding facilities, we can easily modify the token. An easy change for this one is to copy the decoded value for the whole JWT head, from where I grabbed the alg, and paste it into the Text to be encoded… box at the top of the dialog.

ZAP's encode/decode/hash utility with the previous decoded value pasted into the Text to be encoded/decoded/hashed input box on the Encode tab.

I still want it to be well-formed, but I can make a benign addition to the header’s claim, and then copy the Base64-encoded value, ignoring any trailing equals (=) signs (they’re padding characters that don’t belong in .

The JWT section previously pictured, with an additional claim added showing a key of "hello" and a value of "world". The whole section is available Base64 encoded below.

Now, if I use that value to replace the header in the Request Editor, and replay the request, I should expect to see a signature verification error.

The ZAP request editor, showing that the first section of the JWT has been replaced with the modified version containing the extra claim, within the headers of the original request. The trailing equals signs have been omitted from this value.

In my app, I got a 401 error with a message indicating an invalid token signature, as expected. This means that my app saw that the payload didn’t match the signature, therefore it had been tampered with and should be considered invalid.

If we pretend that I had received a regular 200 response, with the tampered token, my next step would be to grab the middle section of the token (between the first and second period characters), and similarly decode and modify that. This section varies greatly from one app to another, but as you can see in the following screenshot, this example has the userId and username claims.

The JWT claims in the Encode/Decode/Hash tool, showing the user ID and user name decoded from the payload.

If the server isn’t validating the signature, I can freely change these to another use with different rights. Another application may have claims for tenant ID, or user group memberships, or a number of other sensitive fields.

Are insecure signing algorithms supported?

Following the same steps as previously mentioned to construct a modified header, we can get a variant of it with a signing alg of none.

Modified version of the first, header section of the JWT, showing the alg value changed from HS256 to none. The Base 4 encoded version of this is highlighted, excluding the trailing equals sign.

Much like last time, I can now go into the Request Editor and replace the header section. The only difference is that this time I’m also removing the signature (leaving the second period).

The HTTP request again, this time with the modified JWT header with the alg set to none (base64 encoded). An arrow shows that the signature section has been removed from the end of the JWT, but the second period is still present.

Most applications will have the none alg disabled, so this will not work. However, occasionally this is enabled either by default or by mistake, and allows the attacker to modify the claims at will and effectively bypass the signature check.

Other Token-Specific Flaws

There are other flaws that JWTs can have, such as the Key Confusion attack if a public key is available. Or if HS256 is used and the secret is weak, guessable or in wordlists, an attacker able to determine it can sign their own modified tokens. Other token types such as SAML 2.0, can have their own implementation flaws. To reiterate, it’s really a topic unto itself, but this is at least an intro to some of the attacks to try.

Check out Day 4 for a rundown of fuzzing to find injection flaws.

Join the professionally evil newsletter