Ce lab ne fait pas de désérialisation “classique” de manière explicite. L’idée est de provoquer une désérialisation en exploitant le comportement de PHP avec le wrapper phar://, afin d’atteindre une RCE via une gadget chain personnalisée, puis supprimer :
/home/carlos/morale.txt
Identifiants fournis :
wiener:peter
1) Repérage côté application
Upload d’avatar
Après authentification, on peut uploader un fichier via une requête POST sur la fonctionnalité avatar.1) Repérage côté application
Quand on clique sur l’image de profil, l’application la charge via :
/cgi-bin/avatar.php?avatar=wiener
Lecture de fichiers dans /cgi-bin/
En visitant /cgi-bin/, on observe une capacité de lecture sur plusieurs fichiers (utile pour récupérer le code et construire la chaîne).
Name
Size
CustomTemplate.php
1091B
CustomTemplate.php~
0B
Blog.php
628B
Blog.php~
0B
avatar.php
540B
2) Code source récupéré
blog.php
On y voit une classe Blog qui initialise Twig dans __wakeup() :
__sleep() sérialise user et desc
__wakeup() construit un Twig_Environment avec un template “index” basé sur desc
__toString() rend index et injecte user
Point important : desc devient directement un template Twig, donc on peut viser une SSTI.
quand PHP ouvre une ressource via phar://..., il peut déclencher la lecture du manifest PHAR et désérialiser les métadonnées (donc exécuter __wakeup, __destruct, etc. selon la chaîne).
5) Chaîne (gadget chain) construite
Tu fabriques :
un objet Blog avec :
user = "pwned"
desc = <payload Twig SSTI>
puis tu mets cet objet dans CustomTemplate->template_file_path
Comme ça, quand CustomTemplate est détruit, lockFilePath() va concaténer une valeur provenant d’un objet, ce qui force une conversion en string → appelle Blog::__toString() → déclenche twig->render() → interprète desc → exécute la commande.
Upload de out.jpg comme avatar
Appel de la ressource via phar:// :
cgi-bin/avatar.php?avatar=phar://wiener
À ce moment, l’application ouvre la ressource avec le wrapper PHAR, la metadata est désérialisée, la chaîne se déclenche, et le fichier :
<?php
require_once('/usr/local/envs/php-twig-1.19/vendor/autoload.php');
class Blog {
public $user;
public $desc;
private $twig;
public function __construct($user, $desc) {
$this->user = $user;
$this->desc = $desc;
}
public function __toString() {
return $this->twig->render('index', ['user' => $this->user]);
}
public function __wakeup() {
$loader = new Twig_Loader_Array([
'index' => $this->desc,
]);
$this->twig = new Twig_Environment($loader);
}
public function __sleep() {
return ["user", "desc"];
}
}
?>
<?php
class CustomTemplate {
private $template_file_path;
public function __construct($template_file_path) {
$this->template_file_path = $template_file_path;
}
private function isTemplateLocked() {
return file_exists($this->lockFilePath());
}
public function getTemplate() {
return file_get_contents($this->template_file_path);
}
public function saveTemplate($template) {
if (!isTemplateLocked()) {
if (file_put_contents($this->lockFilePath(), "") === false) {
throw new Exception("Could not write to " . $this->lockFilePath());
}
if (file_put_contents($this->template_file_path, $template) === false) {
throw new Exception("Could not write to " . $this->template_file_path);
}
}
}
function __destruct() {
// Carlos thought this would be a good idea
@unlink($this->lockFilePath());
}
private function lockFilePath()
{
return 'templates/' . $this->template_file_path . '.lock';
}
}
?>