# Rootme: JWT Attacks ## JWT - Introduction Link tại <a href="https://www.root-me.org/en/Challenges/Web-Server/JWT-Introduction">đây</a>. ![](https://hackmd.io/_uploads/HynxF6qUn.png) Bài cho 1 form login. ![](https://hackmd.io/_uploads/HkENY69I2.png) Sau đó login với guest ![](https://hackmd.io/_uploads/Hy2UFp982.png) Kiểm tra cookie: `jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6Imd1ZXN0In0.OnuZnYMdetcg7AWGV6WURn8CFSfas6AQej4V9M13nsk` Sau đó decode JWT này tại <a href="https://jwt.io/">jwt.io</a>. Kết quả: ![](https://hackmd.io/_uploads/SJQvip9Un.png) Xuất hiện `Invalid Signature` tất nhiên `signature` không đúng. Thuật toán đang sử dụng ở đây là `HS256`, ta thử chuyển nó sang `none` và xóa phần `Signature` của nó đồng thời sửa `guest` thành `admin`. Có thể làm thủ công hoặc dùng extension `JSON Web Tokens` trên Burp Suite: ![](https://hackmd.io/_uploads/HypRnpqLh.png) JWT mới: `eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJ1c2VybmFtZSI6ImFkbWluIn0.` Kết quả: ![](https://hackmd.io/_uploads/r1lzaa9In.png) Như vậy là ứng dụng sử dụng key để `verify` JWT nhưng cũng bỏ qua nó khi không dùng thuật toán (`"alg": "none"`) > Flag: `S1gn4tuR3_v3r1f1c4t10N_1S_1MP0Rt4n7` ## JWT - Weak secret Link tại <a href="https://www.root-me.org/en/Challenges/Web-Server/JWT-Weak-secret">đây</a> ![](https://hackmd.io/_uploads/rJ_LCp5U2.png) Vào bài thì tại `/hello` trả về thông báo: `Let's play a small game, I bet you cannot access to my super secret admin section. Make a GET request to /token and use the token you'll get to try to access /admin with a POST request.` Như vậy: - `/token` chứa token (sử dụng method GET) - `/admin` nơi verify token (sử dụng method POST) Vào `/token`: ![](https://hackmd.io/_uploads/rJ-Dy05Lh.png) Token nhận được: `eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJyb2xlIjoiZ3Vlc3QifQ.4kBPNf7Y6BrtP-Y3A-vQXPY9jAh_d0E6L4IUjL65CvmEjgdTZyr2ag-TM-glH6EYKGgO3dBYbhblaPQsbeClcw` Decode tại <a href="https://jwt.io/">jwt.io</a> : ![](https://hackmd.io/_uploads/HkP510qU2.png) Nó cho ta biết thuật toán sử dụng là `HS512` (đây là mã hóa đối xứng dùng một`secret key` để mã hóa cũng và giải mã ) có payload `"role": "guest"` và mục tiêu là đổi `guest` sang `admin`. Thử lấy token đó verify ở `/admin`: ![](https://hackmd.io/_uploads/Sy9ClAcL3.png) Thử đối thuật toán sang `none` như bài trên nhưng tất nhiên là không được. Nhìn lại tiêu đề là **Weak secret** nghĩ ngay đến việc brute-force. Ta sử dụng `hashcat` cùng với wordlist là `rockyou.txt`: ``` hashcat -a 0 -m 16500 eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJyb2xlIjoiZ3Vlc3QifQ.4kBPNf7Y6BrtP-Y3A-vQXPY9jAh_d0E6L4IUjL65CvmEjgdTZyr2ag-TM-glH6EYKGgO3dBYbhblaPQsbeClcw /usr/share/wordlists/rockyou.txt ``` Kết quả: ![](https://hackmd.io/_uploads/S1zOQA9Ih.png) `secret key` là `lol`. Bây giờ verify lại jwt: ![](https://hackmd.io/_uploads/SkSC7AcIn.png) JWT mới: `eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJyb2xlIjoiYWRtaW4ifQ.y9GHxQbH70x_S8F_VPAjra_S-nQ9MsRnuvwWFGoIyKXKk8xCcMpYljN190KcV1qV6qLFTNrvg4Gwyv29OCjAWA` ![](https://hackmd.io/_uploads/S1wbV0cLh.png) > Flag: `PleaseUseAStrongSecretNextTime` ## JWT - Revoked token Link tại <a href="https://www.root-me.org/en/Challenges/Web-Server/JWT-Revoked-token">đây</a> ![](https://hackmd.io/_uploads/HJHtV0q8h.png) Bài cho source: ``` #!/usr/bin/env python3 # -*- coding: utf-8 -*- from flask import Flask, request, jsonify from flask_jwt_extended import JWTManager, jwt_required, create_access_token, decode_token import datetime #from apscheduler.schedulers.background import BackgroundScheduler import threading import jwt from config import * # Setup flask app = Flask(__name__) app.config['JWT_SECRET_KEY'] = SECRET jwtmanager = JWTManager(app) blacklist = set() lock = threading.Lock() # Free memory from expired tokens, as they are no longer useful def delete_expired_tokens(): with lock: to_remove = set() global blacklist for access_token in blacklist: try: jwt.decode(access_token, app.config['JWT_SECRET_KEY'],algorithm='HS256') except: to_remove.add(access_token) blacklist = blacklist.difference(to_remove) @app.route("/web-serveur/ch63/") def index(): return "POST : /web-serveur/ch63/login <br>\nGET : /web-serveur/ch63/admin" # Standard login endpoint @app.route('/web-serveur/ch63/login', methods=['POST']) def login(): try: username = request.json.get('username', None) password = request.json.get('password', None) except: return jsonify({"msg":"""Bad request. Submit your login / pass as {"username":"admin","password":"admin"}"""}), 400 if username != 'admin' or password != 'admin': return jsonify({"msg": "Bad username or password"}), 401 access_token = create_access_token(identity=username,expires_delta=datetime.timedelta(minutes=3)) ret = { 'access_token': access_token, } with lock: blacklist.add(access_token) return jsonify(ret), 200 # Standard admin endpoint @app.route('/web-serveur/ch63/admin', methods=['GET']) @jwt_required def protected(): access_token = request.headers.get("Authorization").split()[1] with lock: if access_token in blacklist: return jsonify({"msg":"Token is revoked"}) else: return jsonify({'Congratzzzz!!!_flag:': FLAG}) if __name__ == '__main__': scheduler = BackgroundScheduler() job = scheduler.add_job(delete_expired_tokens, 'interval', seconds=10) scheduler.start() app.run(debug=False, host='0.0.0.0', port=5000) ``` Có 2 endpoints: ![](https://hackmd.io/_uploads/rkXzHRqI2.png) Oke bây giờ vào `/login`: Nó sẽ yêu cầu json chứa `username` và `password` đều có giá trị là `admin` sẽ được token: ![](https://hackmd.io/_uploads/rk69S058n.png) Token: `eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2ODU5MzU0NzcsIm5iZiI6MTY4NTkzNTQ3NywianRpIjoiZGIwZmQwYjAtZjM1Zi00MjEzLWI5MTEtZWRmZmNhODdlN2Q2IiwiZXhwIjoxNjg1OTM1NjU3LCJpZGVudGl0eSI6ImFkbWluIiwiZnJlc2giOmZhbHNlLCJ0eXBlIjoiYWNjZXNzIn0.aqfOmz1hmOdxdz3C5Fg3OksrfQ0ZnYUedP-s9g_CzVg` Thử dùng nó trong `/admin` qua header `Authorization: Bearer <token>`: ![](https://hackmd.io/_uploads/HJrXLC58n.png) `Token is revoked`. Đúng vậy vì sau khi tạo thì token sẽ được add vào `backlist`: ``` access_token = create_access_token(identity=username,expires_delta=datetime.timedelta(minutes=3)) ret = { 'access_token': access_token, } with lock: blacklist.add(access_token) ``` Và nếu token dùng trong header `Authorization` không trong `blacklist` thì mới hiện flag: ``` access_token = request.headers.get("Authorization").split()[1] with lock: if access_token in blacklist: return jsonify({"msg":"Token is revoked"}) else: return jsonify({'Congratzzzz!!!_flag:': FLAG}) ``` Token vừa tạo xong đã bị cho vào `blacklist` thì sao bypass được ??? Thử decode nó: ![](https://hackmd.io/_uploads/Sy-WO0qU3.png) Bản chất việc revoke token giống như việc xóa bỏ nó ngay sau khi xác thực thành công. Code trong bài này sẽ so sánh xem có token nào y hệt trong blacklist không, vậy có thể biến đổi token để nó khác ban đầu mà nội dung không thay đổi? Nhìn lại cấu trúc JWT: ![](https://hackmd.io/_uploads/HJ6CiC9Ih.png) Đó là cả 3 phần đều được encode bằng `Base64`, điều quan tâm ở đây là `Base64` sẽ padding thêm dấu `=` nếu thiếu ký tự và nó cũng sẽ bỏ nó nếu đủ. Vậy chỉ cần thêm dấu `=` vào token thì đc token mới không nằm trong `blacklist` nhưng nội dung vẫn vậy. Ta sẽ thêm dấu `=` vào cuối (`signature`) vì nếu thêm vào `header` hay `payload` sẽ thay đổi `signature`. Kết quả: ![](https://hackmd.io/_uploads/HJy4pCqLn.png) > Flag: `Do_n0t_r3v0ke_3nc0d3dTokenz_Mam3ne-Us3_th3_JTI_f1eld` ## JWT - Unsecure File Signature Link tại <a href="https://www.root-me.org/en/Challenges/Web-Server/JWT-Unsecure-File-Signature">đây</a> ![](https://hackmd.io/_uploads/rJKYIls8n.png) ![](https://hackmd.io/_uploads/BJ2yweoI2.png) Xem source thì thấy có endpoint `/admin` và có jwt trả về từ server trong cookie và nó dùng để verify vào endpoit đó: ![](https://hackmd.io/_uploads/SJFjvlsU2.png) Decode JWT: ![](https://hackmd.io/_uploads/rk90DgsLn.png) Đầu tiên mình thử làm theo như mấy bài trên nhưng tất nhiên đều không được. Xem lại đầu bài có nhắc đến `(K)ind (I)dentification (D)ance` dễ dàng biết được bài này liên quan đến `kid`. `kid` (Key ID): Cung cấp `id` mà server có thể sử dụng để xác định `secret key` trong trường hợp có nhiều `secret key` để lựa chọn. Nó nằm trong phần `header` của JWT. Trong nhiều trường hợp nó không chỉ là `id` mà còn là một đường dẫn đến 1 file chứa `secret key` và server sẽ lấy nội dung của file đó để verify. Cụ thể ở đây khi bạn thay đổi giá trị `kid` thành `abc`: ![](https://hackmd.io/_uploads/SJjTFxi82.png) Như vậy là server sẽ lấy nội dung trong file `abc` của thư mục `keys` làm khóa và ở đây file `abc` không được tìm thấy. Vậy nếu chúng ta biết được nội dung của 1 file rồi lấy nó làm khóa (`secret key`) thì sẽ verify thành công. Tuy nhiên điều đó có thể khó khả thi vì làm được khi file nào đó bị leak ra. Đơn giản nhất là lợi dụng `/dev/null` trên linux, đây là file rỗng và `kid` trỏ đến đây thì sẽ `key` lấy được là null. Liên tưởng đến lỗ hỗng `LFI` để `kid` trỏ đến `/dev/null` bằng payload `../../../../`và sign nó bằng giá trị `null` là `AA==` (dạng base64). Tuy nhiên khi `kid` có giá trị là `../../../../../../../dev/null` thì kết quả là `keys/dev/null` ![](https://hackmd.io/_uploads/HJ4dhliU3.png) Có vẻ chưa `../` đã bị replace. Ta bypass cái này bằng `....//....//....//....//....//....//....//dev/null` ![](https://hackmd.io/_uploads/BJl4aesLh.png) Lấy JWT mới set lại cookie, kết quả: ![](https://hackmd.io/_uploads/BJ--RejLh.png) > Flag: `RM{Uns3cUr3_f1l3_H4ndl1nG!!}` ## JWT - Public key Link tại <a href="https://www.root-me.org/en/Challenges/Web-Server/JWT-Public-key">đây</a> ![](https://hackmd.io/_uploads/B1-sLfjL3.png) Như phần **Statement** thì có 3 endpoints: - `/key` lấy `public key` (dùng method GET) - `/auth` tạo token (dùng method POST) - `/admin` verify để lấy flag (dùng method POST) Đầu tiền vào `/key` để lấy `public key`, format lại được: ``` -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3iPoCJJxcoYjU2CE4doM vEC0kyxzh1wRGxDT0QV4Xr+vdscPZQ3RZFDtA8255BoqTWN5BFOH0LWwOrIlkVB+ eQFZ4JWoEweZLRsWYl6BNQm5p+TL9w9nMsNd+p3ha6I9bepLdzJgGm1zv8Ef9MyQ uyZ8Aoaw16XzgevFqeegrlpHLO3odTnqOZ+K8xlXPYEvcoOBEr1LE9XpKxAg5puN alokg+NeRMZBIYkEFODjDkZYsgCo97sfb/Y5hB7m6k4aTvxzpWSSQiBW90UJFLBi BUn7fgwX6BuXt78l3S/eB9ByMscFZdtVckEzt5LP1dkjNcWS8CHfM1Eibq8Ca2eq nQIDAQAB -----END PUBLIC KEY----- ``` Phần `/auth` yêu cầu: `You have to provide 'username=YOURNAME' in your POST request` nên mình `username=chuong` (không nhận giá trị `admin`) và nhận được token: ![](https://hackmd.io/_uploads/BJdULEiI2.png) Sau đó dùng token này vào `/admin` qua header `Authorization: Bearer <token>`: ![](https://hackmd.io/_uploads/S1lOL4oUn.png) Decode nó xem thử: ![](https://hackmd.io/_uploads/r1GU9zsI3.png) JWT sử dụng thuật toán **RS256**. Đây là thuật toán **asymmetric** (bất đối xứng), trong đó **private key** được sử dụng để sign JWT và **public key** được sử dụng để verify signature của JWT. ![](https://hackmd.io/_uploads/HJsciMjUn.png) Vậy hướng khai thác ở đây là gì? Sau một lúc tìm hiểu về phần này thì đó là thay đổi thuật toán. **HS256** chỉ có 1 **secret key** để sign cũng như verify trong khi **RS256** như mình đã nói sử dụng 2 key như trên. Vậy nếu chuyển từ **RS256** sang **HS256** thì sao? Thì **HS256** sẽ dùng **public key** để verify (thứ mà bài đã cho). Việc bây giờ là từ **public key** đó tạo **signature** cho JWT. Mình sẽ dùng <a href="https://github.com/ticarpi/jwt_tool">jwt_tool.py</a> để tạo bằng: ``` python3 jwt_tool.py <token> -S hs256 -k public.pem ``` `public.key` là file chứa **public key** mà bài cho. Và nhớ sửa giá trị `username` thành `admin`. ![](https://hackmd.io/_uploads/SyfQ84oIh.png) Kết quả: ![](https://hackmd.io/_uploads/B1YV8Vo8h.png) > Flag: `HardcodeYourAlgoBro` ## JWT - Header Injection Link tại <a href="https://www.root-me.org/en/Challenges/Web-Server/JWT-Header-Injection">đây</a> ![](https://hackmd.io/_uploads/ryWxPujI3.png) Oke bắt đầu vào: ![](https://hackmd.io/_uploads/B1V7PuoU2.png) Có chỗ login bằng JWT: ![](https://hackmd.io/_uploads/BJ8IvuoLn.png) Tuy nhiên không có chỗ tạo token nên mình lấy cái `placeholder` để verify xem sao: ![](https://hackmd.io/_uploads/H1IaDui83.png) Có vé phải login bằng `user` là `Neo`, decode thấy phần `payload` chứa `message` có vẻ không liên quan nên nên mình bỏ đi cho gọn, và tất nhiên thì verify vẫn thất bại rồi: ![](https://hackmd.io/_uploads/B1EudOiUn.png) Như tiêu đề của bài là **Header Injection**. Mình đã tìm hiểu sơ qua về phần này và sẽ tổng quát lại nó như sau: Như ta biết thì phần `header` của JWT có 1 số tham số hay gặp như `alg`, `typ`, ngoài ra còn 1 số nữa dễ bị khai thác: - `kid`: mình đã nói ở bài `JWT - Unsecure File Signature` - `jwk` (JSON Web Key): Cung cấp một đối tượng JSON được đại diện cho khóa. - `jku` (JSON Web Key Set URL): Cung cấp một URL mà từ đó các server có thể tìm nạp một bộ khóa chứa khóa chính xác. Ở bài này do mình đã làm rồi và xác định là khai thác dựa vào `jwk` nên mình sẽ nói về nó luôn. Ví dụ về `jwk`: ``` "jwk": { "kty": "RSA", "e": "AQAB", "kid": "ed2Nf8sb-sD6ng0-scs5390g-fFD8sfxG", "n": "yy1wpYmffgXBxhAUJzHHocCuJolwDqql75ZWuCQ_cb33K2vh9m" } ``` Nó đại diện cho **public key** của `RSA`, `kid` này cũng định nghĩa như trên, `n`, `e` là tham số liên quan về `RSA` (liên quan đến việc tính toán của thuật toán này). Tham số `jkw` trong bài có thể kiểm soát từ người dùng mà server lại không xấc thực nó => bị tấn công. Ý tưởng ở đây là tạo **private key** sau đó nhúng `jwk` đó vào `header`. Đầu tiên mình sẽ tạo RSA key bằng extension `JWT Editor Keys` của `Burp Suite`: chọn `New RSA Key` -> `Generate` (không cần quan tâm giá trị bit vì nó sẽ được điều chỉnh lại). ![](https://hackmd.io/_uploads/rJAxMYjLn.png) Tiếp theo dùng extension `JSON Web Token` để nhúng nó vào: `Attack` -> `Embedded JWK` -> chon khóa rồi `Ok`. ![](https://hackmd.io/_uploads/BkLQ4tjI2.png) ![](https://hackmd.io/_uploads/ByRPVtoI3.png) JWT tạo được: `eyJraWQiOiJiZDFlMDZmYy03YTBhLTQ5MWUtYjU1MC02YWYxMTQ4MTRjMmIiLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp3ayI6eyJrdHkiOiJSU0EiLCJlIjoiQVFBQiIsImtpZCI6ImJkMWUwNmZjLTdhMGEtNDkxZS1iNTUwLTZhZjExNDgxNGMyYiIsIm4iOiJsd2lUNWhUOVhPOHptaVRoRW5BX3A0aWtqblUxZkJyV2tYVWNTRVRwT19DNWZEeHl1dmVIV1A1T1NpdG4tbjNXME9SSUFWMHlTeW9ZeVhEZnFSMFloZGFiZk16WHR2cnB1b1BucXlPRGpMUHBvR1lJSFV4MGhHekdNajlPbFFMQl9pNTF6ZENGWF9JNXF5bEhPUndiNXhKcUNMcUhCUU5JakFPUjRPM1czSF8wdGZhaGhtT29fblVLRFFob3NMblFNc2N1bGZYUDZvVmFfOHZpWVZKNDAwSTdVemxqYzRTSElzb0lUUnZFR0FjTVRaRFZXd3o2NmZ2QTlJZ3FLa3V1YnVVV293OU5yTE9wTDdxM1RaYU12UzJ3NWI3TnQwMTlOaDA3bnRXcDE4RjlQRG9NT0dWVVdXVTJ0UXlMTVRXX0dQMnp1aDRyMXFFSVNVMXhGWGU5MFEifX0.eyJ1c2VyIjoiTmVvIiwiaWF0IjoxNjc2NjM3ODMyfQ.M0n_hBVOnzi-j751yiqzUmhVCPLxhrVZX4lK6Tfyc34Nm_-3gTYeKOwOtecb8CbYgjz6nWnbVQIIKleeSsVNcIlfYH8e1n2wJ6JAyNvqCCHHA5W7dzWX9hnZ0tiRSLNurNAutp-ghYjTfQouTZc4MyLAwx00YqsQtiCKQQqBXXOXq8A5-vhdPWp7czxy8LT1C5GqM7dWBsyeFZMHa5a-l4wVKmHvKZunG0PWxr1p6xPsZ5DTgPWyexPITjreiGJoEKCXmr5beUAnzu2Q29YA6s_XLNoUUSp6B6O5W_iGPPez8cSLZhmnK-knOqa7Wof6kK3Q33e6d4CBtgnyEtV4_w` Giờ verify nó, kết quả: ![](https://hackmd.io/_uploads/r1y97KsL3.png) > Flag: `RM{N3v3r_All0w_UnTrusTed_K3ys}` ## JWT - Unsecure Key Handling Link tại <a href="https://www.root-me.org/en/Challenges/Web-Server/JWT-Unsecure-Key-Handling">đây</a> ![](https://hackmd.io/_uploads/BkvGHigKn.png) ![](https://hackmd.io/_uploads/B1GrHsgtn.png) Xem qua dễ dàng tìm được endpoint `/admin`. Và có được JWT, và tất nhiên không phải admin rồi: ![](https://hackmd.io/_uploads/rybirjgth.png) Decode JWT: ![](https://hackmd.io/_uploads/HkvCSolF2.png) Nhận thấy có `"jku": "jwk.json"` nghĩ ngay đến việc inject vào `jku` này rồi. Nhưng sau một hồi fuzz thì mình không tìm được gì trên trang web, có vẻ `jwk.json` được lưu vào đâu đó không biết được. Tuy nhiên khi thay đổi `jku` thì: ![](https://hackmd.io/_uploads/H1tOLseKh.png) Như vậy address phải bắt đầu là `http://localhost` Mình nghĩ này đến các ký thuật SSRF nhưng bằng nhiều cách vẫn không được. 1 tháng sau thì mình gặp được bài <a href="https://thanhlocpanda.wordpress.com/2023/06/22/jwt-headers-injection-jku-unsecure-key-handling-root-me-part-ii/">này</a> :3 Như vậy là tạo 1 site bắt đầu với localhost (của mình sẽ là `http://localhostchuong.000webhostapp.com`) bằng <a href="https://www.000webhost.com/">000webhost.com</a> và có `jwk.json` chứa thông tin key tạo qua burp(tham khảo bài trên): ![](https://hackmd.io/_uploads/rk1_YsgY3.png) Key tạo bằng burp: ![](https://hackmd.io/_uploads/HJdjvjeY2.png) Sẽ được: ![](https://hackmd.io/_uploads/ryYiPixKn.png) Và giờ chỉ việc sign bằng key có sẵn: ![](https://hackmd.io/_uploads/H1BrOjlYh.png) Kết quả: ![](https://hackmd.io/_uploads/r1dtOslF3.png) > Flag: `RM{4lw4y5_ch3ck_th3_JKU_C0rr3ctly}` ## Tổng kết Mình có vẽ 1 mindmap nhỏ về **JWT Attacks** mà mình tìm hiểu được ^^ ![](https://hackmd.io/_uploads/BkGZA_2In.png) Một số nguồn tham khảo thêm: - <a href="https://portswigger.net/web-security/jwt">JWT attacks - PortSwigger</a> - https://infosecwriteups.com/attacks-on-json-web-token-jwt-278a49a1ad2e - https://github.com/KathanP19/HowToHunt/blob/master/JWT/JWT.md - https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/JSON%20Web%20Token - https://medium.com/swlh/hacking-json-web-tokens-jwts-9122efe91e4a - https://medium.com/@TamasPolgar/can-timing-attack-be-a-practical-security-threat-on-jwt-signature-ba3c8340dea9 Một số tools: - <a href="https://jwt.io/">jwt.io</a> - <a href="https://github.com/ticarpi/jwt_tool">jwt_tool</a>, <a href="https://github.com/lmammino/jwt-cracker">jwt-cracker</a>, <a href="https://github.com/silentsignal/rsa_sign2n">rsa_sign2n</a>, ...