Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Brute Forcing HS256 Is Possible: The Importance of Using Strong Keys in Signing JWTs

DZone's Guide to

Brute Forcing HS256 Is Possible: The Importance of Using Strong Keys in Signing JWTs

In this article, we go over what exactly a JSON Web Token is, how they can be cracked using brute force, and the best way to avoid this security issue.

· Security Zone
Free Resource

Discover how to provide active runtime protection for your web applications from known and unknown vulnerabilities including Remote Code Execution Attacks.

What Is a JSON Web Token?

A JSON Web Token encodes a series of claims in a JSON object. Some of these claims have a specific meaning, while others are left to be interpreted by the users. These claims can be verified and trusted because it is digitally signed. Examples of these claims are issuer (iss), subject (sub), audience (aud), expiration time (exp), not before (nbf), and issued at (iat). JWTs can be signed using a secret (with HMAC algorithm) or a public/private key pair using RSA or Elliptic-Curve.

Structure of a JSON Web Token

A signed, compact-serialized JWT consists of three main parts separated by a .namely:

  • Header
  • Payload
  • Signature

A JWT comes in this structure, aaaaaa.bbbbbb.ccccc. aaaaaaa represents the header, bbbbb represents the payload while cccccc represents the signature.

Header

The header typically consists of two parts: the type of the token, which is JWT, and the hashing algorithm such as HS256 or RS256. Example:

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

Then, this JSON is Base64Url encoded to form the first part of the JWT.

Payload

This part of the token carries the claims. An example of a payload can be found below:

{
  "sub": "1234567890",
  "name": "John Doe",
  "manager": true
}

The payload is then Base64Url encoded to form the second part of the JWT.

Signature

The last part of the token is the signature. The signature is composed from the signing of the encoded header, encoded payload, and a secret.

An example of a signature using the HMAC SHA256 (HS256) algorithm can be created like so:

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)


A signed JWT

JWT Signing Algorithms

The most common algorithms for signing JWTs are:

  • HMAC + SHA256 (HS256)
  • RSASSA-PKCS1-v1_5 + SHA256 (RS256)
  • ECDSA + P-256 + SHA256 ( ES256)

HS256

Hash-based Message Authentication Code (HMAC) is an algorithm that combines a certain payload with a secret using a cryptographic hash function like SHA-256. The result is a code that can be used to verify a message only if both the generating and verifying parties know the secret. In other words, HMACs allow messages to be verified through shared secrets.

This is an example showcasing an HMAC-based signing algorithm:

const encodedHeader = base64(utf8(JSON.stringify(header)));
const encodedPayload = base64(utf8(JSON.stringify(payload)));
const signature = base64(hmac(`${encodedHeader}.${encodedPayload}`, 
                              secret, sha256));
const jwt = `${encodedHeader}.${encodedPayload}.${signature}`;

An example of signing a JWT with the HS256 algorithm using the jsonwebtoken JavaScript library can be found below:

var jwt = require('jsonwebtoken');

const payload = {
  sub: "1234567890",
  name: "John Doe",
  manager: true
};

const secretKey = 'secret';

const token = jwt.sign(payload, secretKey, { 
    algorithm: 'HS256',
    expiresIn: '10m' // if ommited, the token will not expire
});

RS256

RSA is a public-key algorithm. Public-key algorithms generate split keys: one public key and one private key.

For public-key signing algorithms:

const encodedHeader = base64(utf8(JSON.stringify(header)));
const encodedPayload = base64(utf8(JSON.stringify(payload)));
const signature = base64(rsassa(`${encodedHeader}.${encodedPayload}`, 
                                privateKey, sha256));
const jwt = `${encodedHeader}.${encodedPayload}.${signature}`;

When signing and verifying JWTs signed with RS256, you deal with a public/private key pair rather than a shared secret. There are many ways to create RSA keys. OpenSSL is one of the most popular libraries for key creation and management:

# Generate a private key
openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048

# Derive the public key from the private key
openssl rsa -pubout -in private_key.pem -out public_key.pem

Both PEM files are simple text files. Their contents can be copied and pasted into your JavaScript source files and passed to the jsonwebtoken library.

// You can get this from private_key.pem above.
const privateRsaKey = `<YOUR-PRIVATE-RSA-KEY>`; 

const signed = jwt.sign(payload, privateRsaKey, {
    algorithm: 'RS256',
    expiresIn: '5s'
});
// You can get this from public_key.pem above.
const publicRsaKey = `<YOUR-PUBLIC-RSA-KEY>`;

const decoded = jwt.verify(signed, publicRsaKey, {
    // Never forget to make this explicit to prevent
    // signature stripping attacks.
    algorithms: ['RS256'], 
});

ES256

ECDSA algorithms also make use of public keys. We can use OpenSSL to generate the key as well:

# Generate a private key (prime256v1 is the name of the parameters used
# to generate the key, this is the same as P-256 in the JWA spec). 
openssl ecparam -name prime256v1 -genkey -noout -out ecdsa_private_key.pem

# Derive the public key from the private key
openssl ec -in ecdsa_private_key.pem -pubout -out ecdsa_public_key.pem

If you open these files you will note that there is much less data in them. This is one of the benefits of ECDSA over RSA. The generated files are in PEM format as well, so simply pasting them in your source will suffice.

// You can get this from private_key.pem above.
const privateEcdsaKey = `<YOUR-PRIVATE-ECDSA-KEY>`; 

const signed = jwt.sign(payload, privateEcdsaKey, {
    algorithm: 'ES256',
    expiresIn: '5s'
});
// You can get this from public_key.pem above.
const publicEcdsaKey = `<YOUR-PUBLIC-ECDSA-KEY>`;

const decoded = jwt.verify(signed, publicEcdsaKey, {
    // Never forget to make this explicit to prevent
    // signature stripping attacks.
    algorithms: ['ES256'], 
});

Note: These algorithm notes above are excerpts from the very comprehensive Auth0 JWT book written by Sebastian Peyrott. Download it for more information on signing and validating JWTs using these algorithms mentioned above.

Brute Forcing an HS256 JSON Web Token

As secure as HS256 is, especially when implemented the right way, brute-forcing a JSON web token signed with small and medium sized shared-secrets using HS256 is still very possible.

Recently, I came across a tool written in C on GitHub. It is a multi-threaded JWT brute force cracker. With a huge computing power, this tool can find the secret key of a HS256JSON Web token.

Please note the RFC7518 standard states that, "A key of the same size as the hash output (for instance, 256 bits for "HS256") or larger MUST be used with this algorithm." Auth0 secret keys exceed this requirement making cracking via this or similar tools all but impossible.

Implementing a Brute Force Attack

I used a Mac computer to try out the brute force attack. First, make sure you have openssl installed. If it is not, install it with homebrew like so:

brew install openssl

Then run this command in the terminal like so:

make OPENSSL=/usr/local/opt/openssl/include OPENSSL_LIB=-L/usr/local/opt/openssl/lib

On Ubuntu, you can install openssl like so:

apt-get install libssl-dev

The specs of my MacBook are mentioned below:

  • Processor 2.7 GHz Intel Core i5.
  • Memory 8GB 1867 MHz DDR3.
  • Graphics Intel Iris Graphics 6100 1536 MB.

Go ahead and clone the jwt-cracker from GitHub.

An example JWT signed with HS256 and a secret, Sn1f is:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.cAOIAifu3fykvhkHpbuhbvtH807-Z2rI1FS3vX1XMjE

Now, run the jwt-cracker from your terminal to crack the token like so:

time ./jwtcrack eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.cAOIAifu3fykvhkHpbuhbvtH807-Z2rI1FS3vX1XMjE

Note: Make sure the jwtcrack script is executable by running chmod a+x./jwtcrack

Crack

It took about 6.16s on my laptop to crack the secret key.

With the help of jwt.io, let's sign another token quickly, but with a secret, secret.

Run the cracker again with the new JWT like so:

time ./jwtcrack eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

Crack a new tokenCrack another token

From the results shown above, it cracked the token and got our secret, which is actually secret, in about 3273.51s.

Security Concerns and Recommendation

Let's take another look at the keys we used to generate the tokens that were cracked easily. What are the key sizes? The first key, Sn1f, is 32-bit.

1 character = 8 bits

The second key, secret, is 48-bit. This is simply too short to be a valid key. In fact, the JSON Web Algorithms RFC 7518 states that a key of the same size as the hash output (for instance, 256 bits for "HS256") or larger MUST be used with the HS256 algorithm.

I, therefore, recommend that anyone trying to generate a JSON Web token and signing them with HS256 to use a properly sized secret key. Auth0 secret keys are 512 bits in length and not susceptible to this type of brute force attack. Additionally, Auth0 allows you to easily sign your JWTs with RS256.

Conclusion

JSON Web Tokens (JWTs) are lightweight and can easily be used across platforms and languages. They are a clever way to pass signed or encrypted information between applications. There are several JWT libraries available for signing and verifying the tokens.

We have also been able to show that brute forcing of HS256 JWTs is certainly possible when used with short and weak secret keys. Unfortunately, this is a limitation of most shared-key approaches. All cryptographic constructions, including HS256, are insecure if used with short keys, so ensure that implementations satisfy the standardized requirements.

As a rule of thumb, make sure to pick a shared-key as long as the length of the hash. For HS256 that would be a 256-bit key (or 32 bytes) minimum. 

Find out how Waratek’s award-winning application security platform can improve the security of your new and legacy applications and platforms with no false positives, code changes or slowing your application.

Topics:
security ,json ,jwt

Published at DZone with permission of Prosper Otemuyiwa, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}