Threat Intelligence

De deux bugs mémoire dans Oj à un RCE pré-auth sur GitLab self-managed via le diff de notebooks Jupyter

Admin CyberAfrik 27 July 2026 28 lectures
De deux bugs mémoire dans Oj à un RCE pré-auth sur GitLab self-managed via le diff de notebooks Jupyter

Rapport CTI technique du 26 juillet 2026

Résumé exécutif

Le chercheur Yuhang Wu, de la société depthfirst, a publié le 24 juillet 2026 une preuve de concept fonctionnelle permettant d'exécuter des commandes en tant qu'utilisateur git sur un serveur GitLab self-managed non corrigé en version 18.11.3. L'attaque est déclenchée par un utilisateur authentifié ordinaire : il commite deux notebooks Jupyter forgés dans un projet et demande leur diff. La chaîne ne nécessite aucun droit administrateur, aucun accès aux runners CI, aucune interaction de la victime, ni aucun accès au projet d'un autre utilisateur. Les versions concernées couvrent Community Edition et Enterprise Edition de la 15.2.0 à la 18.10.7, de la 18.11.0 à la 18.11.4, et de la 19.0.0 à la 19.0.1 ; les premières versions corrigées sont 18.10.8, 18.11.5 et 19.0.2, publiées le 10 juin 2026.

Le point notable pour la priorisation défensive : ni la divulgation de depthfirst ni les notes de version GitLab du 10 juin ne mentionnent d'identifiant CVE ni de score CVSS pour les deux bugs de la chaîne. GitLab a classé la montée de version d'Oj vers 3.17.3 dans la section correctifs de bugs de ses notes de version, pas dans le tableau des correctifs de sécurité, et n'a jamais décrit publiquement le chaînage RCE via le diff de notebooks. Les environnements self-managed qui filtrent leurs mises à jour sur la base des seules annonces de sécurité GitLab ont donc de bonnes chances d'avoir laissé passer ce correctif sans le traiter en priorité, alors qu'un exploit fonctionnel circule désormais publiquement sur GitHub.

Chronologie

DateÉvénement
21 mai 2026depthfirst signale les deux bugs mémoire dans Oj au mainteneur du projet
27 mai 2026Le mainteneur d'Oj merge les correctifs
4 juin 2026Publication de la version Oj 3.17.3, contenant les deux correctifs
5 juin 2026depthfirst signale la chaîne d'exploitation complète à GitLab
8 juin 2026GitLab confirme la vulnérabilité selon depthfirst
10 juin 2026GitLab publie les versions corrigées 18.10.8, 18.11.5, 19.0.2 ; GitLab.com est patché le même jour
17 juillet 2026GitLab clôture le rapport de depthfirst
24 juillet 2026depthfirst publie l'analyse technique complète et le code de démonstration public sur GitHub
25 juillet 2026Couverture presse (The Hacker News) ; depthfirst indique n'avoir connaissance d'aucune exploitation in the wild à cette date

Fiche vulnérabilité

Sans CVE attribué (chaîne de deux bugs mémoire dans la gem Ruby Oj) | Score non coté | GitLab CE/EE 15.2.0-18.10.7, 18.11.0-18.11.4, 19.0.0-19.0.1 ; gem Oj 3.13.0-3.17.1

Le renderer de notebooks de GitLab transmet le JSON d'un fichier .ipynb, entièrement contrôlé par quiconque peut committer dans un dépôt, à Oj::Parser.usual.parse à l'intérieur d'un worker Puma de longue durée. Oj est un parseur JSON haute performance pour Ruby, dont une grande partie du code est en C natif. Deux défauts distincts s'enchaînent pour aboutir à l'exécution de code.

Premier bug : Oj stocke l'état d'imbrication dans une pile fixe de 1024 octets, sans jamais vérifier si la profondeur d'imbrication dépasse cette limite. Des tableaux profondément imbriqués écrivent alors des octets 0x01 dans l'état adjacent du parseur, corrompant le pointeur buf.head. Le parseur transmet ensuite ce pointeur interne forgé à realloc(). Une allocation Ruby Array intervenant juste après réclame la même région jemalloc de 3584 octets et écrase p->start.

Second bug : Oj alloue une clé d'objet de 65565 octets mais tronque sa longueur dans un champ signé de 16 bits, ce qui la ramène à 29. Le parseur retourne ces 29 octets, qui contiennent en réalité le pointeur d'allocation de la clé en cours de traitement. GitLab reporte ce pointeur dans le diff de notebook rendu à l'écran, ce qui constitue la fuite d'adresse nécessaire pour réduire l'espace de recherche ASLR.

Conditions d'exploitation : compte authentifié ordinaire, droit de committer dans un projet accessible, pas d'accès administrateur ni CI requis, pas d'interaction victime. Deux fichiers notebook classés dans un ordre lexical précis, envoyés dans une même requête diffs_stream, maintiennent les deux étapes dans le même worker Puma, qui réutilise l'état global du parseur Oj par processus. Le premier fichier corrompt le pointeur de callback et déclenche une erreur que GitLab intercepte avant de poursuivre le diff ; le second parse invoque le pointeur détourné et atteint system() via une séquence de gadgets spécifique au build. Sur l'installation GitLab 18.11.3 à deux workers utilisée pour le profilage, la recherche ASLR a généralement pris cinq à dix minutes ; les chercheurs projettent une à deux heures sur la plage la plus large de workers matures.

PoC disponible : oui, publié par depthfirst sur GitHub (wupco/gitlab-rce-demo), packagé dans un laboratoire GitLab 18.11.3 x86-64 local, spécifique à ce build précis. Les bugs Oj sous-jacents affectent des versions plus larges, mais le PoC public n'est pas portable tel quel à d'autres builds sans adaptation des gadgets.

Patch/mitigation : GitLab 18.10.8, 18.11.5, 19.0.2 ou Oj 3.17.3 côté gem. GitLab.com dédié n'a besoin d'aucune action côté client. Aucun contournement temporaire n'est documenté : GitLab et depthfirst orientent tous deux vers la mise à niveau directe. Les utilisateurs Helm et Operator doivent vérifier la version de GitLab à l'intérieur de l'image Webservice, pas seulement la version du chart ou de l'Operator.

Diagramme de la chaîne d'attaque

Chronologie de la découverte à la publication du PoC, de la remontée du bug Oj au 21 mai jusqu'à l'exploit public du 24 juillet
Chronologie reconstruite à partir de l'analyse technique depthfirst et de la couverture The Hacker News du 25 juillet 2026.

Analyse technique

Étape 1 : soumission de notebooks Jupyter forgés

L'attaquant, authentifié avec un droit de commit ordinaire sur un projet, committe deux fichiers .ipynb construits pour déclencher successivement les deux bugs Oj. GitLab rend automatiquement un diff lisible pour les fichiers notebook via une gem interne (ipynbdiff), qui s'appuie sur le parseur natif d'Oj pour valider que le JSON contient bien un champ cells.

Étape 2 : corruption mémoire et fuite d'adresse dans le worker Puma

Le premier notebook exploite le défaut de vérification de profondeur d'imbrication pour corrompre buf.head et forcer un realloc() sur un pointeur interne forgé. Une allocation Ruby Array qui suit immédiatement réclame la même région jemalloc et écrase p->start. Le second bug, la troncature de longueur de clé sur 16 bits signés, fait fuiter un pointeur d'allocation réel dans les 29 octets retournés comme clé d'objet, que GitLab affiche ensuite dans le diff rendu : c'est l'information nécessaire pour réduire la recherche ASLR à une fenêtre exploitable.

Étape 3 : demande du diff et exécution dans le même worker

L'attaquant place les deux notebooks dans un ordre lexical précis et déclenche une seule requête diffs_stream, garantissant que les deux étapes s'exécutent dans le même worker Puma, qui conserve un état de parseur Oj global au niveau du processus. Le premier fichier corrompt l'état ; GitLab intercepte l'erreur qui en résulte et poursuit le traitement du diff plutôt que de faire échouer la requête entièrement.

Étape 4 : détournement de flux et exécution de commande

Le second parse invoque le pointeur de callback détourné à l'étape précédente, atteignant system() via une chaîne de gadgets propre au build GitLab 18.11.3 x86-64. La commande s'exécute avec les privilèges du compte git, le compte système sous lequel tourne le worker applicatif GitLab.

Étape 5 : portée de l'impact post-exploitation

L'exécution obtenue tourne sous l'identité git, pas root. Sa portée réelle dépend de l'isolation du déploiement, mais peut inclure le code source des projets hébergés, les secrets Rails de l'application, les identifiants de services connectés (registre de conteneurs, intégrations tierces), les données CI/CD, et tout service interne atteignable depuis le processus applicatif GitLab.

Indicateurs de compromission

Aucun IOC réseau ou fichier n'est publié par depthfirst ou GitLab pour cette chaîne : l'exploitation repose sur des artefacts applicatifs (contenu de notebooks committés, requêtes diff), pas sur une infrastructure d'attaquant identifiable de façon générique. Les éléments observables côté défense sont les suivants.

TypeValeur
Artefact de dépôtCommits contenant des fichiers .ipynb de grande taille ou à structure JSON anormalement imbriquée
Comportement applicatifRequêtes diffs_stream répétées ou séquentielles portant sur des paires de notebooks dans un même projet, en dehors d'un usage normal de data science
Journal applicatifErreurs de parsing Oj ou crashs de worker Puma inhabituels suivis d'une requête de diff réussie sur les mêmes fichiers
Version exposéeToute instance GitLab self-managed en version 18.10.7 ou antérieure sur la branche 18.10, 18.11.4 ou antérieure sur la branche 18.11, ou 19.0.1 sur la branche 19.0

MITRE ATT&CK

TactiqueTechniqueIDJustification
Initial AccessExploit Public-Facing ApplicationT1190Exploitation d'une fonctionnalité applicative exposée (rendu de diff de notebook) accessible à tout utilisateur authentifié
ExecutionExploitation for Client ExecutionT1203Détournement du flux d'exécution du worker Puma via corruption mémoire dans le parseur Oj pour atteindre system()
Defense EvasionObfuscated Files or InformationT1027Structure JSON de notebook conçue pour ne pas éveiller de soupçon lors d'une revue de code standard tout en encodant le payload d'exploitation
Credential AccessUnsecured CredentialsT1552Accès potentiel aux secrets Rails et identifiants de services connectés une fois l'exécution obtenue sous l'identité git

Cette cartographie est une lecture technique construite à partir de l'analyse depthfirst, aucune source consultée ne propose de correspondance ATT&CK officielle pour cette chaîne spécifique.

Remédiation : checklist opérationnelle

  1. Identifier toutes les instances GitLab self-managed CE/EE en versions 15.2.0 à 18.10.7, 18.11.0 à 18.11.4, ou 19.0.0 à 19.0.1, y compris les déploiements Helm et Operator où la version réelle se trouve dans l'image Webservice et non dans la version du chart.
  2. Mettre à niveau en priorité vers 18.10.8, 18.11.5 ou 19.0.2 selon la branche suivie, sans attendre une fenêtre de maintenance standard compte tenu de la disponibilité d'un exploit public.
  3. À défaut de mise à niveau immédiate, restreindre les droits de commit aux seuls utilisateurs de confiance sur les projets exposés, en sachant qu'il ne s'agit pas d'une mitigation officiellement validée par GitLab.
  4. Auditer les journaux applicatifs Puma et les commits récents à la recherche de fichiers .ipynb suspects ou de crashs de parsing associés à des requêtes de diff, en particulier sur les projets accessibles à des comptes externes ou peu fiables.
  5. Vérifier la version d'Oj utilisée par toute application Ruby interne consommant du JSON non fiable au-delà de GitLab, la gem étant largement réutilisée dans l'écosystème Ruby et les deux bugs affectant les versions 3.13.0 à 3.17.1 indépendamment du contexte GitLab.
  6. Documenter l'absence de CVE officiel dans les registres internes de gestion de vulnérabilités, en s'appuyant sur l'analyse technique publique plutôt que sur un score CVSS pour justifier la priorité de traitement auprès des équipes non spécialisées.

Sources

Tags : GitLabOjRubyJupyter notebookRCEmemory corruptiondepthfirstASLR bypass
Partager cet article