@@ -147,11 +147,38 @@ When invoked with 5 or more numbers, each group of 4 and then the last
147
147
group of 1, 2, or (usually) 4 are processed as above, where each group
148
148
refers to a corresponding mt_rand() output.
149
149
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
+
150
177
It is possible to have php_mt_seed skip (ignore) some mt_rand() outputs
151
178
by listing for them 4 numbers that would match any output value. By
152
179
convention, this is typically done by listing "0 0 0 0", which literally
153
180
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.
155
182
156
183
157
184
Complex usage example.
@@ -293,6 +320,125 @@ instance (so that it'd be restarted) via one of many non-security bugs
293
320
or resource exhaustion in PHP (but first make sure you're authorized to
294
321
do things like that).
295
322
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
+
296
442
297
443
Xeon Phi specifics.
298
444
@@ -346,7 +492,7 @@ host CPU rather than a coprocessor). Also, the performance impact of
346
492
the non-vectorized portions of code is lower than on first generation.
347
493
348
494
349
- PHP version curiosities (which shouldn't matter to you ).
495
+ PHP version curiosities (mostly unimportant ).
350
496
351
497
While php_mt_seed supports 3 major revisions of PHP's mt_rand()
352
498
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
445
591
no longer valid for PHP 7.1.0+, which means that when searching for
446
592
seeds for PHP 7.1.0+ for mt_rand() called with a range specified, you
447
593
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.
449
605
450
606
PHP 7.1.0 also introduced MT_RAND_PHP as optional second parameter to
451
607
mt_srand(). When specified, it correctly enables behavior identical to
0 commit comments