1 min read

another hmac collision with delimiter injection

In the previous post, we discussed about how hmac collision can arise because of the lack of delimiter between two concatenated values.

The program can be modified with the additional delimiter between two different values username + ":" + str(amount)

from cryptography.hazmat.primitives import hashes, hmac
import os

key = os.urandom(16)


def generate_transaction_hmac(username, amount):
    h = hmac.HMAC(key, hashes.SHA256())
    data = username + ":" + str(amount)
    h.update(data.encode())

    signature = h.finalize()
    return data, signature.hex()


def sign_transaction(username, amount):
    return generate_transaction_hmac(username, amount)


def verify_transaction_signature(data, signature) -> bool:
    print("Verifying: ", data, signature)
    data_arr = data.split(":")
    expected_username, expected_amount = data_arr[0], data_arr[1]

    expected_data, expected_signature = generate_transaction_hmac(expected_username, expected_amount)
    print("Expected: ", expected_data, expected_signature)
    return expected_signature == signature

Yet, there is an edge case that the code fails to address.

Given these inputs:

username = "amy"
amount = 999

Normal scenario

We use the sign_transaction function to generate the data and signature variable.

data, signature = sign_transaction(username, amount)
print("Original: ", data, signature)
print(verify_transaction_signature(data, signature))

When we execute the code, we can observe that the transaction signature is valid.

Original:  amy:999 4df3eaddd776b1963a3013ab2af1d6ef20d2442a706ce0c84780d3dfbb5b1067
Verifying:  amy:999 4df3eaddd776b1963a3013ab2af1d6ef20d2442a706ce0c84780d3dfbb5b1067
Expected:  amy:999 4df3eaddd776b1963a3013ab2af1d6ef20d2442a706ce0c84780d3dfbb5b1067
True

Injecting extra delimiter to create hash collision

Instead of using the data as an input for verify_transaction_signature(), we can write a custom payload that can cause a hash collision.

data, signature = sign_transaction(username, amount)
payload = f"{username}:{amount}:0"
print("Original: ", data, signature)
print(verify_transaction_signature(payload, signature))

Notice that the data (payload amy:999:0) for the verifying function is different.

Original:  amy:999 4df3eaddd776b1963a3013ab2af1d6ef20d2442a706ce0c84780d3dfbb5b1067
Verifying:  amy:999:0 4df3eaddd776b1963a3013ab2af1d6ef20d2442a706ce0c84780d3dfbb5b1067
Expected:  amy:999 4df3eaddd776b1963a3013ab2af1d6ef20d2442a706ce0c84780d3dfbb5b1067
True

Because verify_transaction_signature() splits the data and uses the first two array values, we can exploit this behavior to allow the verifying function to generate the hash that matches the original data.

Where is the hash collision?

Two different sets of inputs are producing the same signature 4df3eaddd776b1963a3013ab2af1d6ef20d2442a706ce0c84780d3dfbb5b1067.

User 1:

  • username: amy
  • amount: 999

Malicious user:

  • username: amy:999
  • amount: 0

Even though their usernames and amounts are different, the signatures generated (by the transaction signing function) are the same.

Credits

Please sign up for https://pentesterlab.com/ if you want similar practices on code reviews.