Cybersecurity
  • Mon Blog
    • 🏠Home
    • 📦Archives
      • 2024
      • 2023
      • 2022
    • 📂Catégories
    • 📧A propos de moi
  • Mes projets
    • Livres / publications
      • Sécurité des applications web - Stratégies offensives et défensives
    • MyExpense
    • XSS Exploitation Tool
  • Mes Articles
    • 2025
      • Mars
        • Comment les requêtes préparées (prepared statement) protègent-elles contre les injections SQL ?
      • Janvier
        • XSS Exploitation Tool v0.7.0
    • 2024
      • Décembre
        • XSS Exploitation Tool v0.6.0
      • Septembre
        • MyExpense v1.4
      • Aout
        • XSS Exploitation Tool v0.5.0
        • Exploitation des injections SQL au sein de la clause ORDER BY
      • Juin
        • Parution de mon livre, Sécurité des applications web - Stratégies offensives et défensives
      • Mai
        • Dompurify 3.0.10 bypass - Confusion nodeName and CDATA
        • Dompurify 3.0.9 bypass - Node type confusion
      • Avril
        • Bypass de validation d'URL et embedded credentials côté front
      • Mars
        • MyExpense v1.3
    • 2023
      • Mai
        • MyExpense v1.2
      • Mars
        • MyExpense v1.1
        • Fonctionnement de l'entête X-Content-Type-Options - Contournement de CSP
      • Février
        • Fonctionnement de l'entête HTTP Strict Transport Security Header (HSTS)
    • 2022
      • Décembre
        • Les injections CSS - Règle @import
        • Les injections CSS - Scroll-to-Text Fragment
      • Novembre
        • Les injections CSS - Attribute Selector
        • Les injections CSS - Règle @font-face et descripteur unicode
      • Octobre
        • XSS Exploitation Tool v0.4.0
      • Septembre
        • Cross-Site Scripting (XSS) et schéma d'URI javascript
      • Juillet
        • SAST - PHP CodeSniffer orienté sécurité dans Visual Studio (sous Windows)
        • SAST - PHP CodeSniffer orienté sécurité dans Visual Studio (sous Debian)
        • Est-il possible de contourner la fonction PHP htmlspecialchars() ?
  • Common Vulnerabilities and Exposures (CVE)
    • 2024
      • CVE-2024-29415
    • 2023
      • CVE-2023-42282
    • 2022
      • CVE-2022-33910
      • CVE-2022-32444
      • CVE-2022-32442
    • 2020
      • CVE-2020-26311
  • Livres
    • 2023
      • Attacking and Exploiting Modern Web Applications
      • DevSecOps - Développez et administrez vos services en toute sécurité
    • 2022
      • Hacking APIs - Breaking Web Application Programming Interfaces
    • 2018
      • Practical Web Penetration Testing
      • Web Hacking 101: How to Make Money Hacking Ethically
  • Walkthroughs
    • Capture The Flag
      • Hack.lu CTF 2019
        • Nucular Power Plant
      • TAMUctf 2019
        • 1337 Secur1ty
        • Bird Box Challenge
        • Science!
    • Deliberately Vulnerable
      • CORS vulnerable Lab
        • Application Trust Arbritrary Origin
        • Application has bad "regex" Implementation to check Trusted Origin
        • Application Trust "null" Origin
      • Damn Vulnerable Web Application (DVWA)
        • Brute Force
          • Niveau "Low"
          • Niveau "Medium"
          • Niveau "High"
        • Command Injection
          • Niveau "Low"
          • Niveau "Medium"
          • Niveau "High"
        • CSRF
          • Niveau "Low"
          • Niveau "Medium"
          • Niveau "High"
        • File Inclusion
          • Niveau "Low"
          • Niveau "Medium"
          • Niveau "High"
        • File Upload
          • Niveau "Low"
          • Niveau "Medium"
          • Niveau "High"
        • Insecure CAPTCHA
          • Niveau "Low"
          • Niveau "Medium"
          • Niveau "High"
        • SQL Injection
          • Niveau "Low"
          • Niveau "Medium"
          • Niveau "High"
        • SQL Injection (Blind)
          • Niveau "Low"
          • Niveau "Medium"
          • Niveau "High"
        • Weak Session IDs
          • Niveau "Low"
          • Niveau "Medium"
          • Niveau "High"
        • XSS (DOM)
          • Niveau "Low"
          • Niveau "Medium"
          • Niveau "High"
        • XSS (Reflected)
          • Niveau "Low"
          • Niveau "Medium"
          • Niveau "High"
        • XSS (Stored)
          • Niveau "Low"
          • Niveau "Medium"
          • Niveau "High"
        • CSP Bypass
          • Niveau "Low"
          • Niveau "Medium"
          • Niveau "High"
        • Javascript
          • Niveau "Low"
          • Niveau "Medium"
          • Niveau "High"
      • Unescape() room
        • Level 1 (practice)
        • Level 2 (practice)
        • Level 3 (practice)
        • Level 4 (practice)
        • Level 5 (practice)
        • Level 6 (practice)
        • Level 7 (practice)
        • Level 8 (practice)
        • Level 9 (practice)
        • Level 10 (practice)
      • VulnHub
        • GoatseLinux: 1
        • Hackademic: RTB1
        • Hackademic: RTB2
        • Holynix: v1
        • Holynix: v2
        • Kioptrix: Level 1 (#1)
        • Kioptrix: Level 1.1 (#2)
        • Kioptrix: Level 1.2 (#3)
        • Kioptrix: Level 1.3 (#4)
        • LAMPSecurity: CTF4
        • LAMPSecurity: CTF5
        • LAMPSecurity: CTF6
        • Metasploitable: 1
        • pWnOS 1.0
        • pWnOS 2.0 (Pre-Release)
      • XSS Vulnerability Challenges
        • xss1
        • xss2
        • xss3
        • xss4
        • xss5
        • xxs6
        • xss7
        • xss8
Propulsé par GitBook
Sur cette page
  • Description
  • Préambule
  • Analyse de la vulnérabilité
  • Code vulnérable
  • Correction
  • Contournement du correctif
  • Labs
  • Conclusion
  • Ressources
  1. Common Vulnerabilities and Exposures (CVE)
  2. 2023

CVE-2023-42282

11 Février 2025

Précédent2023Suivant2022

Dernière mise à jour il y a 2 mois

Analyse de la faiblesse CVE-2023-42282. Cette vulnérabilité de type Server-Side Request Forgery (SSRF) affecte la dépendance ip, ce qui a conduit à son archivage temporaire.

Description

  • Vendeur : N/A

  • Produit : ip ()

  • Version(s) impactée(s) : < 1.1.9

  • Type de vulnérabilité : Server-Side Request Forgery (SSRF) ()

Les versions < 1.1.9 du paquet IP sont vulnérables à une faille de type Server-Side Request Forgery (SSRF) car certaines adresses IP privées sont considérés comme publiques lors de l'appel à la méthode isPublic().

Préambule

Le paquet IP, développé et maintenu par Fedor Indutny, permet de manipuler les adresses IP, notamment en les comparant, en convertissant leur format et en déterminant si une adresse est privée ou publiquement routable, ainsi que d'autres fonctionnalités utiles. Cette dépendance est largement utilisée, avec environ 15 millions de téléchargement par semaine.

En février 2024, la CVE-2023-42282 est enregistrée au sein de la base de données du NVD possédant un score CVSS critique de 9.8. Suite à cela, la vulnérabilité est également enregistrée dans le GHSA (le système d'avis de sécurité de GitHub).

Suite à cette remontée, le mainteneur publie un correctif dans la version 1.1.9 de la dépendance. Cependant, les retours se multiplient, alimentés par l'alerte de sécurité toujours détectée par l'outil npm audit. Suite à cela, Fedor Indutny décide d'archiver son projet et de communiquer sur les réseaux sociaux. Bien qu'il reconnaisse la présence de la vulnérabilité, il estime que sa gravité est exagérée.

A l'heure actuelle, GitHub a décidé de revoir la sévérité de la vulnérabilité, la rétrogradant de High à Low, bien qu'elle soit toujours considéeée comme Critical par le NVD. Depuis, le développeur a décidé de désarchiver son projet.

Analyse de la vulnérabilité

La fonction isPublic() permet donc d'identifier si une adresse IP donnée est routable ou privée. Les adresses privées appartenant aux classes A, B et C sont les suivantes :

  • Classe A : de 10.0.0.0 à 10.255.255.255 (sans oublier la plage 127.0.0.0/8 pour le loopback)

  • Classe B: de 172.16.0.0 à 172.31.255.255

  • Classe C : de 192.168.0.0 à 192.168.255.255

Par exemple, l'adresse IP 10.0.0.1 est considérée comme privée, 127.0.0.1 (souvent désignée par "localhost") appartient à la plage de loopback (elle sera considérée comme privée dans la suite de cet article), tandis que l'adresse IP 8.8.8.8, utilisée pour le DNS de Google, est publique.

Le code suivant permet de tester spécifiquement le comportement de la fonction lors du traitement des adresses IP fournies dans le tableau :

ip = require("ip-1.1.8");
const ips_addresses = [];

ips_addresses.forEach(ip_address => {
    // Nécessaire pour supprimer les "[" et "]" qui entourent les IPv6
    if (ip_address.startsWith("[") && ip_address.endsWith("]")) {
        ip_address = ip_address.slice(1, -1);
    }

    if (ip.isPublic(ip_address)) {
        console.log(`${ip_address} is public`);
    } else {
        console.log(`${ip_address} is private`);
    }
});

Une première tentative simple peut être la suivante :

const ips_addresses = ['8.8.8.8', '127.0.0.1', '10.0.0.1'];

Tout se déroule correctement :

8.8.8.8 is public
127.0.0.1 is private
10.0.0.1 is private

Il existe plusieurs façons de représenter la même adresse IP, notamment à travers la conversion entre IPv4 et IPv6, différents types de notations ou d'encodages, des versions raccourcies, etc. Malheureusement, la fonction isPublic() classe incorrectement certaines adresses IP privées, encodées de manière spécifique, comme étant publiques.

Un exemple avec les addresses IP privées suivantes :

const ips_addresses = [
    'localhost', '127.0.0.1', '0.0.0.0', '[::]', '[0000::1]',
    '[0:0:0:0:0:ffff:127.0.0.1]', '[::ffff:127.0.0.1]', '[::fFFf:127.0.0.1]',
    '127.127.127.127', '127.0.1.3', '127.0.0.0', '127.0.0.2', '127.1.1.1',
    '0', '0x7f.1', '127.1', '127.0.1', '[000:0:0000::01]', '0177.0.0.1',
    '0x7F.0x00.0x00.0x01', '127.0.0.1/32', '2130706433',
    '017700000001', '0177.0.0.1', 'o177.0.0.1', '0o177.0.0.1', 'q177.0.0.1'
];
localhost is public
127.0.0.1 is private
0.0.0.0 is public
:: is private
0000::1 is public
0:0:0:0:0:ffff:127.0.0.1 is public
::ffff:127.0.0.1 is private
::fFFf:127.0.0.1 is private
127.127.127.127 is private
127.0.1.3 is private
127.0.0.0 is private
127.0.0.2 is private
127.1.1.1 is private
0 is public
0x7f.1 is public
127.1 is public
127.0.1 is public
000:0:0000::01 is public
0177.0.0.1 is public
0x7F.0x00.0x00.0x01 is public
127.0.0.1/32 is public
2130706433 is public
017700000001 is public
0177.0.0.1 is public
o177.0.0.1 is public
0o177.0.0.1 is public
q177.0.0.1 is public

L'adresse IP 2130706433 correspond à l'équivalent numérique de l'adresse IPv4 127.0.0.1, mais elle est considérée comme publique par la fonction. De même, le raccourci 0x7f.1 est également interprété comme une adresse publique, alors qu'il représente également 127.0.0.1.

Après avoir effectué quelques tests sur cette liste, il s'avère que les adresses o177.0.0.1, 0o177.0.0.1 et 177.0.0.1, qui sont en octal, sont également considérées comme routables par la fonction. Cependant, elles risquent de ne pas fonctionner correctement lorsqu'elles seront interprétées par certains outils, tels que cURL, Axios, Wget, ou même un navigateur.

Quel est donc le risque finalement ?

Cela pourrait poser un problème si une application dépend de la fonction isPublic() pour autoriser ou interdire l'accès à une ressource, par exemple, hébergée sur le réseau interne (ou localhost) du système, car cela constituerait une possible exploitation d'une vulnérabilité de type SSRF.

Afin de contre balancer cette hypothèse, il est intéressant de noter que la validation de ces adresses permettent parfois de retrouver un format plus générique :

let parsedUrl = new URL("http://0x7f.1/");
let hostname = parsedUrl.hostname;
console.log(hostname); // Affichera 127.0.0.1

Code vulnérable

La fonction isPublic(), présente dans le fichier ip.js de la dépendance en version 1.1.8, est la suivante :

ip.isPublic = function (addr) {
  return !ip.isPrivate(addr);
};

La vulnérabilité réside donc plus précisément dans la fonction isPrivate(), qui repose sur une série d'expressions régulières :

ip.isPrivate = function (addr) {
  return /^(::f{4}:)?10\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/i
    .test(addr)
    || /^(::f{4}:)?192\.168\.([0-9]{1,3})\.([0-9]{1,3})$/i.test(addr)
    || /^(::f{4}:)?172\.(1[6-9]|2\d|30|31)\.([0-9]{1,3})\.([0-9]{1,3})$/i
      .test(addr)
    || /^(::f{4}:)?127\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/i.test(addr)
    || /^(::f{4}:)?169\.254\.([0-9]{1,3})\.([0-9]{1,3})$/i.test(addr)
    || /^f[cd][0-9a-f]{2}:/i.test(addr)
    || /^fe80:/i.test(addr)
    || /^::1$/.test(addr)
    || /^::$/.test(addr);
};

Si une adresse IP ne correspond à aucune des expressions régulières, elle est considérée comme publique. C'est le cas du nom d'hôte localhost ainsi que des différentes variantes d'encodages d'adresses IP privées. Cela met en évidence la difficulté de couvrir tous les cas possibles à l'aide d'expressions régulières.

L'analyse du code vulnérable indique également qu'il est essentiel pour le développeur utilisant cette fonction de valider au préalable que l'adresse IP fournie a un format correct, par exemple en utilisant la librairie net et la fonction isIP de Node.js.

Correction

Un correctif a été apporté dans la version 1.1.9, publiée le 19 février 2024. La nouvelle version de la fonction isPrivate() est la suivante :

ip.isPrivate = function (addr) {
  // check loopback addresses first
  if (ip.isLoopback(addr)) {
    return true;
  }

  // ensure the ipv4 address is valid
  if (!ip.isV6Format(addr)) {
    const ipl = ip.normalizeToLong(addr);
    if (ipl < 0) {
      throw new Error('invalid ipv4 address');
    }
    // normalize the address for the private range checks that follow
    addr = ip.fromLong(ipl);
  }

  // check private ranges
  return /^(::f{4}:)?10\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/i.test(addr)
    || /^(::f{4}:)?192\.168\.([0-9]{1,3})\.([0-9]{1,3})$/i.test(addr)
    || /^(::f{4}:)?172\.(1[6-9]|2\d|30|31)\.([0-9]{1,3})\.([0-9]{1,3})$/i
      .test(addr)
    || /^(::f{4}:)?169\.254\.([0-9]{1,3})\.([0-9]{1,3})$/i.test(addr)
    || /^f[cd][0-9a-f]{2}:/i.test(addr)
    || /^fe80:/i.test(addr)
    || /^::1$/.test(addr)
    || /^::$/.test(addr);
};

Tout d'abord, la liste des expressions régulières se voit amputer de celle-ci (qui permettait d'identifier les adresses IP commençant par 127 ou ::ffff:) :

/^(::f{4}:)?127\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/i

Vient ensuite l'ajout de l'appel à la fonction isLoopback(), qui utilise un ensemble d'expressions régulières dont celle précédemment supprimée :

ip.isLoopback = function (addr) {
  // If addr is an IPv4 address in long integer form (no dots and no colons), convert it
  if (!/\./.test(addr) && !/:/.test(addr)) {
    addr = ip.fromLong(Number(addr));
  }

  return /^(::f{4}:)?127\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})/
    .test(addr)
    || /^0177\./.test(addr)
    || /^0x7f\./i.test(addr)
    || /^fe80::1$/i.test(addr)
    || /^::1$/.test(addr)
    || /^::$/.test(addr);
};

Cette fonction existait déjà dans la version 1.1.8 mais a été enrichie par la version 1.1.9.

La conversion du format décimal permet de convertir les adresses IP, comme 2130706433, en leur équivalent au format IPv4 :

ip.fromLong = function (ipl) {
  return (`${ipl >>> 24}.${
    ipl >> 16 & 255}.${
    ipl >> 8 & 255}.${
    ipl & 255}`);
};
let addr = "2130706433";

if (!/\./.test(addr) && !/:/.test(addr)) {
    addr = ip.fromLong(Number(addr));
    console.log(addr) // Affichera 127.0.0.1
}

Cependant, elle ne transforme pas correctement les adresses au format octal :

let addr = "017700000001";

if (!/\./.test(addr) && !/:/.test(addr)) {
    addr = ip.fromLong(Number(addr));
    console.log(addr) // Affichera 31.0.145.1
}

Les tests des expressions régulières effectués ensuite par la fonction ne changera rien, et l'adresse IP au format octal sera finalement considérée comme n'appartenant pas au loopback/localhost.

L'appel à la fonction isV6Format() permet de s'assurer qu'une adresse IPv4 (hors localhost, car déjà traité par la partie précédepent) possède un format valide. Pour cela, elle ne doit pas correspondre à l'expression régulière suivante, qui représente une adresse IPv6 :

var ipv6Regex = /^(::)?(((\d{1,3}\.){3}(\d{1,3}){1})?([0-9a-f]){0,4}:{0,2}){1,8}(::)?$/i;

La fonction normalizeToLong() prend en entrée une adresse IPv4 et tente de la convertir en un entier normalisé en tenant compte de son format (octal, hexadécimal, décimal). our cela, elle divise la chaîne à chaque occurrence du caractère " . " :

ip.normalizeToLong = function (addr) {
  const parts = addr.split('.').map(part => {
    // Handle hexadecimal format
    if (part.startsWith('0x') || part.startsWith('0X')) {
      return parseInt(part, 16);
    }
    // Handle octal format (strictly digits 0-7 after a leading zero)
    else if (part.startsWith('0') && part !== '0' && /^[0-7]+$/.test(part)) {
      return parseInt(part, 8);
    }
    // Handle decimal format, reject invalid leading zeros
    else if (/^[1-9]\d*$/.test(part) || part === '0') {
      return parseInt(part, 10);
    }
    // Return NaN for invalid formats to indicate parsing failure
    else {
      return NaN;
    }
  });

Par exemple, l'adresse représentée en hexadécimal sous la forme 0x0A.0x00.0x00.0x01 (correspondand à 10.0.0.1), sera convertie en sa valeur entière : 167772161.

Le dernier appel effectué est l'appel à la fonction fromLong() qui extrait les quatre octets de la valeur entière précédemment convertit :

ip.fromLong = function (ipl) {
  return (`${ipl >>> 24}.${
    ipl >> 16 & 255}.${
    ipl >> 8 & 255}.${
    ipl & 255}`);
};

Résultant finalement en 10.0.0.1.

Contournement du correctif

Malheureusement, ce correctif reste insuffisant, et certaines adresses IP privées, notamment celles représentant la boucle locale, sont toujours considérées comme routables, ce qui les rend exploitables dans le cadre d'attaques SSRF.

Pour le vérifier, il suffit de reprendre la liste des adresses IP testées sur la version 1.1.8 du paquet et de les tester à nouveau avec le correctif de la version 1.1.9 :

const ips_addresses = [
    'localhost', '127.0.0.1', '0.0.0.0', '[::]', '[0000::1]',
    '[0:0:0:0:0:ffff:127.0.0.1]', '[::ffff:127.0.0.1]', '[::fFFf:127.0.0.1]',
    '127.127.127.127', '127.0.1.3', '127.0.0.0', '127.0.0.2', '127.1.1.1',
    '0', '0x7f.1', '127.1', '127.0.1', '[000:0:0000::01]', '0177.0.0.1',
    '0x7F.0x00.0x00.0x01', '127.0.0.1/32', '2130706433',
    '017700000001', '0177.0.0.1', 'o177.0.0.1', '0o177.0.0.1', 'q177.0.0.1'
];

Certaines adresses ne sont désormais plus reconnues comme valides :

localhost - Error: Error: invalid ipv4 address
o177.0.0.1 - Error: Error: invalid ipv4 address
0o177.0.0.1 - Error: Error: invalid ipv4 address
q177.0.0.1 - Error: Error: invalid ipv4 address

D'autres sont désormais correctement identifiées comme privées :

0x7f.1 is private
0177.0.0.1 is private
0x7F.0x00.0x00.0x01 is private
127.0.0.1/32 is private
2130706433 is private
0177.0.0.1 is private

Certaines continuent d'être incorrectement classées :

0.0.0.0 is public
0000::1 is public
0:0:0:0:0:ffff:127.0.0.1 is public
0 is public
127.1 is public
127.0.1 is public
000:0:0000::01 is public
017700000001 is public

Et une régression est même apparue :

::fFFf:127.0.0.1 is public

Labs

Conclusion

Cette vulnérabilité présente un intérêt technique intéressant, tant du côté de l'attaque, où différentes méthodes peuvent conduire aux mêmes résultats, que du côté de la défense, où la complexité de prendre en compte tous les scénarios possibles lors du traitement de données non fiables devient manifeste.

Toutefois, ce qui s'avère encore plus fascinant, c'est la réflexion qu'elle suscite sur le principe même de l'open-source. Une telle dépendance n'a peut-être pas été conçue à l'origine pour être utilisée à des fins de sécurité par son auteur, mais rien n'empêche un utilisateur de l'exploiter ainsi. Et comment le savoir, dans ce cas ? Dès lors, se pose la question de savoir si ces failles de sécurité sont véritablement de la seule responsabilité du mainteneur. Dans le cadre des projets open-source ou des projets soutenus par une entreprise, cette responsabilité semble plus claire. Mais qu'en est-il lorsque le développeur œuvre seul, sur son temps libre ? Est-il, dans ce cas, responsable des vulnérabilités remontées et doit-il s'engager à les corriger dans un délai précis ? Doit-il répondre à toutes les sollicitations, fournir des informations détaillées sur la vulnérabilité et offrir des correctifs, comme cela a été le cas pour Fedor Indutny ? Ces questions invitent à une réflexion plus profonde sur la responsabilité dans le monde de l'open-source.

Ressources

Cette liste a été élaborée en se basant sur .

Une image Docker illustrant une vulnérabilité SSRF, où le contrôle d'accès est effectué à l'aide de la dépendance ip, est disponible .

https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/Server%20Side%20Request%20Forgery/README.md
ici
https://github.com/indutny/node-ip/
https://programmation.developpez.com/actu/359875/Un-developpeur-contraint-d-archiver-le-depot-GitHub-de-son-projet-apres-une-avalanche-de-rapports-douteux-sur-une-vulnerabilite-il-denonce-une-tendance-qui-peut-nuire-a-la-reputation-des-projets-open-source/
https://github.com/github/advisory-database/blob/1116380c2d45a6aa5aef36845fde10087ee70ac0/advisories/github-reviewed/2024/02/GHSA-78xj-cgh5-2h22/GHSA-78xj-cgh5-2h22.json
https://github.com/github/advisory-database/blob/49fc787f17bfab369776763fa0407372620b12b0/advisories/github-reviewed/2024/02/GHSA-78xj-cgh5-2h22/GHSA-78xj-cgh5-2h22.json
https://nvd.nist.gov/vuln/detail/CVE-2024-29415
https://github.com/advisories/GHSA-78xj-cgh5-2h22
https://security.snyk.io/vuln/SNYK-JS-IP-6240864
https://github.com/indutny/node-ip/
CWE-918 - Server-Side Request Forgery (SSRF)