⏰ THE TIME IS UP!

How something as ordinary as response time can quietly expose information.

⏰ THE TIME IS UP!

Every system leaks information as part of its operation. Given a system's inputs and outputs, we can make assumptions about what happens between them.

Specific behavioral quirks and features - from a warning log for a corrupt input, to the way RFC standards were implemented - can help attackers infer the OS and stack used, software installed, and sometimes even the exact versions.

Developers encounter such information disclosure vulnerabilities sooner or later, sometimes only after an attack - through exposing .env files and MySQL databases to the Internet, forgetting to disable stack trace output on public-facing error handlers, and leaving version output for nginx enabled.

However, today we will talk about a deceptively minor oversight most developers are completely oblivious to - where time itself becomes a variable leaked by the system.

This post covers timing attacks, observable timing discrepancies, and a medium-severity security vulnerability I discovered in a popular nation-building game, NationStates.

What does this mean? How does this happen?

Due to the specifics of I/O operations such as database lookups, file reads and writes, and caching, two separate operations in a single product can take different amounts of time to complete depending on how the code "branches".

In other words, in some cases, the attacker can derive whether or not an operation was successful based on the amount of time it took to complete.

Let's take a look at this code example:

def forgot_password(nation, email):
public_msg = (
"For privacy reasons, we cannot confirm the e-mail address you entered was correct. "
"But if it was, an email has been sent you with a link to log in to your nation!"
)
record = db.find_nation(nation)
if not record:
return public_msg
if email == record.email:
# ...send the reset email...
return public_msg

It's a public method that has three possible outcomes:

1) the user (aka "nation") doesn't exist, and a message is returned (failure)

2) the email doesn't match, and thus a message is returned (failure)

3) the email matches, we send the reset email, and then return the same generic message (success)

If the e-mail was sent to the victim successfully, to the attacker, it means that the user uses the e-mail address specified, which opens opportunities for social engineering and other attacks. However, at first sight, it seems there's no way to determine if the e-mail was sent successfully, since the same exact message is returned for both a failure and a success.

So how does an attacker go about this?

The Exploit

The attacker can repeatedly send HTTP requests to determine the precise amount of time it takes the remote server to process the request. This information is then used to infer what function calls took place on the remote server.

We can split our example code into two branches, where each branch takes a significantly different amount of time to complete:

1) looking for nation (50ms) -> data doesn't match expectations (1ms) -> return the message (1ms)

2) looking for nation (50ms) -> data matches expectations (1ms) -> send the reset e-mail (200ms) -> return the message (1ms)

[the precise millisecond timings are illustrative]

Thus, by recording a baseline response time of several incorrect e-mails, and then comparing it to a supposed victim's e-mail, the malicious actor can observe a significant increase in response time, which signifies the e-mail entered is correct.

In our specific example, the difference in time will approach hundreds of milliseconds, but at scale one can detect extremely small discrepancies. Timing attacks are often used against cryptographical implementations to expose secret values and to gather information about the private key, where the time differences are often sub-millisecond.

This specific vulnerability was discovered on NationStates by me. I validated this behavior in a controlled test using my own accounts, then responsibly disclosed the findings. The issue was patched promptly.

Mitigation

Now that we know about the vulnerability, it's essential to learn how to protect your applications.

In theory, all we need to do is to make sure that both valids and invalids take roughly the same amount of time. However, the practical approaches vary, so here are a few of the most common ones:

1) Return the response as soon as possible. For any operations that require an extensive amount of time, push it to a background queue, so that the response is not delayed.

2) Enforce a minimum response time, so that the request takes N ms regardless of whether or not it's a success or a failure.

3) In cryptography and high-load contexts, use constant-time comparison wherever possible. Different languages provide their own tools like System.Security.Cryptography.CryptographicOperations.FixedTimeEquals in C#.

I hope you've enjoyed reading this write-up. I do not feel like the issue is talked about enough, and in some cases, it can lead to a severe loss of confidentiality and put sensitive information at a risk of exposure. Hope I've helped you learn something new. Stay safe!