Using Burp Python Scripts to encrypt requests with RSA keys
In this article, I aim to present another mechanism for securing API requests, the implementation of which was a significant element of a real-world project. The discussed mechanism stands out with an additional layer of security through encrypting request content using RSA keys. This serves as an extra protective measure that can effectively complement the standard HTTPS protocol. Furthermore, digital signatures using RSA keys will be introduced as the icing on the cake in the later part of the article, ensuring the integrity verification of the messages we send. All communication will also be further secured against potential replay attacks.
Problem Description
In the previous article, I discussed using Burp Python Scripts to sign requests using RSA keys. Today, I would like to revisit a project in which I had the pleasure to participate, which utilized an API for communication with a mobile application. During the transmission of sensitive data, such as personal information or credit card details, the API author decided to implement additional security mechanisms aimed at protecting against data modification and complicating request manipulation and further exploitation.
This topic seemed fascinating to me, especially considering the lack of similar implementations in the context of Burp Suite resources. Therefore, I decided to share this experience.
In summary, the project author, in addition to the request signing mechanism (X-Signature header) and protection against replay attacks (X-Nonce-Value and X-Nonce-Created-At headers), which I described in previous articles, also opted for encrypting request content using RSA keys. All these security measures significantly hinder attackers from inspecting communication, especially in the absence of internal knowledge about the implementation.
Bypassing pinning in mobile applications is often a challenge, and in cases where the application author uses custom solutions, access to the API becomes even more complex, sometimes requiring decryption of message content, as in the case discussed here.
To address the additional encryption of request bodies and conduct effective API security testing, I initially considered using a provided library. However, this solution had its limitations, such as the need for frequent changes in the library code and restrictions on the quantity and type of tests I could perform.
Ultimately, I decided to utilize a Burp plugin called Python Scripter. I will describe this process in detail in the subsequent part of the article.
Description of the Applied Security Mechanism
In comparison to the mechanism described in the earlier article “Using Burp Python Scripts to Sign Requests with RSA Keys”, our current algorithm introduces additional stages related to encryption and decryption of request content. In the new version, the entire process looks as follows:
- The application encrypts the request content using the public key received from the recipient (our server).
- The application constructs a standardized request schema containing the encrypted message.
- The application adds a nonce value to the request, using uuid4.
- The application adds the current date to the request.
- The mobile application generates a digital signature for the request using its private key, the message content, the nonce value, and the current date.
- The application adds the digital signature to the request.
- The API server verifies the digital signature using the client public key.
- If the digital signature is valid, the API server accepts the request. If the signature is invalid, the request is rejected.
- The API server decrypts the message using its private key.
The algorithm discussed here omits details regarding the exchange of public keys between communication parties. Since this is beyond the scope of this article, the assumption is that those implementing this mechanism have already considered all available methods of key exchange. For pentesters, verifying this in practice remains the only task :)
In the next part of the article, we will focus on simplifying the entire process, using only one pair of RSA keys in the context of a proof of concept (PoC).
Implementation
In the earlier article, I presented the expansion of a simple script that updates the value of the ‘X-Nonce-Value’ header in each sent request to a stage where every sent request was signed using a private RSA key and secured against replay attacks. I also introduced how to invoke system commands using the subprocess library, enabling the execution of any command in the system from Python. The functionality of invoking additional system commands is crucial, considering the limitations in terms of available libraries and Python 2.7 version in the Burp Python Scripts environment.
Next, I focused on the process of building the signature input value, crucial for signature verification. I demonstrated how to formulate this value correctly, taking into account white spaces in the request content. Additionally, I discussed the issue of JSON deserialization in the context of differences between Python 2.7 and Python 3.
For encrypting the message body, we will also utilize the capability to invoke any system command. However, before doing so, we need to properly prepare our message for encryption. In this case, I have good news - we no longer need to worry about differences in JSON serialization between Python 2.7 and Python 3. We can deserialize the message to remove unnecessary additional white spaces and then serialize and encode it in base64. The code will look as follows:
msg = helpers.bytesToString(requestBody)
msg = json.loads(msg)
msg_b64 = base64.standard_b64encode(json.dumps(msg, ensure_ascii=False).encode()).decode()
Next, we invoke the openssl command to encrypt the message, removing newline characters ("\n") from the received encrypted message:
cmd = 'printf %s "{}" | openssl pkeyutl -encrypt -pubin -inkey {} | openssl base64'.format(msg_b64, PUBLIC_KEY)
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
output, err = process.communicate()
if err.decode() != "":
raise Exception(err)
encrypted_body = output.decode().replace("\n", "")
The final step is to fill in the message template, which will be sent to the server:
newPayload = '{\"keyId\": \"init\", \"encryptedPayload\": \"PAYLOAD\"}'.replace("PAYLOAD", encrypted_body)
Why do we build the message in this way instead of using JSON dump? The differences in JSON serialization mentioned earlier, we want to ensure that the order of keys in the message never changes.
Below is the entire code written in Python, which implements the described mechanism of encryption and signing of requests:
# Request body encryption script with RSA for Burp Suite
# Python-Scripter
# Michal Walkowski - https://mwalkowski.com/
# https://github.com/mwalkowski
#
# Tested Burp Professional v2023.11.1.4 and Python Scripter 2.1
# For RSA Encrypting Process
# Based On: https://mwalkowski.com/post/using-burp-python-scripts-to-encrypt-requests-with-rsa-keys/
import base64
import subprocess
import json
PUBLIC_KEY = "public.pem"
if messageIsRequest:
requestInfo = helpers.analyzeRequest(messageInfo.getRequest())
headers = requestInfo.getHeaders()
requestBody = messageInfo.getRequest()[requestInfo.getBodyOffset():]
msg = helpers.bytesToString(requestBody)
msg = json.loads(msg)
msg_b64 = base64.standard_b64encode(json.dumps(msg, ensure_ascii=False).encode()).decode()
cmd = 'printf %s "{}" | openssl pkeyutl -encrypt -pubin -inkey {} | openssl base64'.format(msg_b64, PUBLIC_KEY)
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
output, err = process.communicate()
if err.decode() != "":
raise Exception(err)
encrypted_body = output.decode().replace("\n", "")
newPayload = '{\"keyId\": \"init\", \"encryptedPayload\": \"PAYLOAD\"}'.replace("PAYLOAD", encrypted_body)
request = helpers.buildHttpMessage(headers, newPayload)
messageInfo.setRequest(request)
Example of Usage
Below, I present the functioning of the described scripts, I also attach a video
In Figure 1, the sending of the request in plaintext - unencrypted is depicted, which, according to the implementation, is processed by the server. As seen in the server’s response, the request processing failed. The server expects a message with the encryptedPayload
field.
In Figure 2, the same request is depicted, this time utilizing the written script. As evident from the server’s response received, the request was processed correctly.
In Figure 3, the actual request sent to the server is depicted, which was logged in the Burp Logger. We can observe that the message body has been completely transformed into the format expected by our server.
As I mentioned earlier, as a finishing touch to the sent message, a digital signature using RSA keys will be added. We will use the script that was written in the previous article for this purpose. To ensure everything works properly in Burp Python Scripts, the order of the added scripts must be maintained, as shown in Figure 4. This order is crucial because the scripts will be executed in that order when sending our request. Encryption will be performed first, followed by adding the signature.
In Figure 5, the sent request is shown from the perspective of Burp Logger, where the previously mentioned headers responsible for our digital signature are missing, namely ‘X-Nonce-Value’ and ‘X-Nonce-Created-At’, as well as ‘X-Signature’.
The sent request, which is correctly interpreted by the server, is presented in Figure 6.
In Figure 7, the sent request logged in Burp Logger is shown. As we can see, the message sent to the server has been encrypted. Additionally, the headers ‘X-Nonce-Value’ and ‘X-Nonce-Created-At’ have been added, and the message has been digitally signed.
Summary
The article presented a mechanism for encrypting API request content using RSA keys. This can serve as an additional security layer alongside the standard HTTPS protocol. The mechanism was implemented in a real project and featured the following elements:
- RSA Encryption: The request content is encrypted using the recipient’s public key, making it unreadable to third parties.
- Digital Signatures: A digital signature is added to the request, allowing for verification of message integrity and origin.
- Protection against replay attacks: Headers ‘X-Nonce-Value’ and ‘X-Nonce-Created-At’ protect against the re-sending of the same request.
The code snippet provided significantly facilitates further API testing, as there is no longer a need to focus on message encryption or ensuring the correctness of all headers.
This article also aims to introduce alternative mechanisms for securing the integration of sent requests beyond HMAC and a possible way to test them. If anyone is interested in testing the script’s functionality, they can find a simple server-side code implementing signature verification and message encryption at this link.
References
- https://en.wikipedia.org/wiki/Replay_attack
- https://www.zimuel.it/blog/sign-and-verify-a-file-using-openssl
- https://sereysethy.github.io/encryption/2017/10/23/encryption-decryption.html
- https://httpwg.org/http-extensions/draft-ietf-httpbis-message-signatures.html#name-rsassa-pkcs1-v1_5-using-sha
- https://github.com/PortSwigger/python-scripter
- https://github.com/lanmaster53/pyscripter-er/tree/master/snippets
- https://github.com/mwalkowski/api-request-security-poc/
- https://stackoverflow.com/questions/51769239/why-json-dumps-in-python-3-return-a-different-value-of-python-2
- https://mwalkowski.com/post/using-burp-python-scripts-to-sign-requests-with-rsa-keys/