Working around unusual end-to-end cryptography with Burp and Burpscript
Earlier this year, Danny Rosseau shared his work on Burpscript, a multi-language scripting extension for Burp Suite. Since then, I’ve found it to be a great tool for quickly augmenting the capabilities of Burp, particularly when encountering applications that are outside the norm. A good example of this is when we encounter web applications that integrate some form of customized cryptographic controls into their API. Some of these applications, for instance, apply an additional layer of end-to-end cryptography, on top of HTTPS. Their intention for doing this is often to provide additional guarantees about the integrity and authenticity of the messages being exchanged, and as you might expect, applications like this are difficult to dynamically test using tools like Burp Suite, because any modifications made to the request or response would break the cryptographic integrity in some way.
In this post, I am going to show how to use Burpscript to work around this problem by creating a script that will automatically re-calculate the cryptographic portions of the HTTP messages as they pass through Burp. This allows us to use all the features of Burp Suite, like Repeater, and Intruder, as we normally would, while satisfying the cryptographic requirements of the application.
Target application
Suppose we have a web application that implements a message integrity and authenticity check using public key cryptography. The application requires that each request be signed by the client, and that the server verifies the signature before processing the request.
Our imaginary application will have the following behavior:
- The client attaches two headers to its request:
Signature
: The signature of the request being made, signed using RSASSA-PKCS1-v1_51Content-Digest
: The SHA-256 hash of the request body.
- The following pieces of the request are serialized and then signed using the client’s private key:
- The request method (i.e.
GET
, orPOST
) - The request path (i.e.
/api/user/...
) - The
Host
header - The
Content-Digest
header - The
Content-Length
header - The
Content-Type
header
- The request method (i.e.
- The server verifies the
Signature
using the client’s public key, and checks that the request body hash matches theContent-Digest
header.
This is loosely based on RFC 9421 - HTTP Message Signatures, which is a proposed standard for creating and verifying digital signatures within an HTTP message. If you’re interested in learning more about these types of controls, and the threat model they protect against, I recommend taking a look at the RFC.
In the spirit of RFC 9421, we will use a similar, but simpler serialization scheme to create our signatures. Below is example frontend code that generates the signature and headers for a request in our example application:
const PRIVATE_KEY = ...;
const method = 'POST';
const path = '/api/user/123';
const host = 'example.ivision.com';
const contentDigestBase64 = ...; // SHA-256 digest of the request body
const contentLength = ...;
const signedParams = [
`@method:${method}`,
`@authority:${host}`,
`@path:${path}`,
`content-digest:${contentDigestBase64}`,
`content-length:${contentLength}`,
`content-type:application/json`,
];
const serializedParams = signedParams.join('\n');
const signature = await crypto.subtle.sign(
{ name: 'RSASSA-PKCS1-v1_5' },
PRIVATE_KEY,
new TextEncoder().encode(serializedParams)
);
const signatureBase64 = ...;
const headers = new Headers({
'Content-Type': 'application/json',
'Content-Length': contentLength,
'Content-Digest': contentDigestBase64,
'Signature': signatureBase64
});
Using Burpscript to attack
Uh-oh. Our example application is vulnerable to an Insecure Direct Object Reference (IDOR) attack, and we are able to enumerate user IDs by sending requests to /api/user/{id}
. Burp Intruder is normally a useful tool for automating this attack, but in this case, it won’t work because the requests it generates would lack valid signatures.
To deal with this, we can use Burpscript to automatically calculate the signature and digest values as the requests are generated by Intruder.
There are, however, some unfortunate limitations to the modules we can import into our scripts. This has to do with limitations imposed by the GraalVM polyglot API, which is the scripting runtime used by Burpscript. Because of those limitations, we are going to avoid using third-party Python modules, such as cryptography
. Thankfully, Burpscript has built-in cryptographic utilities that we can use here to sign our requests.
Mirroring the frontend code above, below is our Python script that will re-sign the requests before they are sent to the server.
from base64 import b64encode
from hashlib import sha256
# Private key PEM, extracted from application client
PRIVATE_KEY = """
-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----
"""
def on_request(req: "ScriptHttpRequest") -> "ScriptHttpRequest":
# Use Burpscript's crypto helpers
pkcs1: "SignatureApi" = helpers.getCryptoHelper().newSignature("SHA256withRSA")
pkcs1.setKey(CLIENT_PRIV_KEY)
params = [
f"@method:{req.method()}",
f"@authority:{req.headerValue('Host')}",
f"@path:{req.pathWithoutQuery()}",
]
content = bytes(req.bodyToBytes())
if content:
content_digest = sha256(content).digest()
content_digest_b64 = b64encode(content_digest).decode()
params.extend(
[
f"content-digest:{content_digest_b64}",
f"content-length:{req.headerValue('Content-Length')}",
f"content-type:{req.headerValue('Content-Type')}",
]
)
# Update the `Content-Digest` header
req = req.withHeader("Content-Digest", content_digest_b64)
folded_params = "\n".join(params)
sig = bytes(pkcs1.sign(folded_params.encode()))
sig_b64 = b64encode(sig).decode()
# Update the `Signature` header
req = req.withHeader("Signature", sig_b64)
print(f"re-signed {req.path()}: {sig_b64[:10]}...")
return req
By default, Burpscript will only execute scripts on requests and responses that come from the Proxy. That is to say, by default, it will not execute scripts for requests and responses that come from other tools, such as the Repeater, or Intruder. You can change this behavior by going to the Scripting
tab and unchecking the Proxy Only
option next to the script.
Now, we can use Intruder to automate our IDOR attack to enumerate users and our script will automatically re-sign the requests that are generated.
And we can also see traces of our script’s output in the Output
window of the Extensions
tab.
Summing up
For assessing web applications with unique or complex behaviors, you can always reach for something like mitmproxy
for its first-class scripting capability, but if you’re like me and prefer using Burp Suite, Burpscript gives you a lot of the same flexibility without having to completely change your workflow. Burpscript’s built-in support for functionality that’s not normally provided by the scripting language (like cryptography) is a big plus, and it makes it easier to write scripts that are more self-contained and easier to share with others.
-
RSASSA-PKCS1-v1_5, also referred to as PKCS#1 v1.5, is a signature scheme that uses RSA public-key encryption along with the SHA-256 hash function. It is widely used in practice, but has been deprecated by the NIST. It is recommended to use an alternate scheme, such as RSASSA-PSS, for new applications. ↩