Skip to content

Commit a8d86c4

Browse files
committed
Added more examples and PHP peculiarities
CVS-ID: README 1.16
1 parent 3b144ae commit a8d86c4

File tree

1 file changed

+159
-3
lines changed

1 file changed

+159
-3
lines changed

Diff for: README

+159-3
Original file line numberDiff line numberDiff line change
@@ -147,11 +147,38 @@ When invoked with 5 or more numbers, each group of 4 and then the last
147147
group of 1, 2, or (usually) 4 are processed as above, where each group
148148
refers to a corresponding mt_rand() output.
149149

150+
Although the syntax above technically requires specification of ranges
151+
when matching multiple mt_rand() outputs, it is also possible to match
152+
exact outputs and/or outputs from mt_rand() without a range specified by
153+
listing the value to match twice (same minimum and maximum) and/or by
154+
listing the range "passed into" mt_rand() as "0 2147483647". The latter
155+
is assumed to be equivalent to mt_rand() called without a range. For
156+
example, this matches first mt_rand() output of 1328851649 followed by
157+
second mt_rand() output of 1423851145:
158+
159+
$ time ./php_mt_seed 1328851649 1328851649 0 2147483647 1423851145
160+
Pattern: EXACT EXACT
161+
Version: 3.0.7 to 5.2.0
162+
Found 0, trying 0xfc000000 - 0xffffffff, speed 15658.7 Mseeds/s
163+
Version: 5.2.1+
164+
Found 0, trying 0x48000000 - 0x49ffffff, speed 91.9 Mseeds/s
165+
seed = 0x499602d2 = 1234567890 (PHP 5.2.1 to 7.0.x; HHVM)
166+
Found 1, trying 0xfe000000 - 0xffffffff, speed 91.9 Mseeds/s
167+
Found 1
168+
169+
real 0m47.035s
170+
user 6m15.273s
171+
sys 0m0.004s
172+
173+
This is on the same machine as above. The additional constraint (on the
174+
second mt_rand() output) caused no slowdown, but removed extra seeds
175+
from the output.
176+
150177
It is possible to have php_mt_seed skip (ignore) some mt_rand() outputs
151178
by listing for them 4 numbers that would match any output value. By
152179
convention, this is typically done by listing "0 0 0 0", which literally
153180
means "the output must be from 0 to 0 as returned by mt_rand(0, 0)", a
154-
condition that is always true.
181+
condition that is always true. This is illustrated further below.
155182

156183

157184
Complex usage example.
@@ -293,6 +320,125 @@ instance (so that it'd be restarted) via one of many non-security bugs
293320
or resource exhaustion in PHP (but first make sure you're authorized to
294321
do things like that).
295322

323+
The above attack might not have worked against old versions of Drupal
324+
as-is. There could be reseeding and/or other uses of mt_rand() getting
325+
in the way. It is just an illustration of how to approach applying
326+
mt_rand() seed cracking in a real-world'ish scenario.
327+
328+
329+
When extra tools or php_mt_seed changes are needed.
330+
331+
Sometimes applications post-process mt_rand() outputs in ways very
332+
different from what was illustrated above. It isn't always practical to
333+
write and use tiny scripts like we did above to reverse those tokens,
334+
generated passwords, etc. to mt_rand() output constraints that can be
335+
passed to php_mt_seed.
336+
337+
In simpler ones of those other cases, a pre-existing extra tool can be
338+
used. For example, if a PHP application exposes md5(mt_rand()) as a
339+
token, then a password hash cracker such as John the Ripper -jumbo or
340+
Hashcat can be used to crack the MD5 hash, retrieving the mt_rand()
341+
output value that can be passed to php_mt_seed. For example:
342+
343+
$ php -r 'echo md5(mt_rand()), "\n";' | tee hashfile
344+
a67d0e9f38d578eefb1720d611211a26
345+
$ time ./john --format=raw-md5 --incremental=digits --max-length=10 --fork=32 hashfile 2>/dev/null
346+
Loaded 1 password hash (Raw-MD5 [MD5 128/128 AVX 4x3])
347+
1871584565 (?)
348+
349+
real 0m40.922s
350+
user 6m41.117s
351+
sys 0m1.739s
352+
$ time ./php_mt_seed 1871584565
353+
Pattern: EXACT
354+
Version: 3.0.7 to 5.2.0
355+
Found 0, trying 0x48000000 - 0x4bffffff, speed 24159.2 Mseeds/s
356+
seed = 0x4be01ac0 = 1272978112 (PHP 3.0.7 to 5.2.0)
357+
seed = 0x4be01ac1 = 1272978113 (PHP 3.0.7 to 5.2.0)
358+
Found 2, trying 0x5c000000 - 0x5fffffff, speed 25725.1 Mseeds/s
359+
seed = 0x5fe49e4e = 1608818254 (PHP 3.0.7 to 5.2.0)
360+
seed = 0x5fe49e4f = 1608818255 (PHP 3.0.7 to 5.2.0)
361+
Found 4, trying 0xfc000000 - 0xffffffff, speed 28185.7 Mseeds/s
362+
Version: 5.2.1+
363+
Found 4, trying 0x86000000 - 0x87ffffff, speed 234.4 Mseeds/s
364+
seed = 0x86d2e002 = 2261966850 (PHP 7.1.0+)
365+
Found 5, trying 0xc2000000 - 0xc3ffffff, speed 234.5 Mseeds/s
366+
seed = 0xc24768d7 = 3259459799 (PHP 5.2.1 to 7.0.x; HHVM)
367+
seed = 0xc24768d7 = 3259459799 (PHP 7.1.0+)
368+
Found 7, trying 0xc6000000 - 0xc7ffffff, speed 234.4 Mseeds/s
369+
seed = 0xc6d8b812 = 3336091666 (PHP 5.2.1 to 7.0.x; HHVM)
370+
seed = 0xc6d8b812 = 3336091666 (PHP 7.1.0+)
371+
Found 9, trying 0xfe000000 - 0xffffffff, speed 234.5 Mseeds/s
372+
Found 9
373+
374+
real 0m18.478s
375+
user 9m48.751s
376+
sys 0m0.015s
377+
$ php -r 'mt_srand(3259459799); echo md5(mt_rand()), "\n";'
378+
a67d0e9f38d578eefb1720d611211a26
379+
$ php -r 'mt_srand(3336091666); echo md5(mt_rand()), "\n";'
380+
a67d0e9f38d578eefb1720d611211a26
381+
382+
We found two seeds that generate our observed md5(mt_rand()) token (and
383+
some more that would do it with other versions of PHP). While both are
384+
correct given what we knew (assuming that we know the PHP version), in a
385+
real-world scenario only one of those would likely allow us to correctly
386+
infer prior and predict further mt_rand() outputs. That's good enough.
387+
388+
The invocation of JtR is sub-optimal in that it'd search all strings of
389+
up to 10 digits rather than numbers that fit in 31 bits and do not start
390+
with a 0 (except for the number 0). This can be partially corrected by
391+
splitting the invocation in two:
392+
393+
$ time ./john --format=raw-md5 --incremental=digits --max-length=9 --fork=32 hashfile 2>/dev/null
394+
Loaded 1 password hash (Raw-MD5 [MD5 128/128 AVX 4x3])
395+
396+
real 0m4.540s
397+
user 0m43.320s
398+
sys 0m1.762s
399+
$ time ./john --format=raw-md5 --mask='[12]?d?d?d?d?d?d?d?d?d' --fork=32 hashfile 2>/dev/null
400+
Loaded 1 password hash (Raw-MD5 [MD5 128/128 AVX 4x3])
401+
1871584565 (?)
402+
403+
real 0m4.092s
404+
user 1m58.155s
405+
sys 0m1.609s
406+
407+
As a slightly trickier example, old eZ Publish used:
408+
409+
$time = time();
410+
$userID = $user->id();
411+
$hashKey = md5( $userID . ':' . $time . ':' . mt_rand() );
412+
413+
yet this can be cracked similarly, by obtaining the timestamp from the
414+
server itself (such as from the HTTP headers) or assuming synchronized
415+
time and by knowing or cracking the target user ID as well. The known
416+
portions of the information may be specified in a JtR or Hashcat mask
417+
as-is (e.g., as --mask='100:1503415769:[12]?d?d?d?d?d?d?d?d?d' in the
418+
second invocation of JtR above) and then mt_rand() output extracted from
419+
the cracked "password" and passed into php_mt_seed.
420+
421+
As a harder to handle example, old MediaWiki used:
422+
423+
function generateToken( $salt = '' ) {
424+
$token = dechex( mt_rand() ) . dechex( mt_rand() );
425+
return md5( $token . $salt );
426+
}
427+
428+
Two mt_rand() outputs at once are unlikely to be quickly cracked by a
429+
program not aware of mt_rand() specifics. This is a case where we'd
430+
need to modify php_mt_seed internals - specifically, introduce recording
431+
of two mt_rand() outputs in php_mt_seed's diff() function and have it
432+
compute MD5 and compare the result against our token value. Then we'd
433+
invoke php_mt_seed with dummy command-line arguments, but not exactly
434+
arbitrary ones: e.g., "0 1" is non-trivial enough for php_mt_seed to
435+
always call diff() and thus let our added code take over the comparison.
436+
437+
(Cracking seeds from old MediaWiki tokens as above is readily supported
438+
as an example exploit in Snowflake, an alternative to php_mt_seed.
439+
However, in general either php_mt_seed or Snowflake would need custom
440+
code written for new cases like this.)
441+
296442

297443
Xeon Phi specifics.
298444

@@ -346,7 +492,7 @@ host CPU rather than a coprocessor). Also, the performance impact of
346492
the non-vectorized portions of code is lower than on first generation.
347493

348494

349-
PHP version curiosities (which shouldn't matter to you).
495+
PHP version curiosities (mostly unimportant).
350496

351497
While php_mt_seed supports 3 major revisions of PHP's mt_rand()
352498
algorithm and that sort of covers PHP 3.0.7 through 7.1.0+ (up to the
@@ -445,7 +591,17 @@ without a range, and php_mt_seed still assumes so. This assumption is
445591
no longer valid for PHP 7.1.0+, which means that when searching for
446592
seeds for PHP 7.1.0+ for mt_rand() called with a range specified, you
447593
can specify at most a range one smaller than that, thus "0 2147483646"
448-
being the maximum that php_mt_seed supports for those versions.
594+
being the maximum that php_mt_seed supports for those versions. This
595+
minor limitation shouldn't matter in practice, except that you might
596+
need to be aware you can continue to specify a range of "0 2147483647"
597+
to indicate that no range was passed into mt_rand().
598+
599+
PHP 7.1.0 also aliased rand() to mt_rand() and srand() to mt_srand().
600+
This means that on one hand you can use php_mt_seed to crack rand()
601+
seeds for PHP 7.1.0+ (since those are also mt_rand() seeds), but on the
602+
other hand this cross-seeding and cross-consumption of random numbers
603+
can affect which attacks work or don't work, and exactly how, against
604+
specific applications that make use of both sets of PHP functions.
449605

450606
PHP 7.1.0 also introduced MT_RAND_PHP as optional second parameter to
451607
mt_srand(). When specified, it correctly enables behavior identical to

0 commit comments

Comments
 (0)