Introduction
Qu’est ce que les sites web suivants ont en commun en dehors de leur popularité : Airbnb, Github, Shopify, Groupon, Kickstarter, Gitlab, Slideshare, Hulu, Twitch, Les Pages Jaunes, Urban Dictionary, Zendesk, Soundcloud ?
Ils s’appuient tous sur Ruby on Rails, l’un des frameworks MVC les plus utilisés pour écrire les backends d’applications web, et, comme son nom l’indique, c’est un framework en Ruby.
Quel est le point commun entre les outils de sécurité suivants : Metasploit, Beef, WPScan, CeWL, Bettercap, WhatWeb, fingerprinter, envizon, Pipal, Evil-WinRM, PacketFu, Metasm, Hashview, Dradis ? Vous l’aurez deviné, ils sont tous écrits en Ruby.
N’est-ce pas une raison suffisante pour s’intéresser à la sécurité de Ruby ?
Aperçu de Ruby 3.2.0
La dernière version stable de Ruby disponible à l’heure où j’écris ces lignes est la version 3.1.2. Mais le 3 Avril 2022 une version preview de Ruby 3.2.0 a été rendue disponible.
Voyons voir ce que nous avons de beau au menu :
- WASM, introduction du support WebAssembly
- WASI, introduction d’une interface système pour le WebAssembly
- Regexp timeout, une fonctionnalité d’expiration pour les expressions régulières
- des méthodes sympas comme
String#byteindex
- la mise à jour d’Unicode en version 14
- plein d’autres choses mais qui nous intéressent moins
Ca sert à quoi tout ça ? La bonne question pardi, c’est un peu le but de l’article !
WASM et WASI
Bon, la paraphrase de synthèse c’est drôle ni à écrire ni à lire donc voici une citation de Wikipedia définissant WebAssembly :
WebAssembly, abrégé wasm, est un standard du World Wide Web pour le développement d’applications. Il est conçu pour compléter JavaScript avec des performances supérieures. Le standard consiste en un bytecode, sa représentation textuelle et un environnement d’exécution dans un bac à sable compatible avec JavaScript. Il peut être exécuté dans un navigateur Web et en dehors. WebAssembly est standardisé dans le cadre du World Wide Web Consortium.
Comme WebAssembly ne spécifie qu’un langage de bas niveau, le bytecode est généralement produit en compilant un langage de plus haut niveau. Parmi les premiers langages supportés figurent Rust avec le projet/module (crate) wasm-bindgen ainsi que le C et C++, compilés avec Emscripten (basé sur LLVM). De nombreux autres langages de programmation possèdent aujourd’hui un compilateur WebAssembly, parmi lesquels : C#, Go, Java, Lua, Python ou Ruby.
Les navigateurs Web compilent le bytecode wasm dans le langage machine de l’hôte sur lequel ils sont utilisés avant de l’exécuter.
Concrètement WASM est une machine virtuelle bas niveau (comme JVM) qui peut être incluse un peu partout de manière autonome mais surtout faite pour être intégrée aux navigateurs web. En gros, au lieu de faire tourner du JavaScript côté client, les applications pourront faire tourner un binaire pré-compilé, compatible avec JavaScript, mais avec de bien meilleurs performances et de manière plus sécurisée (exécution en sandbox).
D’ailleurs on peut même charger un modèle WebAssembly depuis JavaScript et utiliser en JavaScript des objets exposés depuis le WebAssembly. Et pas besoin d’écrire de l’assembleur pour ça, on peut écrire du code dans des langages de programmation haut niveau et compiler le code en WASM. Donc on va pouvoir écrire du code côté client en Ruby !
WASM peut être aussi utilisé en dehors des navigateurs web, c’est pourquoi WASI propose une interface système modulaire pour les applications autonomes afin de communiquer avec le système d’exploitation.
WebAssembly est déjà supporté par tous les navigateurs (sauf Internet Explorer bien sûr 😏).
On a toujours un peu de mal à se représenter ce que l’on peut faire avec quelque chose d’abstrait, voici des exemples ambitieux :
Cela pourra aussi permettre d’exécuter du code depuis n’importe quel langage dans n’importe quel autre langage via le support WASM ou d’écrire des applications Android.
Des sites web comme runrb.io ou try ruby permettent déjà d’exécuter du Ruby dans le navigateur.
Regexp timeout
Bon WASM c’était pour le côté futur, maintenant parlons sécu avec l’expiration d’expression régulière !
- "Mais dis-moi noraj, en quoi une regexp qui expire au bout de 3 secondes a un lien avec la sécurité ?"
- "Moi"
- "…"
Et bien c’est simple, les personnes qui se baladent souvent sur snyk Vulnerability DB savent sans doute que les XSS ne sont pas les seules vulnérabilités qui ciblent les bibliothèques Javascript, il y a aussi les pollutions de paramètre et les ReDoS.
Pour ceux qui ne connaîtraient pas les ReDoS, ce sont les Regular expression denial of service, attaque de complexité algorithmique qui produit un déni de service en fournissant une expression régulière et/ou une entrée dont l’évaluation prend beaucoup de temps.
Assez parlé, place à la pratique !
Voici une petite regex toute gentille, mais naïve, qui va vérifier que l’entrée utilisateur est bien une adresse email. Faisons un test de performance afin d’observer le temps d’exécution avec les 3 valeurs suivantes :
- une adresse email légitime
- une adresse un peu malveillante (mais pas trop)
- une adresse carrément malveillante (qui en vrai n’a pas grand chose à voir avec une adresse)
require 'benchmark'
n = 1
regexp = /[a-z]+@[a-z]+([a-z\.]+\.)+[a-z]+/
Benchmark.bm(7) do |x|
x.report('legit payload:'.ljust(25)) { regexp.match?('noraj.ruby@acceis.fr') }
x.report('malicious payload:'.ljust(25)) { regexp.match?('noraj@a...............................') }
x.report('very malicious payload:'.ljust(25)) { regexp.match?('noraj@a' + '.' * 40) }
end
Voyons voir ce que cela donne :
$ ruby poc.rb
user system total real
legit payload: 0.000007 0.000001 0.000008 ( 0.000005)
malicious payload: 0.187264 0.000682 0.187946 ( 0.188438)
very malicious payload: 18.484982 0.000146 18.485128 ( 18.501884)
Aïe ! Il se passe quoi ? 😱
- Pour l’adresse email légitime, on observe un temps d’exécution marginal
- Pour l’adresse un peu malveillante, le temps d’exécution est fortement supérieur mais toujours peu important
- Pour l’adresse carrément malveillante, plus de 18 secondes d’exécution
Et encore, là, l’adresse carrément malveillante ne l’était pas tant que ça. Vous voyez déjà l’énorme différence entre la seconde adresse qui utilise 31 points et la 2ième qui en utilise 40. Et ca continue comme ca de manière exponentielle, donc imaginez maintenant que je mette 1000 points et que j’envoie 200 requêtes HTTP par seconde au serveur web, il se passera quoi ? C’est simple, le CPU va instantanément mourir et l’application ne répondra plus, un Déni de Service quoi ! La charge malveillant exploite la fonctionnalité de backtracking des expressions régulières.
Bon, vous voyez après cette longue introduction à quoi va servir le délai d’expiration.
Allez, on se la refait avec Regexp#timeout
:
require 'benchmark'
n = 1
regexp = /[a-z]+@[a-z]+([a-z\.]+\.)+[a-z]+/
Regexp.timeout = 1.0
Benchmark.bm(7) do |x|
x.report('legit payload:'.ljust(25)) { regexp.match?('noraj.ruby@acceis.fr') }
x.report('malicious payload:'.ljust(25)) { regexp.match?('noraj@a...............................') }
x.report('very malicious payload:'.ljust(25)) do
begin
regexp.match?('noraj@a' + '.' * 40)
rescue Regexp::TimeoutError
print 'Wow ! Tout doux ! On va se détendre !'
end
end
end
Allez hop on exécute ça.
$ ruby poc.rb
user system total real
legit payload: 0.000005 0.000002 0.000007 ( 0.000004)
malicious payload: 0.182599 0.000000 0.182599 ( 0.182919)
very malicious payload: Wow ! Tout doux ! On va se détendre ! 0.999344 0.000000 0.999344 ( 1.000076)
Terminé ! Rideau les ReDos ! Ciao ! C’est le 🔥 nan ?
byteindex
String#byteindex
n’a rien d’extraordinaire :
Renvoie l’indice basé sur les octets de la première occurrence de la sous-chaîne pour une sous-chaîne donnée, ou nil si aucune n’est trouvée.
irb(main):001:0> 'acceis'.byteindex('c')
=> 1
irb(main):002:0> 'acceis'.byteindex('cc')
=> 1
String#byterindex
(avec le r
en plus) fait la même chose mais pour la dernière occurrence au lieu de la première.
irb(main):003:0> 'acceis'.byterindex('c')
=> 2
irb(main):004:0> 'acceis'.byterindex('cc')
=> 1
Bon bon… on est content mais c’est basique nan ?
Sauf que ce qui est cool, c’est que, comme la majorité des méthodes de manipulation de string en Ruby, cette méthode supporte Unicode.
Il y a 5 jours dans une semaine de travail donc nous allons exécuter cette routine 5 fois.
Mais bon, ce qui nous intéresse surtout c’est de savoir quand est-ce la prochaine fois qu’on mange de la pizza ! Démonstration !
eat_sleep_pizza_repeat = '🍽️ 😴 🍕 🔁' * 5
puts eat_sleep_pizza_repeat.byteindex('🍕')
$ ruby poc.rb
13
Unicode 14
Trop bien ma pirouette avec String#byteindex
sert de transition pour parler d’Unicode.
Vous n’êtes pas sans savoir que j’aime les magouilles à base d’Unicode.
Et bien Ruby 3.2.0 apportera la compatibilité avec Unicode v14.0.0, soit 838 caractères dont 5 nouveaux scripts et 37 émojis.
Des trucs vraiment indispensables comme 🫠 🫵 🫶 🧌 🛝. Et même des trucs pour la SANTEXPO de Christophe, Steeve et Gwendal comme 🩼 ou 🩻.
0xff
Bon bah voilà ! C’était fun, on a bien rigolé et on s’est mangé une bonne tranche de futur. WebAssembly c’est le turfu comme dirait les jeunes, donc c’est appréciable que çà débarque dans Ruby. Une fois que son utilisation sera plus démocratisée, je n’ose même pas imaginer le nombre de nouveaux problèmes de sécurité que cela va faire émerger ; enfin j’espère 🙊, comme ça je pourrais vous faire une petit billet de blog sur le sujet. Maintenant vous avez le contexte pour vous protéger des ReDoS donc je ne veux plus en voir dans mes tests d’intrusion d’accord ? Et puis bon l’Unicode ❣️, on en reparle très bientôt.
A propos de l’auteur
Article écrit par Alexandre ZANNI alias noraj, Ingénieur en Test d’Intrusion chez ACCEIS.