Skip to content

Commit bc64e47

Browse files
committed
Land rapid7#3370, cleanup for sap_icm_urlscan
2 parents 8235556 + 848227e commit bc64e47

File tree

1 file changed

+80
-64
lines changed

1 file changed

+80
-64
lines changed

modules/auxiliary/scanner/sap/sap_icm_urlscan.rb

Lines changed: 80 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
# Current source: https://github.com/rapid7/metasploit-framework
44
##
55

6-
require 'rex/proto/http'
76
require 'msf/core'
87

98
class Metasploit3 < Msf::Auxiliary
@@ -30,143 +29,160 @@ def initialize(info = {})
3029
register_options(
3130
[
3231
OptString.new('VERB', [true, "Verb for auth bypass testing", "HEAD"]),
33-
OptString.new('URLFILE', [true, "SAP ICM Paths File", "sap_icm_paths.txt"])
32+
OptPath.new('URLFILE', [true, "SAP ICM Paths File",
33+
File.join(Msf::Config.data_directory, 'wordlists', 'sap_icm_paths.txt')])
3434
], self.class)
3535
end
3636

3737
# Base Structure of module borrowed from jboss_vulnscan
3838
def run_host(ip)
39-
# If URLFILE is set empty, obviously the user made a silly mistake
40-
if datastore['URLFILE'].empty?
41-
print_error("Please specify a URLFILE")
42-
return
43-
end
44-
45-
# Initialize the actual URLFILE path
46-
if datastore['URLFILE'] == "sap_icm_paths.txt"
47-
url_file = "#{Msf::Config.data_directory}/wordlists/#{datastore['URLFILE']}"
48-
else
49-
# Not the default sap_icm_paths file
50-
url_file = datastore['URLFILE']
51-
end
52-
53-
# If URLFILE path doesn't exist, no point to continue the rest of the script
54-
if not File.exists?(url_file)
55-
print_error("Required URL list #{url_file} was not found")
56-
return
57-
end
58-
59-
res = send_request_cgi(
39+
res = send_request_cgi(
6040
{
6141
'uri' => "/" + Rex::Text.rand_text_alpha(12),
6242
'method' => 'GET',
63-
'ctype' => 'text/plain',
64-
}, 20)
43+
})
6544

6645
if res
6746
print_status("Note: Please note these URLs may or may not be of interest based on server configuration")
6847
@info = []
69-
if not res.headers['Server'].nil?
48+
if res.headers['Server']
7049
@info << res.headers['Server']
7150
print_status("#{rhost}:#{rport} Server responded with the following Server Header: #{@info[0]}")
7251
else
7352
print_status("#{rhost}:#{rport} Server responded with a blank or missing Server Header")
7453
end
7554

76-
if (res.body and /class="note">(.*)code:(.*)</i.match(res.body) )
55+
if (res.body && /class="note">(.*)code:(.*)</i.match(res.body) )
7756
print_error("#{rhost}:#{rport} SAP ICM error message: #{$2}")
7857
end
7958

8059
# Load URLs
81-
urls_to_check = []
82-
File.open(url_file) do |f|
60+
urls_to_check = check_urlprefixes
61+
File.open(datastore['URLFILE']) do |f|
8362
f.each_line do |line|
8463
urls_to_check.push line
8564
end
8665
end
8766

8867
print_status("#{rhost}:#{rport} Beginning URL check")
68+
@valid_urls = ''
8969
urls_to_check.each do |url|
9070
check_url(url.strip)
9171
end
92-
# check custom URLs
93-
check_urlprefixes
9472
else
9573
print_error("#{rhost}:#{rport} No response received")
9674
end
9775

76+
if @valid_urls.length > 0
77+
l = store_loot(
78+
'sap.icm.urls',
79+
"text/plain",
80+
datastore['RHOST'],
81+
@valid_urls,
82+
"icm_urls.txt", "SAP ICM Urls"
83+
)
84+
print_line
85+
print_good("Stored urls as loot: #{l}") if l
86+
end
9887
end
9988

10089
def check_url(url)
90+
full_url = write_url(url)
10191
res = send_request_cgi({
102-
'uri' => url,
92+
'uri' => normalize_uri(url),
10393
'method' => 'GET',
104-
'ctype' => 'text/plain',
105-
}, 20)
94+
})
10695

10796
if (res)
108-
if not @info.include?(res.headers['Server']) and not res.headers['Server'].nil?
109-
print_good("New server header seen [#{res.headers['Server']}]")
110-
@info << res.headers['Server'] #Add To seen server headers
97+
if res.headers['Server']
98+
unless @info.include?(res.headers['Server'])
99+
print_good("New server header seen [#{res.headers['Server']}]")
100+
@info << res.headers['Server'] #Add To seen server headers
101+
end
111102
end
112103

113-
case
114-
when res.code == 200
115-
print_good("#{rhost}:#{rport} #{url} - does not require authentication (200) (length: #{res.headers['Content-Length']})")
116-
when res.code == 403
117-
print_good("#{rhost}:#{rport} #{url} - restricted (403)")
118-
when res.code == 401
119-
print_good("#{rhost}:#{rport} #{url} - requires authentication (401): #{res.headers['WWW-Authenticate']}")
104+
case res.code
105+
when 200
106+
print_good("#{full_url} - does not require authentication (#{res.code}) (length: #{res.headers['Content-Length']})")
107+
@valid_urls << full_url << "\n"
108+
when 403
109+
print_status("#{full_url} - restricted (#{res.code})")
110+
when 401
111+
print_status("#{full_url} - requires authentication (#{res.code}): #{res.headers['WWW-Authenticate']}")
112+
@valid_urls << full_url << "\n"
120113
# Attempt verb tampering bypass
121114
bypass_auth(url)
122-
when res.code == 404
115+
when 404
123116
# Do not return by default, only display in verbose mode
124-
vprint_status("#{rhost}:#{rport} #{url.strip} - not found (404)")
125-
when res.code == 500
126-
print_good("#{rhost}:#{rport} #{url} - produced a server error (500)")
127-
when res.code == 301, res.code == 302
128-
print_good("#{rhost}:#{rport} #{url} - redirected (#{res.code}) to #{res.headers['Location']} (not following)")
117+
vprint_status("#{full_url} - not found (#{res.code})")
118+
when 400,500
119+
print_status("#{full_url} - produced a server error (#{res.code})")
120+
when 301, 302,
121+
print_good("#{full_url} - redirected (#{res.code}) to #{res.redirection} (not following)")
122+
@valid_urls << full_url << "\n"
123+
when 307
124+
print_status("#{full_url} - redirected (#{res.code}) to #{res.redirection} (not following)")
129125
else
130-
vprint_status("#{rhost}:#{rport} - unhandle response code #{res.code}")
126+
print_error("#{full_url} - unhandled response code #{res.code}")
127+
@valid_urls << full_url << "\n"
131128
end
132129

133130
else
134-
print_status("#{rhost}:#{rport} #{url} - not found (No Response code Received)")
131+
vprint_status("#{full_url} - not found (No Repsonse code Received)")
135132
end
136133
end
137134

135+
def write_url(path)
136+
if datastore['SSL']
137+
protocol = 'https://'
138+
else
139+
protocol = 'http://'
140+
end
141+
142+
"#{protocol}#{rhost}:#{rport}#{path}"
143+
end
144+
138145
def bypass_auth(url)
139-
print_status("#{rhost}:#{rport} Check for verb tampering (#{datastore['VERB']})")
146+
full_url = write_url(url)
147+
vprint_status("#{full_url} Check for verb tampering (#{datastore['VERB']})")
140148

141149
res = send_request_raw({
142-
'uri' => url,
150+
'uri' => normalize_uri(url),
143151
'method' => datastore['VERB'],
144152
'version' => '1.0' # 1.1 makes the head request wait on timeout for some reason
145-
}, 20)
153+
})
146154

147-
if (res and res.code == 200)
148-
print_good("#{rhost}:#{rport} Got authentication bypass via HTTP verb tampering (length: #{res.headers['Content-Length']})")
155+
if (res && res.code == 200)
156+
print_good("#{full_url} Got authentication bypass via HTTP verb tampering")
149157
else
150-
print_status("#{rhost}:#{rport} Could not get authentication bypass via HTTP verb tampering")
158+
vprint_status("#{rhost}:#{rport} Could not get authentication bypass via HTTP verb tampering")
151159
end
152160
end
153161

162+
# "/urlprefix outputs the list of URL prefixes that are handled in the ABAP part of the SAP Web AS.
163+
# This is how the message server finds out which URLs must be forwarded where.
164+
# (SAP help) -> this disclose custom URLs that are also checked for authentication
154165
def check_urlprefixes
155-
# "/urlprefix outputs the list of URL prefixes that are handled in the ABAP part of the SAP Web AS. This is how the message server finds out which URLs must be forwarded where." (SAP help)
156-
# -> this disclose custom URLs that are also checked for authentication
166+
urls = []
157167
res = send_request_cgi({
158168
'uri' => "/sap/public/icf_info/urlprefix",
159169
'method' => 'GET',
160-
'ctype' => 'text/plain',
161-
}, 20)
162-
if (res and res.code == 200)
170+
})
171+
172+
if (res && res.code == 200)
163173
res.body.each_line do |line|
164174
if line =~ /PREFIX=/
165175
url_enc = line.sub(/^PREFIX=/, '')
176+
# Remove CASE and VHOST
177+
url_enc = url_enc.sub(/&CASE=.*/, '')
166178
url_dec = URI.unescape(url_enc).sub(/;/, '')
167-
check_url(url_dec.strip)
179+
urls << url_dec.strip
168180
end
169181
end
182+
else
183+
print_error("#{rhost}:#{rport} Could not retrieve urlprefixes")
170184
end
185+
186+
urls
171187
end
172188
end

0 commit comments

Comments
 (0)