Post

Découverte de vulnérabilités dans l'API de Oblyk

Oblyk est une application web communautaire très complète autour de l’escalade, développée en Ruby avec le framework Ruby on rails.

Elle possède plus de 20 000 utilisateurs et est utilisée par beaucoup de salles d’escalade (C’est d’ailleurs comme ça que je l’ai découverte).

Voyant que l’application est totalement gratuite et open-source, j’ai voulu remercier Oblyk en vérifiant la sécurité de son API.

Environnement de test

Oblyk est séparé en 2 applications:

  • Le front (avec Nuxt.js)
  • L’API (avec Ruby on Rails).

J’ai écrit 2 Dockerfile afin de déployer les applications dans des conteneurs docker. L’API avec l’image ruby:2.6.5 et bundler 2.4.17, et le front avec l’image node:15.14.0-buster.

Recherche de vulnérabilités

Username enumeration (CWE-200, CWE-209)

On commence par tester l’authentification. En regardant les routes, on trouve 7 endpoints:

1
2
3
4
5
6
7
8
9
10
11
12
13
namespace :api do
    namespace :v1 do
      namespace :sessions do
        post 'tokens', controller: :token, action: :refresh
        post 'sign_in', controller: :signin, action: :create
        post 'sign_up', controller: :signup, action: :create
        post 'reset_password', controller: :password, action: :create
        put 'new_password', controller: :password, action: :update
        delete 'sign_in', controller: :signin, action: :destroy
      end
      [...]
  end
end

Message d’erreurs et timing attacks

Certains endpoints permettent de confirmer l’existence d’un compte utilisant l’email donné.

Cette information peut être utilisée par un attaquant pour un bruteforce ou pour établir une liste d’utilisateurs.


Lors de la création d’un compte, un message d’erreur est donné dans le cas où l’email est utilisé. Message d'erreur sur lors de la création d'un compte

Sur la page de connexion, le message d’erreur cache cette information, mais il est quand même possible d’obtenir cette information sur la page de connexion en utilisant une Timing Attack.

1
2
3
4
user = User.find_by email: params[:email]
not_found && return if user.blank?
if user.authenticate(params[:password])
  [...]

Une requête SQL récupère l’utilisateur assigné à l’email donné, et c’est uniquement si l’utilisateur existe que le mot de passe est hashé par authenticate puis comparé au hash enregistré.

Une fonction de hashage étant assez lourde, le serveur mettra plus de temps à répondre à notre requête. En comparant les temps de réponses, il est possible de déduire si la fonction de hashage a été utilisée, et donc que l’email donné existe.

Différence des temps de réponses sur des tentatives de connexion

Dans l’exemple, les temps de réponses sont extrêmement courts parce que les tests sont effectués localement, mais la différence de temps de réponse peut quand même être observée en ligne.

Sur la page de réinitialisation de mot de passe, on a aussi un message confirmant l’envoi de l’email, ou une erreur révélant que l’email n’est pas utilisé. Message d'erreur sur la réinitialisation de mot de passe

Une timing attack y est aussi présente, la sauvegarde du token et l’envoi de l’email étant exécutés de manière synchrone, ajoutant du délai à la réponse en cas d’email valide.

1
2
3
4
5
6
7
8
def send_reset_password_instructions
  token = SecureRandom.base36
  self.reset_password_token = token
  self.reset_password_token_expired_at = Time.zone.now + 30.minutes
  save!

  UserMailer.with(user: self, token: token).reset_password.deliver_now
end

Injection SQL

Oblyk possède énormément de fonctionnalités de recherche utilisant l’input utilisateur. Une seule erreur introduirait une injection SQL.

La majorité des requêtes SQL sont faites depuis la classe ActiveRecord.

En lisant la documentation, on peut voir que certaines méthodes sont vulnérables si l’argument donné est un string.

La documentation ne précise pas toujours que certaines méthodes peuvent être vulnérables. C’est par exemple le cas de find_by qui donne directement l’input à where, méthode étant dite vulnérable aux injection SQL dans sa documentation.

Avant d’inspecter chaque endpoint, on peut déjà essayer de trouver une injection en recherchant uniquement des fonctions injectables utilisées de manière dangereuse.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function search() {
    grep --color=always -rE "$1" .
}

function searchUses() {
    method=$1
    par_or_space_regx='( |\()'
    printf "\nString concat:\n"
    concat_regex='".*#{.*"'
    search "$method$par_or_space_regx$concat_regex"

    printf "\nRaw use:\n"
    input_text="params"
    search "$method$par_or_space_regx$input_text"

    printf "\nVariables use:\n"
    variable_regex='[:\@a-zA-Z0-9_-]*($|\))'
    search "$method$par_or_space_regx$variable_regex"
}

dangerous_methods=("calculate" "average" "count" "maximum" "minimum" "sum" "delete_all" "delete_by" "destroy_by" "exists?" "find_by" "find_by!" "find_or_create_by" "find_or_create_by!" "find_or_initialize_by" "from" "group" "having" "joins" "lock" "not" "select" "reselect" "where" "rewhere" "update_all" )

for method in "${dangerous_methods[@]}"
do
    occurences=$(grep -rE "$method(\(| )" . | wc -l)
    printf "\n\n------ USES OF: %s, OCCURENCES: %s\n" "$method" "$occurences"
    searchUses "$method"
done

J’ai écrit ce script qui va utiliser des regex pour chercher des fonctions injectables ayant comme argument:

  • Un string qui insère une variable
  • Directement notre input (Ce qui nous donnerais une injection quasi certaine)
  • Une variable ou un champ

Cette manière de faire ne remplace pas une vérification manuelle ! Les regex ne couvrent pas toutes les fonctions dangereuses et les syntaxes possibles.

On a maintenant une liste de 132 utilisations de fonctions potentiellement injectable.

Injection SQL (UNION-Based) dans le filtre d’une recherche

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def geo_json
  [...]
  climbing_filter = '1 = 1'
  climbing_filter = "#{params[:climbing_type]} IS TRUE" unless params.fetch(:climbing_type, 'all') == 'all'

  # Crags
  if params.fetch(:crags, 'true') == 'true'
    crags = minimalistic ? @department.crags : @department.crags.includes(photo: { picture_attachment: :blob })
    crags.where(climbing_filter).find_each do |crag|
      features << crag.to_geo_json(minimalistic: minimalistic)
    end
  end
  [...]
end

On voit ici notre input climbing_type directement intégré dans un texte avant d’être passé à where ! En jettant un oeil à routes.rb, on trouve l’endpoint qui mène à cette fonction.

On essaye déjà une utilisation prévue avec la valeur bouldering (qui est le nom d’une colonne de la table crag). La requête SQL sera:

1
SELECT `crags`.* FROM `crags` WHERE ...AND (bouldering IS TRUE) ORDER BY...

J’affiche seulement la partie de la requête qui nous intéresse, pour éviter que ça soit trop long.

Maintenant, si on essaye d’y mettre la valeur SLEEP(2), la requête SQL sera:

1
SELECT `crags`.* FROM `crags` WHERE ...AND (SLEEP(2) IS TRUE) ORDER BY...

C’est confirmé, la requête ayant pris 2 secondes, ça montre que notre SLEEP a fonctionné ! On a donc bien une injection SQL.

On peut maintenant essayer d’intégrer une autre table à la réponse avec UNION.

Ajout des emails utilisateur à la réponse Dans cet exemple on peut voir les email utilisateurs dans la réponse, mais on peut y intégrer n’importe quelle information (Nom de la base de donnée, sa version, les mots de passe utilisateurs etc..)

Cross-site scripting (XSS)

Un XSS peut apparaître lorsqu’un input utilisateur est inséré dans une page sans qu’il soit proprement “nettoyé”, permettant une injection de JavaScript.

Injecter du JavaScript dans une page permet (dans la majorité des cas) à un attaquant de voler les cookies des utilisateurs visitant la page, menant au vol de leur compte.

Stored-XSS dans la recherche de partenaire

La carte des grimpeurs est une carte qui permet aux utilisateurs de se placer sur la carte en précisant une ou plusieurs villes, afin de trouver quelqu’un avec qui grimper.

En indiquant une ville sur laquelle apparaître, il est possible d’y ajouter une note.

Carte des grimpeurs sur Lille Carte des grimpeurs sur Lille

Les notes étant directement intégrées à la page avec v-html, il est possible d’y insérer du HTML nous permettant d’exécuter du JavaScript.

1
2
3
4
<div v-if="climberLocality.locality_user.description">
  <u>Note par rapport à </u>
  <div v-html="climberLocality.locality_user.description" />
</div>

On peut vérifier avec cette note qui affiche une pop-up contenant nos cookies.

1
<img src=x onerror=alert(document.cookie)/>

Si maintenant on essaye d’afficher les grimpeurs, on voit que notre javascript est exécuté ! Pop-up affichant nos cookies sur la carte des grimpeurs

On peut aussi trouver la note affichée sur le profil de notre utilisateur, elle aussi avec v-html:

1
2
3
<v-card-text>
  <div v-html="userLocality.description" />
</v-card-text>

En allant sur le profil de l’attaquant, on peut aussi voir que notre javascript est exécuté.

Cette vulnérabilité peut être utilisée pour discrètement envoyer à l’attaquant les cookies de tous les utilisateurs ayant vu la note afin de voler leur compte.

Conclusion

Les choses à retenir

  1. Comme on a pu le voir, certains endroits de la documentation de Ruby on Rails préviennent que certaines fonctions sont dangereuses, mais ce n’est pas précisé partout. Il ne faut donc pas hésiter à directement jeter un oeil au code de fonctions pouvant être dangereuses, ainsi que chercher des resources externes.

  2. C’est valable pour tous les languages “Dynamically typed”: Toujours vérifier le type des variables contenant l’input utilisateur. Une variable reçu de l’utilisateur peut souvent être un texte, un booléen, un dictionnaire, une liste, etc.., et rendre son utilisation imprévisible.

Derniers mots

Ce projet m’a appris beaucoup de choses sur le fonctionnement de Ruby et du framework Ruby on Rails.

Je n’ai pas montré ici toutes les vulnérabilités trouvées, certaines étant similaires et/ou moins intéressantes que celles écrites ici.

Merci beaucoup à Lucien, le créateur et développeur d’Oblyk, de garder ce projet gratuit et open-source, et d’avoir rapidement écrit des correctifs pour les problèmes reportés.

Merci d’avoir lu. N’hésitez pas à me contacter pour la moindre question ou requête.

Cet article est sous licence CC BY 4.0 par l'auteur.