top of page

The Only JWT Security Guide You Will Ever Need


Introduction To Breaking JWT


JWTs are primarily used for authentication and authorization almost everywhere on the modern web, however, they are not something that gets well tested frequently by security researchers, pentesters during security engagements. JWTs can possess security vulnerabilities if configured and implemented improperly, potentially causing havoc.


Thus, understanding how JWTs work and how they might fail is critical when securing systems that leverage JWTs. In this blog, we will cover potentially all scenarios where JWTs could fail and how we can exploit them with practical hands-on exercises.


What Are JWTs?



JSON Web Token (JWT) is a compact, URL-safe means of representing claims (the information packed in JSON object) to be transferred between two parties. The claims in a JWT are encoded as a JSON object that is used as the payload of a JSON Web Signature (JWS) structure or as the plaintext of a JSON Web Encryption (JWE) structure, enabling the claims to be digitally signed or integrity protected with a Message Authentication Code (MAC) and/or encrypted.



JSON Web Token (JWT) is a compact, URL-safe means of representing claims to be transferred between two parties.

Role Of JWTs & Why Are JWTs Important?


Imagine logging into a web application, let's say an e-commerce platform. Once you enter your credentials the website verifies it against the database if a login attempt is made via a legitimate user. Instead of making the user log in every time the user visits the page, the website gives the user a JWT. The JWT is signed by the website, so it can’t be faked or tempered with, that is by its design.


JWTs provide speed as the server doesn’t have to look up the credentials repeatedly. Secure implementation of JWT provides robust security, as no one can change the information when JWTs are signed. When a user authenticates, the server signs a JWT with a secret or private key and hands it to the client. From that moment on, every request simply presents the token; the server can verify the signature, trust the claims inside (like user ID, role, or expiry time), and skip repeated database look‑ups. Because the token is self‑contained and cryptographically protected, it enables truly stateless authentication while giving you fine‑grained control over validity periods and revocation strategies.


Below is how a typical JWT token looks like, each part is base64url encoded and the token `.` is used to separate each part of the structure.



JWT debugger screen showing encoded and decoded sections.
Image Courtesy: https://jwt.io/

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ. SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

The JWT Format


A JSON Web Token consists of three parts:


  • A Header

  • A Payload

  • A Signature


JWT Header


The header contains meta-data about the token, such as the token type and the signing algorithm used. For example, the HS256 algorithm. Here's how the decoded header looks like:


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

JWT Payload


The payload part of the JWT contains claims or what we call information related to the user and some additional data. Payload can include user ID, role, permissions, and expiration time. Here's how the decoded payload looks like:


{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

JWT Signature


The signature segment is interesting, it is made up of the base64 URL encoded header and payload segment, a secret which is the key in the signing algorithm, all hashed using the algorithm defined in the header segment:


HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),  
  your-256-bit-secret
)

The signature's role is to ensure that the data in the header and payload have not been changed (tampered with) and that JWT is legit.


The JWT header might also include some additional parameters that are crucial to understand, like JKU, JWK, KID.


JSON Web Key (JWK)

JWK provides a set of keys which contains the public key/keys used to verify any JSON web token(JWT) which is issued by the authorization server and signed using the RS256 algorithm.


JSON Web Key Set (JWKS)


A JSON Web Key Set (JWKS) is a JSON object that represents a set of JWKs. The JWKS is often exposed by an authorization server at a specific URL. This allows clients to retrieve the public keys needed to verify the signature of a JWT.


JSON Web Key Set URLs (JKU)


The JKU parameter is a web address that points to a collection of JSON-encoded public keys; one of these keys is used to create a digital signature for the JWS (JSON Web signature).


Key ID (KID)


The KID parameter is an identifier for the key used to create the digital signature.


How Does JWT Work?


Now that we understand what a JSON Web Token is, let’s explore how it actually moves through an application. We can picture a JWT as a signed, tamper‑evident hall‑pass that the client carries between requests. The server issues the pass once, and from then on can instantly validate who’s knocking without re‑querying the user database. Here’s the typical flow—simple in concept, powerful in practice:


JWT Workflow diagram
JWT Workflow

  1. User authentication – The user logs in with their credentials.

  2. Token creation – The server verifies those credentials and, on success, generates a JWT.

  3. Token delivery – The freshly minted JWT is sent back to the client.

  4. Client‑side storage – The client stores the token (e.g., local storage, session storage, or an HTTP‑only cookie).

  5. Authenticated requests – For every subsequent request, the client adds an Authorization: Bearer <JWT> header.

  6. Signature check – The server receives the request and confirms the token’s signature hasn’t been altered. The server verifies a JWT by splitting it, decoding the header and payload, recalculating the signature, & comparing it to the received signature. (see below)

  7. Token decoding – It decodes the JWT, splitting it into header and payload.

  8. Algorithm verification – The server reads the header to confirm which signing algorithm was used.

  9. Re‑sign & compare – It re‑calculates the signature with its secret/private key and compares the result to the token’s signature.

  10. Claim validation – Finally, it inspects key claims (e.g., exp for expiration, is for issuer) to determine whether the token is still trustworthy.


When every check passes, the request is processed as an authenticated action—no session tables, no extra round‑trips, just a lightweight and stateless security handshake.


Breaking JWT Security


Now that we have understood what JWT actually is and how it works, let’s focus on how to break it from a security standpoint, and truly test JWT implementations during penetration testing engagements.


Lab Setup


We will require the following to get started with our testing engagement:


  • Functional Kali Linux instance with Burp Suite (community or professional) 

  • Access to PortSwigger Web Academy from within the Kali machine. Make sure your network configuration is set up correctly. 


Setting Up Kali Linux


Please follow the below visual guide on how to set up Kali Linux:


By default, the kali linux instance comes pre installed with Burp Suite Community edition.


Setting Up JWT Editor Extension


JWT Editor is a Burp Suite extension which aims to be a Swiss Army Knife for manipulating JSON Web Tokens (JWTs) within Burp Suite. It provides detection of JWTs within both HTTP and WebSocket messages and allows for their editing, signing, verifying, encryption and decryption. Additionally it facilitates several well-known attacks against JWT implementations.


Please follow the below visual guide on how to set up JWT Editor extension in Burp Suite:



Setting Up PortSwigger Web Academy


Kindly create an account on the PortSwigger Web Academy as we will be using the same for demonstrating all the different attack strategies related to JWT security testing. This step is optional, and users can refer to any other source that they want for the same.


Exploiting Flawed JWT Signature Verification


What Is Flawed JWT Signature Attack?


The JWT authentication server does not store any information about the JWTs it issues to the client. Instead, as mentioned above, each token is a self-contained entity. This design can lead to issues as the server doesn't have a record of the original contents or structure of the token, to compare with; which is by design.


It must rely on the token's signature to verify its authenticity. Therefore, if the server fails to verify the signature properly, there's nothing to stop an attacker from making arbitrary changes to the rest of the token body. As a result, improperly validating JWT can allow attackers to gain unauthorized access and privilege escalation.


What Can The Attacker Do After Modifying The Token?


Once the attacker gets to alter the JWT and the server accepts it due to flawed signature verification, the attacker may:


  • Gain unauthorized access to restricted / protected endpoints.

  • Perform Escalation of Privileges (EOP).

  • Bypass authentication and authorization checks.

  • Forge identities, impersonate users.


For example, consider the following:


{
	"username": "raphael",
	"isAdmin": false
}

If the server identifies the session based on this `username`, modifying its value might enable an attacker to impersonate other logged-in users (IDOR style situation).


Similarly, if the isAdmin value is used for access control, this could provide a simple vector for privilege escalation.


Testing Flawed JWT Signature Attack


We will be using the JWT authentication bypass via unverified signature from PortSwigger Web Academy to demonstrate and test for this attack.


JWT libraries (by default) provide functions, one to verify the token (verify()) and another to decode the token (decode()) in Node.js. Developers confuse between the two methods and pass the incoming token to the decode() method only, which eventually means that the application doesn't verify the signature at all, such confusions or mistakes can lead to privilege escalation.


The critical difference between verify() and decode() method


Decode() Method


// DANGEROUS - No signature verification 
const jwt = require('<jsonwebtoken>'); 
const payload = jwt.decode(token);

The purpose of the decode() method is to extract and decode the payload without verification, it does not validate the signature and this method should only be used when the signature is handled separately.


Verify() Method


The verify() method validates the signature and extracts the payload out of it. It ensures that the integrity and authenticity of the token is maintained. This method should be used for decisions involving authentication and authorization.


To understand the vulnerable implementation of these methods, let’s solve a vulnerable lab by portswigger web academy.


Intercepting the /login endpoint using burpsuite, we have a JWT in the request.


Intercepting /login endpoint request in lab

It’s always a great idea to decode the JWT to enumerate its contents. Therefore, jwt.io provides a handy debugger that can be used to decode, verify, and generate JSON Web Tokens.


Copy and paste the JWT from the request to the jwt debugger.


Sample JWT request to the jwt debugger

Since we know we’re testing for improper implementation of verify and decode methods, we should tinker with the claims of the JWT.


improper implementation of JWT

Modify the sub-parameter to administrator, and change the signing algorithm to verify the signature and insert some random secret. A new JWT is generated. Send this token with the request and see if we can escalate privileges.


Burp screen showing the Cookie and response

The response says 302 found. Maybe that’s a hit. Let's forward the request!


Response seen after forwarding the request in Burp

We were able to escalate our role to an administrator. Therefore, we conclude that the JWT handling is not implemented properly in this application i.e. the application does not verify the signature within the JSON Web Token.


Allowing the NONE algorithm


A JSON web token is a self contained token. The JWT header contains a key parameter called alg, which stands for algorithm. This tells the server which algorithm was used to sign the token, and in turn, which algorithm it should use to verify that signature.


There are several other parameters that are used to sign the algorithm, like RSA, HMAC, NONE, etc.


Some common signing algorithms include RSA, HMAC and others. Out of which is an unusual option is the none algorithm that indicates that the token is unsigned. If the server validates tokens with alg set to none, it indicates that the server is vulnerable to signature bypass/None algorithm attack, which allows attackers to forge valid tokens.


An example lab that simulates a similar scenario. Please use this lab link


JWT lab request and response loaded in burp

The "/my-account?id=wiener" endpoint accepts a JWT and we know that the lab is vulnerable to NONE algorithm attack. This time let’s use burp’s JWT editor extension.


Checking the JWT Editor extenstion in BApp Store
Checking JWT Editor Installation in BApp Store

Burp Suite allows users to add extensions making it more powerful and customizable based on specific needs. These extensions can be installed from the BApp store in Burp Suite.


Go to the extensions tab > Click on the BApp Store > search for JWT and click on the JWT editor extension and install the extension.


Once installed the burp extension, a JSON web token tab will appear within the repeater tool. Navigate to the tab and it will automatically decode the token and split it into header, payload and signature sections.


Using the JSON Web token extension in burp

Now let’s test if the application is vulnerable to none algorithm attack by setting the "alg parameter" to none and change the value of sub parameter to administrator and remove the signature section.


altering the algorithm parameter in JWS section

This is how the final changes to the token should look like. The encoded token will automatically be embedded to request.


Navigate to the Raw tab to review the token. We can remove the signature section from the token.

Make sure to leave the period at the end of the token that suggests the token’s structure.

Raw loaded request in burp post edit.

The final request body must look like this. After this we are ready to send the request.


Succesful exploitation via token algorithm manipulation leading to admin panel being disclosed

The test was successful and as a result we were able to escalate our privileges.



Forcing Secret Keys


JWTs use secret keys to sign the algorithm when making use of algorithms like HMAC. If the attacker is able to bruteforce the keys, they can forge valid JSON web tokens and as a result, allowing attackers to gain unauthorized access to protected resources.


Let’s explore how the JWT secret brute-force works and how weak secrets can be exploited to forge valid tokens using a portswigger academy’s lab. Please use the following Lab link for this challenge.


Access the lab and login using provided user credentials. While logging in, capture the request using burp to inspect the token.


Captured logging in request in burp repeater.

Copy the token and let’s use jwt.io to gather required information that is the alg the token is signed with.


Using JWT.io to analyse the token

The algorithm is signed with the HS256 (HMAC with sha256) algorithm. To brute force the secret key we can use a python script. You can find the script used below.


import hmac
import hashlib
import base64
import sys

def decode_base64url(data):
    padding = '=' * (4 - len(data) % 4)
    return base64.urlsafe_b64decode(data + padding)

def load_keys_from_file(file_path):
    try:
        with open(file_path, 'r') as file:
            return [line.strip() for line in file]
    except FileNotFoundError:
        print(f"Error: The file {file_path} was not found.")
        sys.exit(1)
    except Exception as e:
        print(f"Error: An error occurred while reading the file {file_path}. {e}")
        sys.exit(1)

def brute_force_jwt(jwt, candidate_keys):
    header, payload, signature = jwt.split('.')

    decoded_header = decode_base64url(header)
    decoded_payload = decode_base64url(payload)

    for candidate in candidate_keys:
        message = f'{header}.{payload}'
        new_signature = hmac.new(candidate.encode(), message.encode(), hashlib.sha256).digest()
        new_signature_encoded = base64.urlsafe_b64encode(new_signature).rstrip(b'=').decode('utf-8')

        if new_signature_encoded == signature:
            return candidate

    return None

Code image

This script is straightforward, it base64 decodes the token and loads potential keys from the file provided via argument, and handles errors with exceptions of loading the file.


  1. The brute_force_jwt() function is the core functionality.

  2. The JWT token is split with the help of ‘.’ which separates the different sections of the token.

  3. Then the decode_base64url() function is called to decode the header and the payload.

Then the keys are generated, encoded and verified with the token.


Running the script


The script described above accepts a JWT and path to the wordlist of potential secret keys in the form of command-line arguments in the following format:


python crackjwt.py “your jwt here” <path to list of keys>

Let’s execute the script. For the list of potential secret keys we will use the one provided in the lab description.


Executing the crackjwt.py code

The brute-force was quick and now we can use this key to sign the token. One easy way of doing so is by using jwt.io.


Using JWT.io to edit the token

After entering the secret key on jwt.io, we have the verified signature, now to escalate to administrator, let’s change the sub parameter to administrator.


editing the "sub" value in payload section

Still verified, now we can submit this token with the request.


Updated token with edits

On sending the new verified token with the request, we were able to escalate the privileges to the administrator.


JWT authentication bypass lab wit users cred

JWT Header Parameter Injections


The JWT header parameters like jkw, jku, kid are used at the time of verification process to help the server to determine which key to use for the token’s verification.

The parameters can become attack vectors if the server fails to validate or restrict their use.


JWK Parameter Injection


The JSON Web Signature (JWS) specification describes an optional `jwk` header parameter, which servers can use to embed their public key directly within the token itself in JWK format.


A JWK (JSON Web Key) is a standardized format for representing keys as a JSON object.

Example:


{
    "kid": "ed2Nf8sb-sD6ng0-scs5390g-fFD8sfxG",
    "typ": "JWT",
    "alg": "RS256",
    "jwk":
        {
            "kty": "RSA",
            "e": "AQAB",
            "kid": "ed2Nf8sb-sD6ng0-scs5390g-fFD8sfxG",
            "n": "yy1wpYmffgXBxhAUJzHHocCuJolwDqql75ZWuCQ_cb33K2vh9m"
        }
}

Sometimes, misconfigured servers use any key that is embedded in the jwk parameter.


This behavior can be exploited by signing a modified JWT using your own RSA private key, then embedding the matching public key in the jwk header. Lab link


Screenshot of the lab

According to the lab description this lab uses JWT-based mechanisms for session management,+ and it fails to check if the provided key comes from legitimate or trusted source making it vulnerable to attacks such as key injection or signature bypass attacks.


Understanding the Token


Inspecting cookie in browser

To retrieve the JWT, open the browser’s developer tool, go to the Storage tab, and expand the cookies section in the left panel. Select the domain and you will find the JWT under the value of the session cookie.


Let's use the online tool jwt.io and learn more about token’s information and its structure.


Using JWT.io to decode the token

According to the information we have from the lab description, the server supports JWK in the JWT header. By formatting this token to include a properly formatted JWK field, we can exploit this behavior to bypass authorization.


Exploitation


Navigate to the URL and login using the credentials provided, intercept the request.


Intercepting target request in burp

Send the intercepted request to the repeater.


Once in the repeater tab, navigate to the JSON web token tab to interact with the decoded token.


highlighting the vulnerable request JSON Web Token extenstion in burp

Once you navigate to the JSON web token tab you must be able to see the similar interface as below, you can edit the header and the payload section. This burp extension allows you to embed your public/private keys directly into the token.


To proceed with the attack click on the attack button on the bottom left of the interface.


Highlighting the attack button

The option “embedded JWK” allows you to directly generate a new RSA key pair within the burp-suite JWT editor extension.


Embedded JWK in extenstion

In the payload section change the “sub” parameter from wiener to administrator.


Next, click on the “embedded JWK” option. A dialogue box will appear with a signing key and you’ll have the keys automatically embedded under the JWK parameter.


Viewing embedded key

The final JSON web token is now generated, upon sending the request, you’ll observe that the admin access is granted to the attacker.


Burp with JWT editor.

To solve this lab you need to delete the user Carlos.


Navigate to the admin panel and send a request to the following endpoint, “/admin/delete?username=carlos” This parameter deletes the user.


Add this path to the GET request URL in Burp Repeater. Once the request is sent and processed successfully, the lab will be marked as solved.


Succesful User deletion in challenge page

Conclusion


The scenario where the server fails to check what keys are being used, the attacker can sign the token using their public/private keys with modified JWT.


JKU Parameter Injection


The JKU (JWK set URL) header parameter is used to specify a URL pointing to the JSON Web Key (JWK) set. The URL in the JKU header parameter is the reference to the JWK set containing the key that will be used by the server to verify the signature of the token.


If the website is fetching the keys from the untrusted domains or user-controlled URL, an attacker can take advantage of this behavior by hosting their own JWK set or by using URL parsing techniques to bypass filtration if they exist. Lab link


Let’s understand how attackers can use the above-described scenario to escalate privileges using Portswigger’s lab on JWT authentication bypass via JKU header injection. This lab demonstrates how insecure handling of JKU can allow attackers to supply malicious JWK sets and bypass signature verification.


Viewing the target lab, with highlighted creds

The lab description says that the server fails to check whether the URL provided in the JKU parameter originates from a trusted source.


To exploit this scenario:


  1. Login to the web application using the user credentials provided in the lab description.

  2. Intercept the request using burp.

  3. Send the request to the repeater for further testing and modifications.


Viewing the captured request in burp

Understanding the token


The JSON web token extension allows you to sign the token directly from the burp itself making it convenient and quick. Navigate to the JSON web token tab under repeater. From here we can inspect and modify the token for the JKU injection attack.


Dissecting the request using the JWT extension

The JKU parameter does not exist, therefore we need to add the parameter manually to the token’s header, before that we must publicly host the KEY somewhere. Fortunately, this lab provides an exploit server for the same purpose.


Crafting and hosting JWK


Craft response page in challenge lab

In the body section of the response, you need to provide the format for the JWK and under that the KEYS that we will generate to sign the token.


For now, the structure should look like this:


Pre attack start structure

Let's generate a new RSA key using the JWT editor extension.


Using JWT editor to generate the new key

Click on the JWT editor tab and New RSA key and a dialogue box will appear.


JWT Editor window with RSA key tab open

Click generate and click on Ok, without changing anything.


Burp window with JWT editor open

Now, right click on the RSA key generated and click on the copy public key as JWK.

This JWK that we just copied goes to the response that we are crafting on the exploit server.


Sending malformed exploit request to server

The BODY of the response should look like this, also change the content type in the HEAD from text/html to application/json, now click on store, and we are ready to tweak the JWT in Burpsuite.


Modifying JSON web token

In your burp suite request, replace the KID with the KID in the New RSA key that we generated, it can be found in the response that we crafted.


Updated request in burp JWT editor

These are the changes made, this is the final JWT, now before sending the request we need to sign the JWT, Click on the sign option in the bottom.


Signing the JWT


Signing the JWT in editor

Make sure to use the signing key that you just generated, and don’t modify the header and click ok.


Exploitation


Send the request and click on follow redirection.


Sending exploit request

We have the admin panel access; we were able to escalate privileges. Let’s navigate to the admin panel, to solve this lab we are required to delete the user carlos.

updated request containing admin JWT

Change the path to admin and send the request and find out the endpoint that deletes the user.


Successfully exploited admin screen

‘/admin/delete?username=carlos’ This endpoint deletes the user, let’s send this via request in repeater.


Post exploitation screen with succesful user deletion

Conclusion

The lab is solved, to conclude: the server didn’t verify if the URL in the JKU parameter is trusted, therefore and attacker can craft a JWK with his own keys and publicly host it and use the JKU parameter or replace the URL in the JKU parameter to their own to sign the token.


KID Parameter Injection


The KID (Key ID) parameter in the JSON web token is used to identify the key that was used to sign the token.


The JWK (JSON web keys) store the keys that are used to verify the signature; could be one or multiple. Therefore, the server looks for JWK with the same KID as in the JWT.


As shown in the JKU parameter injection lab that the KID of the JWK hosted publicly and the KID in the JWT matched to exploit the vulnerability.

The KID parameter is often used to retrieve the key from a database or filesystem.


{
    "kid": "path of the file",
    "typ": "JWT",
    "alg": "HS256",
    "k": "asGsADas3421-dfh9DGN-AFDFDbasfd8-anfjkvc"
}

Therefore, the parameter might be vulnerable to directory traversal, if it is then the attacker might use /dev/null because it is an empty file and signing a token with empty string results in valid signature and it is present in most linux systems. Lab Link


To solidify our understanding let’s again solve a lab on portswigger. The following lab will require us to sign the token with an empty string.


KID header lab

It is already mentioned in the lab description that it uses JWT for handling sessions. Also the server uses the KID parameter in the JWT header to fetch the key from the filesystem.


We are required to access the admin panel and delete the user carlos. Use the user credentials provided to login to the application and capture the request in burp and send the request with endpoint /my-account?id=wiener to the repeater.

Loading endpoint /my-account?id=wiener

Understanding the token


We have the JWT in the request. We can use the JWT editor extension from the BApp store or we can use JWT.io, or either of the tools to solve this lab. We will be using JWT.io for this blog.

Copy the JWT and paste it in the JWT.io debugger.


Inspecting the JWT token in JWT editor

As we can see, it says invalid signature. We are supposed to sign this token with an empty string. By removing the secret, the signature should be valid.


Manipulating the signature on JWT token

We are supposed to escalate to the administrator role, therefore we are required to change sub: parameter to administrator and we know that the path traversal exists in the KID parameter.


updated request in jWT io

On making initial changes, the JWT sections look like this, as explained before: /dev/null file is used because it is present in almost all the linux operating systems and it’s empty. On sending this jwt with the request the server will verify the secret against the KID and since both are empty and empty string is a valid signature.


Exploitation


Let’s change the existing JWT in the request and analyze the result.


Loading /admin endpoint in Burp

Changed the endpoint to /admin also the JWT we got after modifications and the response is 401 unauthorized.


Since we are looking for path traversal let’s try a path traversal sequence.


JWT editor with decoded code

Copy the token again and replace it in the request and see if admin privileges are granted, if not continue the sequence.


Trying the sequences, ../../../dev/null, it worked.


login sequence request manipulation using burp

We were able to escalate to admin now the next task is to delete the user carlos. Let’s change the end point to /admin and send a request and it should reveal the endpoint that finally deletes the user carlos.


Burp repeater page with /admin page highlight

We got the endpoint, on changing the endpoint and sending the request Carlos's account should be deleted and eventually the lab should be solved.


Using /admin endpoint to delete user, completing the lab


Conclusion


The KID(Key ID) parameter is that stores the keys that are used to verify the signature, or the path of the keys. Therefore, the JWT when sent with the request, the KID parameter is used to search for the JWK (JSON Web Key) which resides in the server.


Blog Conclusion


JWTs are an awesome way of enabling stateless authentication and authorization, but they come with security vulnerabilities that cannot be passed over. This guide shows how a misconfiguration can be leveraged to compromise an application. To use JWTs safely, enforce strong authentication, validate tokens, securely manage keys, and avoid trusting unverified data from headers.


References



Register for instructor-led online courses today!


Check out our self-paced courses!


Check out our bundled Pricing & Plans for cost effective plans!


Contact us with your custom pen testing needs at: info@darkrelay.com  or WhatsApp.

bottom of page