Zed Attack Proxy (ZAP) Scripting and Request Signing with RSA Keys
One of the OWASP organization members asked me if I would like to present a method for testing an API secured with RSA keys using ZAP. At first, I wasn’t sure if I could handle it because in my daily work I use to Burp, but as is often the case in our penetration testing profession, you have to keep evolving and exploring new possibilities. That’s why I agreed, and in this article, I describe how you can test an API secured with a request-signing mechanism using RSA keys, this time utilizing Zed Attack Proxy (ZAP) Scripting.
Introduction
As I mentioned in the previous article, the project I had the pleasure to test, utilized an API for communication, among other things, with a mobile application. The mobile app sends sensitive data, such as personal information or credit card details, to the backend. For obvious reasons, the app developer, aiming to additionally protect the data from modification in transit, implemented a mechanism complicating attackers’ attempts to manipulate requests and further exploit the system. At the same time, this mechanism makes security testing more challenging. In the examined software, the developer applied two additional headers, X-Nonce-Value
and X-Nonce-Created-At
, designed to defend against replay attacks. Additionally, there is a third header, X-Signature
, securing the integrity of the transmitted message through an RSA signature.
For the actual testing in the discussed project, I used an implementation prepared for Burp Suite, and this implementation was discussed here. However, encouraged by one of the OWASP organization members, namely Simon Bennetts, the leader of the ZAP project, I ventured to create a similar implementation for the Zed Attack Proxy (ZAP). To my surprise, the way to implement the plugin was just as straightforward as with Burp Python Scripts. The plugin described in this post is also available in the community-scripts repository, which includes a collection of various other add-ons created by the ZAP community, all of which can be useful in our daily work.
Description of the Mechanism
As a reminder, the mechanism implemented by the authors of the examined application works as follows:
- 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 the private key, method, path, message body, nonce value, and current date.
- The application adds the digital signature to the request.
- The API server verifies the digital signature using the public key.
- If the digital signature is valid, the API server accepts the request. If the digital signature is invalid, the API server rejects the request
Implementation
Firstly, to leverage the Python language for implementing additional scripts in ZAP, we need to install an extension called Python Scripting. Similar to Burp, this extension is a Python implementation in the Java language, commonly known as Jython. Just like in the case of Burp, it is version 2.7, which is no longer supported. In both implementations of the plugin, I encountered the same issue, namely the inability to install additional Python libraries. Since I already had a working solution for Burp, I decided to utilize it.
When we install the Python Scripting extension in ZAP, the next step is to create a script of our chosen type (Fig 1). The choice of the script type we decide to use is crucial. Each available type in the Scripting window behaves differently, meaning a script of a specific type is executed under different conditions. You can read more information about script types here. For our considered case, we want the signature to be added to every request, including those generated by the ZAP Attack module or passing through the ZAP Proxy. Therefore, we choose the HTTP Sender module.
After creating a script of our chosen type (in Fig 1: RsaSigningForZap.py), we get a piece of code presented in the listing below. In the further implementation, we will only use the first function, the one responsible for processing sent requests (sendingRequests). The second function, responseReceived, is not needed, and to avoid adding unnecessary logs during script execution, we will remove the print function from it.
def sendingRequest(msg, initiator, helper):
# Debugging can be done using print like this
print('sendingRequest called for url=' +
msg.getRequestHeader().getURI().toString())
def responseReceived(msg, initiator, helper):
# Debugging can be done using print like this
print('responseReceived called for url=' +
msg.getRequestHeader().getURI().toString())
Let’s start with something simple. The code snippet below updates the value of the ‘X-Nonce-Value’ header with each sent request. Not without reason, I begin with a random value for the nonce header; this script can be useful in any penetration tests where we want to track the execution of our requests. For example, when running ZAP Attack and wanting to find in the logs a request triggering a vulnerability or trace the execution of our request in the application logs. A unique header value significantly facilitates this task.
import uuid
NONCE_HEADER = 'X-Nonce-Value'
def sendingRequest(msg, initiator, helper):
nonce_value = str(uuid.uuid4())
print('Adding new {}: {}'.format(SIGNATURE_HEADER, signature))
msg.getRequestHeader().setHeader(SIGNATURE_HEADER, signature)
In the next step, we will add a timestamp to the script.
import uuid
import datetime
NONCE_HEADER = 'X-Nonce-Value'
NONCE_CREATED_AT_HEADER = 'X-Nonce-Created-At'
def sendingRequest(msg, initiator, helper):
nonce_value = str(uuid.uuid4())
print('Adding new {}: {}'.format(SIGNATURE_HEADER, signature))
msg.getRequestHeader().setHeader(SIGNATURE_HEADER, signature)
nonce_created_at = '{}+00:00'.format(datetime.datetime.utcnow().isoformat())
print('Adding new {}: {}'.format(NONCE_CREATED_AT_HEADER, nonce_created_at))
msg.getRequestHeader().setHeader(NONCE_CREATED_AT_HEADER, nonce_created_at)
When we have already implemented two basic headers, it’s worth taking a look at the signature itself. Currently, in ZAP Python Scripting, there is no possibility to extend it with additional libraries. Another limitation is that we only have Jython version 2.7 available, which is no longer supported. To overcome these limitations, we can use subprocess library for system calls. For example:
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
output, err = process.communicate()
if err.decode() != "":
raise Exception(err)
It allows us to execute any command in the system from Python. At this stage, we already have almost all the essential elements, and we only need information on how to build the signature input. To pass the entire value of the signature input to the system without worrying about additional spaces in the message body, it needs to be encoded using base64. The code building the signature input for the described case will look like this:
msg = helpers.bytesToString(requestBody)
signature_input = "{}{}{}{}{}".format(method, path, nonce_value, nonce_created_at, msg)
signature_input_b64 = base64.standard_b64encode(signature_input.encode()).decode()
Below is the function that implements the described mechanism of signing requests:
def sign(signature_input):
print('signature_input', signature_input)
signature_input_b64 = base64.standard_b64encode(signature_input.encode()).decode()
print('signature_input_b64', signature_input_b64)
cmd = """printf %s "{}" | openssl dgst -sha256 -sign {}| openssl base64""".format(signature_input_b64, PRIVATE_KEY)
print(cmd)
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
output, err = process.communicate()
if err.decode() != "":
raise Exception(err)
return output.decode().replace("\n", "")
At the end, I present a solution that implements the described mechanism of signing requests:
# RSA Signing Script for Zed Attack Proxy - ZAP
# HelpAddOn Script - HTTPSender
# Michal Walkowski - https://mwalkowski.com/
# https://github.com/mwalkowski
#
# Tested with Jython 14 beta and ZAP 2.14.0
# For RSA Signing Process: https://httpwg.org/http-extensions/draft-ietf-httpbis-message-signatures.html#name-rsassa-pkcs1-v1_5-using-sha
# Based On: https://mwalkowski.com/post/using-burp-python-scripts-to-sign-requests-with-rsa-keys/
import urlparse
import uuid
import datetime
import base64
import subprocess
# path to private.key
PRIVATE_KEY = "private.key"
SIGNATURE_HEADER = 'X-Signature'
NONCE_HEADER = 'X-Nonce-Value'
NONCE_CREATED_AT_HEADER = 'X-Nonce-Created-At'
def sign(signature_input):
print('signature_input', signature_input)
signature_input_b64 = base64.standard_b64encode(signature_input.encode()).decode()
print('signature_input_b64', signature_input_b64)
cmd = """printf %s "{}" | openssl dgst -sha256 -sign {}| openssl base64""".format(signature_input_b64, PRIVATE_KEY)
print(cmd)
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
output, err = process.communicate()
if err.decode() != "":
raise Exception(err)
return output.decode().replace("\n", "")
def sendingRequest(msg, initiator, helper):
method = msg.getRequestHeader().getMethod()
path = urlparse.urlparse(msg.getRequestHeader().getURI().toString()).path
body = msg.getRequestBody().toString()
print(msg.getRequestBody().toString())
nonce_value = str(uuid.uuid4())
nonce_created_at = '{}+00:00'.format(datetime.datetime.utcnow().isoformat())
signature = sign("{}{}{}{}{}".format(method, path, nonce_value, nonce_created_at, body))
print('Adding new {}: {}'.format(SIGNATURE_HEADER, signature))
msg.getRequestHeader().setHeader(SIGNATURE_HEADER, signature)
print('Adding new {}: {}'.format(NONCE_HEADER, nonce_value))
msg.getRequestHeader().setHeader(NONCE_HEADER, nonce_value)
print('Adding new {}: {}'.format(NONCE_CREATED_AT_HEADER, nonce_created_at))
msg.getRequestHeader().setHeader(NONCE_CREATED_AT_HEADER, nonce_created_at)
def responseReceived(msg, initiator, helper):
pass
In Figure 2, an attempt to send a request that did not include additional headers is presented. As you can see, the server indicated their absence and did not process the message.
In Figure 3, another attempt to send a request is shown; this time, the required headers were added. However, a change in the message body resulted in a mismatch with the signature, causing the server to reject the message as well.
In Figure 4, the use of the written code is presented. The headers ’nonce’ and ‘signature’ are correct, as evidenced by the proper response received from the server.
Summary
The nonce and request signing mechanism can be an effective way to protect API requests from modification. Without additional information provided by the application’s author, it would be challenging to recognize how the implemented digital signature is constructed. The piece of code I presented significantly facilitates further API testing, as there is no longer a need to focus on ensuring the correctness of all header values.
This article also aims to introduce other mechanisms securing the integration of sent requests than HMAC and a possible way to test them. If someone is interested in testing the plugin, under this link, they can find a simple server code implementing the verification of the applied signature.
Additionally, you can find the plugin discussed in this post in the community-scripts repository for the ZAP project.
References
- https://en.wikipedia.org/wiki/Replay_attack
- https://www.zaproxy.org/docs/desktop/addons/python-scripting/
- https://github.com/zaproxy/community-scripts
- 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.1.html#name-rsassa-pkcs1-v1_5-using-sha
- https://github.com/mwalkowski/api-request-security-poc/
- https://mwalkowski.com/post/using-burp-python-scripts-to-sign-requests-with-rsa-keys