Category: code

Aktive Verteidigung gegen Krypto-Trojaner

29.02.2016 yahe code legacy security thoughts

Mit Locky sieht die IT-Welt derzeit einen der koordiniertesten Trojanerangriff der jüngeren Zeit. Während er sich Anfangs als Schläfer getarnt haben soll, findet er inzwischen auf unterschiedlichsten Wegen sein Ziel: als Word- oder Excel-Dokument mit schädlichem Makro; als JavaScript-Datei, die sich z.B. als angebliches Sipgate-Fax tarnt; oder gar als altmodische und als ausgestorben geglaubte Batch-Datei. Bei den Opfern des Verschlüsselungstrojaners handelt es sich auch um größere Organisationen wie Krankenhäuser und ein Fraunhofer-Institut. Man ging zeitweise von 5000 Neuinfektionen pro Stunde aus.

Die Vorschläge für Gegenmaßnahmen sehen derzeit noch eher mager aus. Die einen raten zur Symptombekämpfung durch Backups, deaktivierte Makros und Virenscannern. Andere wiederum versuchen, den Krypto-Trojaner anhand seiner Funktionsweise zu erkennen und so eine Anti-Malware-Software auf Heuristikbasis zu entwickeln.

Um sich erfolgreich gegen solch einen Trojanerangriff zur Wehr zu setzen, muss man jedoch an mehreren Baustellen aktiv werden und anstatt unterschiedlichste Teilmaßnahmen einzuführen, sollte man sich überlegen, welche Kombination von Maßnahmen zu einem ganzheitlichen Schutz führen kann. Die folgende Auflistung soll dabei eine kleine Hilfestellung für einen aktiven Umgang mit solch einem Infektionsrisiko bieten. Das folgende Dreiergespann aus Vermeidung, Erkennung und Behebung ist auch in der Medizin durchaus weit verbreitet.

Zuerst einmal sollte man natürlich das Thema Vermeidung betrachten. Hierzu zählen Dinge wie das Deaktivieren der Makrofunktion in Office-Anwendungen und die Aufklärung von Mailnutzern darüber, dass Office-Anhänge von außen nicht zu öffnen und ZIP-Dateien von außen nicht zu entpacken sind. Auch das Deaktivieren von JavaScript auf exponierten Arbeitsplätzen oder gar der Wechsel auf ein Betriebssystem mit konsequenter Trennung von Ausführungsrechten und Dateinamen kann eine Option darstellen.

Anschließend sollte man sich darum kümmern, eine Erkennung eines erfolgreichen Angriffs zu ermöglichen. Erst, wenn eine Infektion erkannt werden kann, kann sie im Anschluss auch behoben werden. Leider ist es noch häufig so, dass eine Erkennung lediglich durch Virenscanner erfolgt. Diese sind jedoch so spezifisch an einzelne Schädlinge und deren Verhalten angepasst, dass neue Generationen häufig für längere Zeit unentdeckt bleiben. Anstatt Verhaltensmuster der Schädlinge zu untersuchen, ist es daher sinnvoller, Standardverhaltensmuster seiner Mitarbeiter zu aggregieren und Abweichungen von diesem Standardverhalten in einem Sicherheitsmonitoring zu sammeln. So ist es möglich, auch neue Schädlinge anhand eines vom Durchschnitt abweichenden Verhaltens zu erkennen. Beispielsweise könnte zum Erkennen eines Krypto-Trojaners die durchschnittliche Anzahl an neu erstellten, bearbeiteten und gelöschten Dateien pro Tag herhalten. Während ein typischer Büroarbeiter eher selten viele Dateien gleichzeitig anlegt und gleichzeitig viele Dateien löscht, ist genau das das Hauptgeschäft von Krypto-Trojanern. Solch eine Erkennung von Abweichungen kann mit Canaries ergänzt werden. Dabei handelt es sich um Dateien, die extra als Opfer für einen Verschlüsselungstrojaner bereitgestellt werden und deren Bearbeitung als ein eindeutiges Indiz für das Vorhandensein eines Krypto-Trojaners dienen kann. Das Ziel der Erkennung ist es, von einem Problem zu erfahren, noch bevor der Nutzer etwa Ungewöhnliches feststellt.

Abschließend muss das Thema der Behebung betrachtet werden. Hierzu zählen an erster Stelle regelmäßige Backups. Das häufig kommunizierte Mantra, Dateien auf einem externen Datenträger zu sichern und diesen anschließend wieder vom Computer zu trennen, stellt in den meisten Umgebungen keinen ausreichenden Schutz dar. Denn während des Backups kann auch der externe Datenträger befallen und verschlüsselt werden. Der eigentliche Wunsch hinter solch einer Aussage ist vielmehr, eine Sicherung zu erstellen, die von einer eventuellen Infektion nicht beeinflusst werden kann. Sinnvolle Varianten können hierbei extern durchgeführte Backups sein, die nicht vom infizierten Nutzer angestoßen und demnach auch nicht vom infizierten Nutzer modifiziert werden können. Auch lokale Backups unter der Hoheit eines anderen Systemnutzers stellen eine Möglichkeit dar. Noch sinnvoller als ein einfaches Backup ist eine Versionierung, die zwar mehr Speicherplatz benötigt, dafür aber auch eine selektive Wiederherstellung von Dateiinhalten unterschiedlichster Zeitpunkte ermöglicht.

Die Implementierung eines entsprechenden Schutzes ist auch mit einfachen Tools möglich, wobei speziell entwickelte Werkzeuge wesentlich mehr Komfort bieten können. Im Folgenden soll eine Erkennung und Behandlung eines Krypto-Trojaner-Angriffs mit Hilfe von Mercurial dargestellt werden. Anbei folgt ein beispielhaftes Skript zur Erkennung von Abweichungen in der Bearbeitung von Dateien. Das Script ermittelt Abweichungen von der durchschnittlichen Menge an erstellten/gelöschten/bearbeiteten Dateien. Zudem prüft es separat die Veränderung eines Canaries. Das Script ist nur als Proof-of-Concept eines dateiorientierten, Host-basierten, sowie Anomalie-basierten Intrusion Detection Systems zu verstehen.

#!/usr/bin/php
<?php
  // user-defined values
  define("CHECKFOLDER", "/testpit/");
  define("CHECKCANARY", CHECKFOLDER . "data/do-not-edit.txt");
  define("STATUSFILE",  CHECKFOLDER . "status/status");

  // deviation of user behaviour from previously collected behaviour
  define("DEVIATIONVALUE", 0.5); // default deviation of 50% is allowed

  // generated values
  define("CHECKDATE", date("Ymd-His"));

  // concatenated actions
  define("ADDREMOVECMD", "hg addremove -R \"" . CHECKFOLDER . "\" -X \"" . CHECKCANARY . "\"");
  define("COMMITCMD",    "hg commit -R \"" . CHECKFOLDER . "\" -X \"" . CHECKCANARY . "\" -m \"" . CHECKDATE . "\"");
  define("STATUSCMD",    "hg status -R \"" . CHECKFOLDER . "\"");

  // static definitions
  define("ADDEDHINT",     "A");
  define("MISSINGHINT",   "!");
  define("MODIFIEDHINT",  "M");
  define("NOTTRACKEDHINT","?");
  define("REMOVEDHINT",   "R");

  define("HINTDELIMITER",       " ");
  define("STATISTICSDELIMITER", ":");

  function check_filename($line, $filename) {
    $result = false;

    $parts = explode(HINTDELIMITER, $line, 2);
    if ((false !== $parts) && (2 === count($parts))) {
      $result = (0 == strcasecmp(CHECKFOLDER . $parts[1], $filename));
    }

    return $result;
  }

  function check_hint($line, $hint) {
    $result = false;

    $parts = explode(HINTDELIMITER, $line, 2);
    if ((false !== $parts) && (2 === count($parts))) {
      $result = (0 == strcasecmp($parts[0], $hint));
    }

    return $result;
  }

  function get_statistics_text($additions, $deletions, $modifications) {
    return (CHECKDATE . STATISTICSDELIMITER . ADDEDHINT . STATISTICSDELIMITER . $additions .
                        STATISTICSDELIMITER . MODIFIEDHINT . STATISTICSDELIMITER . $modifications .
                        STATISTICSDELIMITER . REMOVEDHINT . STATISTICSDELIMITER . $deletions);
  }

  function check_statistics($additions, $deletions, $modifications) {
    $result = true;

    // with any modification there's nothing to check
    if (0 < ($additions + $deletions + $modifications)) {
      // read statistics and execute statistics checkvg_additions_count     = 0;
      $avg_additions_count     = 0;
      $avg_additions_value     = 0;
      $avg_deletions_count     = 0;
      $avg_deletions_value     = 0;
      $avg_modifications_count = 0;
      $avg_modifications_value = 0;

      if (is_file(STATUSFILE)) {
        $statistics = file(STATUSFILE, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
        if ((false !== $statistics) && (0 < count($statistics))) {
          // calculate average additions, deletions and modifications from statistics data
          foreach ($statistics as $line) {
            $parts = explode(STATISTICSDELIMITER, $line, 7);
            if ((false !== $parts) && (7 === count($parts))) {
              // check if value is integer and bigger than 0
              if (is_numeric($parts[2]) && (0 < $parts[2])) {
                $avg_additions_value += $parts[2];
                $avg_additions_count++;
              }
              if (is_numeric($parts[6]) && (0 < $parts[6])) {
                $avg_deletions_value += $parts[6];
                $avg_deletions_count++;
              }
              if (is_numeric($parts[4]) && (0 < $parts[4])) {
                $avg_modifications_value += $parts[4];
                $avg_modifications_count++;
              }
            }
          }
        }
      }

      // there's nothing wrong when nothing happened
      if (0 < $additions) {
        // when actions has never been seen then that's a deviation
        if (0 === $avg_additions_count) {
          $result = false;
        } else {
          // more additions than expected
          if (((1.0 + DEVIATIONVALUE) * ($avg_additions_value / $avg_additions_count)) < $additions) {
            $result = false;
          }
        }
      }

      // there's nothing wrong when nothing happened
      if (0 < $deletions) {
        // when actions has never been seen then that's a deviation
        if (0 === $avg_deletions_count) {
          $result = false;
        } else {
          // more deletions than expected
          if (((1 + DEVIATIONVALUE) * ($avg_deletions_value / $avg_deletions_count)) < $deletions) {
            $result = false;
          }
        }
      }

      // there's nothing wrong when nothing happened
      if (0 < $modifications) {
        // when actions has never been seen then that's a deviation
        if (0 === $avg_modifications_count) {
          $result = false;
        } else {
          // more modifications than expected
          if (((1 + DEVIATIONVALUE) * ($avg_modifications_value / $avg_modifications_count)) < $modifications) {
            $result = false;
          }
        }
      }
    }

    // add new result to statistics
    file_put_contents(STATUSFILE,
                      get_statistics_text($additions, $deletions, $modifications) . "\n",
                      FILE_APPEND | LOCK_EX);

    return $result;
  }

  function main() {
    $additions     = 0;
    $deletions     = 0;
    $modifications = 0;

    $canary_found = false;

    // retrieve information about file changes
    exec(STATUSCMD, $output);

    // accept file changes right away
    exec(ADDREMOVECMD);
    exec(COMMITCMD);

    // iterate through file changes
    foreach ($output as $line) {
      // check if the canary file is part of the modifications
      if (check_filename($line, CHECKCANARY)) {
        $canary_found = true;
      } else {
        // check if a file has been added
        if (check_hint($line, ADDEDHINT) || check_hint($line, NOTTRACKEDHINT)) {
          $additions++;
        } else {
          // check if a file has been deleted
          if (check_hint($line, MISSINGHINT) || check_hint($line, REMOVEDHINT)) {
            $deletions++;
          } else {
            // check if a file has been modified
            if (check_hint($line, MODIFIEDHINT)) {
              $modifications++;
            }
          }
        }
      }
    }

    if (0 < ($additions + $deletions + $modifications)) {
      // accept file changes
      exec(ADDREMOVECMD);
      exec(COMMITCMD);
    }

    // print result
    print(get_statistics_text($additions, $deletions, $modifications) . "\n");

    // the canary has been modified
    if ($canary_found) {
      //!!! do something
      print("ALERT! CANARY HAS BEEN MODIFIED!\n");
    }

    // check if the modifications do not match the statistics
    if (!check_statistics($additions, $deletions, $modifications)) {
      //!!! do something
      print("ALERT! BEHAVIOUR DOES NOT MATCH STATISTICS!\n");
    }
  }

  // execute application
  main();

?>

Bei jedem Aufruf ermittelt das Script mit Hilfe von Mercurial, welche Dateien in einem Repository/Ordner hinzugefügt, entfernt oder bearbeitet wurden. Sollte sich darunter die Canary-Datei befinden, wird Alarm geschlagen. Zudem wird eine statistische Analyse durchgeführt. In diesem einfachen Beispiel gilt als Abweichung, wenn mehr als 150% der durchschnittlichen Dateioperationen erkannt wurden. Wird solch eine Abweichung erkannt, wird ebenfalls Alarm geschlagen. Durch die Verwendung von Mercurial ließen sich zudem zu jeder Zeit alle bearbeiteten Dateien rekonstruieren.

In einem realen Umfeld könnte solch eine Analyse natürlich noch viel tiefergreifend sein. So könnten beispielsweise folgende Prüfungen mit einfließen, um Anomalien besser erkennen zu können:

  • Es könnte die Uhrzeit der Dateibearbeitung mit in die Analyse einfließen. In Kombination mit der Auswertung von Arbeitsplänen und/oder Anwesenheitszeiten ließen sich bessere Modelle erstellen. So sollte es seltener der Fall sein, dass Dateien eines Mitarbeiters bearbeitet werden, der gar nicht anwesend ist.
  • Es könnte die Relation der Dateibearbeitungen untereinander betrachtet werden. Beispielswiese werden Büromitarbeiter wesentlich mehr Dateien erstellen und bearbeiten als löschen, da Arbeitsergebnisse selten wieder vernichtet werden.
  • Es könnte das ursprüngliche Dateidatum mit berücksichtigt werden. Beispielsweise ist es eher unüblich, dass Dateien ab einem bestimmten Alter noch einmal bearbeitet werden. Stattdessen werden sie eher als Referenz vorgehalten, anstatt als aktives Arbeitsdokument zu fungieren.

Je besser das erstellte Modell ist, mit dem das Verhalten der Systemnutzer abgeglichen wird, desto schneller erkennt man in Ausnahmesituationen einen potentiellen Angreifer. Nicht immer müssen das externe Angreifer sein. Auch interne Mitarbeiter, die ein ungewöhnliches Verhalten an den Tag legen, können auf diese Weise unter Umständen identifiziert werden. Es muss nicht einmal zwingend eine böse Absicht hinter diesem Verhalten stecken, evtl. stellt ein Mitarbeiter einfach eine Ausnahme zur üblichen Regel dar.

Generell sollte man sich bei solch einer Angriffserkennung auf Basis von Abweichungsermittlungen daran gewöhnen, auch False Positives zu erhalten. Diese sind durchaus wünschenswert, da sie einerseits zeigen, dass die durchgeführten Analysen tatsächlich einen Effekt haben und einem andererseits Verbesserungspotentiale in den erstellten Anomaliemodellen aufzeigen.


Passwort-Reset in WordPress deaktivieren

09.12.2015 yahe code legacy security wordpress

Immer wieder findet man vermeindliche Kleinigkeiten, mit denen man sich noch besser gegen Angreifer schützen kann. Einer dieser Blind Spots war bei mir bisher die Passwort-zurücksetzen-Funktion meiner Webseiten. WordPress liefert hier leider keine einfache Konfigurationseinstellung, um sie zu deaktivieren. Da ich in meinem Fall neben dem WordPress-Nutzer auch gleichzeitig der Serveradmin bin und mir direkt auf der Kommandozeile ein neues Passwort verpassen kann, benötige ich den Weg über eine Passwort-Reset-E-Mail gar nicht und kann diese Funktion auch genauso gut abschalten. So besteht nicht mehr die Gefahr, dass meine Webseiten durch gehijackte E-Mail-Accounts kompromittiert werden können.

Hierfür notwendig ist ein winziges Plugin, das man sich schnell selbst zusammenschreiben kann:

<?php
  /*
    Plugin Name: KillPWReset
    Description: Disables the Password Reset in WordPress.
    Version: 0.1c1
  */

function killpwreset_disable($userid) {
  return false;
}

function killpwreset_errortext($errortext) {
  return "Login failed.";
}

add_filter("allow_password_reset", "killpwreset_disable");
add_filter("login_errors",         "killpwreset_errortext");
?>

Der Code sorgt dafür, dass für sämtliche Nutzer der Passwort-Reset deaktiviert wird. Weiterhin sorgt er dafür, dass eine generische Fehlermeldung erzeugt wird, wenn ein Login fehlschlägt. Typischerweise ist in der Fehlermeldung die Passwort-Reset-Funktion mit verlinkt. So wird der Fehlertext samt Link einfach überschrieben.


Java: Klasse mit korrektem Interface dynamisch laden

21.10.2015 yahe code java legacy

Bei größeren Anwendungen, die flexibel in verschiedenen Bereichen einsetzbar sein sollen, kommt schnell der Wunsch auf, diese erweiterbar zu gestalten, sodass andere Nutzer zur Not eigene Erweiterungen schreiben können. Java bietet hierfür mit seinen von Hause aus mitgebrachten Classloadern bereits die passende Grundlage, diese muss nur noch richtig zusammengesetzt werden.

Um das Vorgehen einmal darzustellen, habe ich ein kleines Projekt aufgesetzt, das aus drei Teilen besteht:

  1. Die Klassenbibliothek "lib", in der ein Interface definiert ist: Das Interface dient als Schnittstelle zwischen der eigentlichen Anwendung und der Erweiterung. Durch Zuhilfenahme eines Interfaces müssen die späteren Methodenaufrufe des Plugins nicht über Reflection realisiert werden.
  2. Die Klassenbibliothek "plugin", in der die Erweiterung implementiert ist: Die Erweiterung ist eine Klasse, die das Interface aus der Klassenbiliothek "lib" implementiert.
  3. Die Anwendung "app", die die eigentliche Anwendungslogik implementiert: Die Anwendung ist der eigentliche Nutznießer der Erweiterbarkeit. Sie lädt das Plugin und führt den darin enthaltenen Code aus.

Fangen wir mit der Implementierung der Klassenbibliothek "lib" an. Die beschriebenen Schritte sind beispielhaft für die Entwicklungsumgebung Netbeans, sollten in anderen IDEs jedoch genauso einfach umsetzbar sein:

  1. Zuerst legen wir ein neues Projekt an und wähle als Projektart "Java Class Library" aus. Als Projektnamen vergeben wir einfach den Namen "lib".
  2. Als nächstes legen wir im Projekt eine neue Datei an und wähle als Typ das "Java Package" aus. Als Namen erhält es in diesem Beispiel "com.example.loadclass.lib".
  3. Innerhalb des Packages wiederum legen wir eine neue Datei vom Typ "Java Interface" an. Die Klasse erhält den Namen "ExampleInterface".
  4. Der Inhalt der Interface-Datei ist in diesem Beispiel minimal:
package com.example.loadclass.lib;

public interface ExampleInterface {

    public String returnValue();

}

Nun machen wir weiter mit der Implementierung des Plugins:

  1. Wir legen ein weiteres Projekt an und wähle als Projektart wieder die "Java Class Library" aus. Als Projektnamen vergeben wir dieses Mal den Namen "plugin".
  2. Mit einem Rechtsklick auf den Projekteintrag im Projektbrowser gehen wir auf "Properties" und im Konfigurationsfenster auf die Kategorie "Libraries". Hier können wir über "Add Project..." nun das vorhin angelegte Projekt "lib" als Abhängigkeit aufnehmen. Nach einem Klick auf "OK" geht es weiter.
  3. Wir legen im Projekt eine neue Datei an und wählen als Typ das "Java Package" aus. Als Namen erhält es nun "com.example.loadclass.plugin".
  4. Innerhalb des Packages legen wir eine neue Datei vom Typ "Java Class" an. Die Klasse erhält den Namen "ExamplePlugin".
  5. Die Funktion des Plugins ist entsprechend des Interfaces ebenfalls minimal:
package com.example.loadclass.plugin;

import com.example.loadclass.lib.ExampleInterface;

public class ExamplePlugin implements ExampleInterface {

    @Override
    public String returnValue() {
        return "Hello world!";
    }

}

Bis hierhin gibt es, abgesehen davon, dass wir Interface und Implementierung in eigene Projekte auslagern, wenig Neues im Vergleich zu einer einfachen Java-Anwendung. Spannend wird es in der eigentlichen Anwendung, die wir nun anlegen:

  1. Wir legen ein weiteres Projekt an und wählen nun als Typ die "Java Application" aus. Als Namen erhält das Projekt dieses Mal den Wert "app". Zudem wählen wir aus, dass eine Main-Klasse mit dem Namen "com.example.loadclass.app.Main" erstellt werden soll.
  2. Als nächsten Schritt machen wir wieder einen Rechtsklick auf den Projekteintrag im Projektbrowser, gehen auf "Properties" und im Konfigurationsfenster auf die Kategorie "Libraries". Über "Add Project..." nehmen wir das Projekt "lib" wieder als Abhängigkeit auf und klicken abschließend auf "OK".
  3. Anschließend können wir die Main-Klasse mit Leben füllen. Wir beginnen mit ein paar Definitionen, die später in den Methoden benötigt werden. Die Methode "checkName()" dient dazu, herauszufinden, ob ein angegebener Plugin-Pfad tatsächlich korrekt ist. Als Pluginpfad verwenden wir die Form "<Klassenname>@<Bibliothekspfad>". Die Methode "loadClass()" verwendet den URLClassLoader, um die angegebene Klasse zu laden. Die Methode "createObject()" erzeugt eine Instanz der geladenen Klasse. Und in der Methode "main()" wird schlussendlich alles zusammengefügt:
package com.example.loadclass.app;

import com.example.loadclass.lib.ExampleInterface;
import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.jar.JarFile;

public class Main {

    protected static final String LOADCLASS_CLASS_EXTENSION = ".class";
    protected static final char   LOADCLASS_CLASS_SEPARATOR = '.';
    protected static final String LOADCLASS_JAR_EXTENSION   = ".jar";
    protected static final char   LOADCLASS_PATH_SEPARATOR  = '@';
    protected static final String LOADCLASS_URL_PREFIX      = "file://";

    protected static final String APP_PLUGIN_CLASS = "com.example.loadclass.plugin.ExamplePlugin";
    protected static final String APP_PLUGIN_JAR   = "/Users/kenny/Desktop/loadClass/plugin/dist/plugin.jar";
    protected static final String APP_PLUGIN_PATH  = APP_PLUGIN_CLASS + "@" + APP_PLUGIN_JAR;

    // structure is "<class>@<path>", where "<path>" may be a folder or a *.jar file
    protected static boolean checkName(String aString) {
        boolean lResult = false;

        if (null != aString) {
            // the combinator must not be at the very beginning or at the very end
            if ((aString.indexOf(LOADCLASS_PATH_SEPARATOR) > 0) &&
                (aString.indexOf(LOADCLASS_PATH_SEPARATOR) < aString.length()-1)) {
                String lClassName = aString.substring(0, aString.indexOf(LOADCLASS_PATH_SEPARATOR));
                String lPathName  = aString.substring(aString.indexOf(LOADCLASS_PATH_SEPARATOR)+1);

                File lPath = new File(lPathName);
                if (lPath.exists()) {
                    if (lPath.isFile()) {
                        if (lPathName.endsWith(LOADCLASS_JAR_EXTENSION)) {
                            try (JarFile lJarFile = new JarFile(lPath)) {
                                if (null != lJarFile.getJarEntry(lClassName.replace(LOADCLASS_CLASS_SEPARATOR,
                                                                                    File.separatorChar) +
                                                                 LOADCLASS_CLASS_EXTENSION)) {
                                    lResult = true;
                                }
                            } catch (Exception lException) {}
                        }
                    } else {
                        if (lPath.isDirectory()) {
                            // fix path delimiter
                            if (File.separatorChar != lPathName.charAt(lPathName.length()-1)) {
                                lPathName = lPathName + File.separator;
                            }

                            // check if the class exists
                            lPath = new File(lPathName +
                                             lClassName.replace(LOADCLASS_CLASS_SEPARATOR, File.separatorChar) +
                                             LOADCLASS_CLASS_EXTENSION);
                            if (lPath.isFile()) {
                                lResult = true;
                            }
                        }
                    }
                }
            }
        }

        return lResult;
    }

    protected static Class loadClass(String aName) {
        Class lResult = null;

        if (checkName(aName)) {
            // get name parts of parameter
            String lClassName = aName.substring(0, aName.indexOf(LOADCLASS_PATH_SEPARATOR));
            String lPathName  = aName.substring(aName.indexOf(LOADCLASS_PATH_SEPARATOR)+1);

            // check that directory name ends with path separator
            if (new File(lPathName).isDirectory()) {
                if (File.separatorChar != lPathName.charAt(lPathName.length()-1)) {
                    lPathName = lPathName + File.separator;
                }
            }

            try {
                URL lURL = new URL(LOADCLASS_URL_PREFIX + lPathName);

                URLClassLoader lClassLoader = new URLClassLoader(new URL[]{lURL},
                                                                 ClassLoader.getSystemClassLoader());

                lResult = lClassLoader.loadClass(lClassName);
            } catch (Exception lException) {}
        }

        return lResult;
    }

    protected static Object createObject(String aName, Class aInterface) {
        Object lResult = null;

        if (null != aInterface) {
            Class lClass = loadClass(aName);
            if (null != lClass) {
                boolean lCorrectInterface = false;

                for (Class lInterface : lClass.getInterfaces()) {
                    if (aInterface.equals(lInterface)) {
                        lCorrectInterface = true;
                        break;
                    }
                }

                if (lCorrectInterface) {
                    try {
                        lResult = lClass.newInstance();
                    } catch (Exception lException) {}
                }
            }
        }

        return lResult;
    }

    public static void main(String[] args) {
        ExampleInterface lObject = (ExampleInterface)createObject(APP_PLUGIN_PATH,
                                                                  ExampleInterface.class);
        if (null != lObject) {
            System.out.println(lObject.returnValue());
        }
    }

}

Wenn ihr die Pakete "lib.jar", "plugin.jar" und "app.jar" alle einmal erzeugt habt, müsst ihr euch ansehen, unter welchem Dateipfad das Plugin-Paket erreichbar ist. In meinem Beispiel ist es "/Users/kenny/Desktop/loadClass/plugin/dist/plugin.jar". Diesen vollständigen Pfad müsst ihr in die Variable "APP_PLUGIN_JAR" schreiben. In einem tatsächlichen Anwendungsfall würde man solche Informationen dann wahrscheinlich eher aus einer Konfigurationsdatei auslesen.


Java-Anwendung sauber beenden mit addShutdownHook

21.09.2015 yahe code java legacy

Bei einfachen Java-Anwendungen macht man sich typischerweise wenig Gedanken um das saubere Beenden aller Prozesse. Man geht davon aus, dass der Nutzer weiß, was er tut, wenn er auf Beenden klickt und fragt zur Not vorher nochmal nach, ob er tatsächlich fortfahren will. Anders sieht das jedoch aus, wenn man mehrere Threads verwaltet, wie das zum Beispiel bei Serveranwendungen der Fall ist. Dann möchte man vor dem tatsächlichen Beenden lieber doch noch die einen oder anderen Aufräumarbeiten durchführen, zum Beispiel Logdateien flushen, laufende Serveranfragen beantworten oder temporäre Dateien wegräumen. Die JVM-Runtime bietet für solche Fälle die Möglichkeit, sogenannte Shutdown-Hooks via "Runtime.getRuntime().addShutdownHook()" zu registrieren. Wenn die JVM beendet wird, werden die registrierten Hooks ausgeführt. Im Grunde sind es Threads, deren "run()"-Methode aufgerufen wird.

Im folgenden Beispiel habe ich mal eine kleine Vorlage gebastelt, in der man sieht, wie man das ganze verwenden kann. In der Methode "startupMain()" implementiert man alle Initialisierungen, die beim Programmstart ausgeführt werden sollen, in der Methode "runMain()" führt man dann die tatsächliche Anwendung aus, während in der Methode "shutdownMain()" notwendige Deinitialisierungen durchgeführt werden können:

public class Main extends Object implements Runnable {

    // singleton instance
    protected static Main fInstance = null;

    static {
        // create singleton instance
        fInstance = new Main();
    }

    // prevent external instantiation
    protected Main() {
        super();
    }

    public static Main getInstance() {
        return fInstance;
    }

    public static void main(String[] args) {
        Main lMain = Main.getInstance();

        // call startup code
        lMain.startupMain(args);

        // register shutdown code
        Runtime.getRuntime().addShutdownHook(new Thread(lMain));

        // call main code
        lMain.runMain();
    }

    @Override
    public void run() {
        // call shutdown code
        shutdownMain();
    }

    public synchronized void startupMain(String[] aArguments) {
        // startup code
        // ...
    }

    public synchronized void shutdownMain() {
        // shutdown code
        // ...
    }

    public void runMain() {
        // main code
        // ...
    }

}

Als Test habe ich mir eine einfache Anwendung geschrieben, die beim Starten das Wort "start", während des Laufens in einer Endlosschleife das Wort "sleep" und beim Beenden das Wort "stop" schreibt. Die drei Methoden sehen dabei wie folgt aus:

    public synchronized void startupMain(String[] aArguments) {
        // startup code
        // ...
        System.out.println("start");
    }

    public synchronized void shutdownMain() {
        // shutdown code
        // ...
        System.out.println("stop");
    }

    public void runMain() {
        // main code
        // ...
        while (true) {
            System.out.println("sleep");
            try {
                Thread.sleep(1000);
            }
            catch (Exception e) {}
        }
    }

Nach dem Starten verwende ich den Linuxbefehl "kill -SIGINT <Prozess-ID>" um die Anwendung wieder zu beenden. Dabei wird vor dem tatsächlichen Beenden noch der Shutdown-Hook ausgeführt. Ich habe ein kleines Video bei Youtube hochgeladen, das das ganze mal demonstriert.

Wie ihr seht, kann man mit einfachen Mitteln Java-Anwendungen sauber und ordentlich beenden. Es ist übrigens auch möglich, mehrere Shutdown-Hooks zu registrieren. Da jedoch deren Ausführungsreihenfolge nicht definiert ist, ist anzuraten, nur einen Shutdown-Hook für die gesamte Anwendung zu verwenden und in diesem zur Not weiteren Cleanup-Code aufzurufen. Da der Shutdown-Hook ein Thread ist, müsst ihr euch zudem um Themen wie Threadsicherheit kümmern.


WordPress mit TLS Client Authentication absichern

27.04.2015 yahe administration code legacy security wordpress

Vor einiger Zeit hatte ich einmal gezeigt, wie man WordPress gegen eine größere Menge von Zero-Day-Exploits absichern kann, indem man verschiedene Datenbanknutzer einführt. Dadurch verwenden normale Webseitenbesucher einen Datenbanknutzer mit geringeren Privilegien und Nutzer des Backends einen Datenbanknutzer mit höheren Privilegien. Bei Softwarefehlern, die schreibenden Zugriff auf die Datenbank ermöglichen, können so Schreibversuche unterbunden werden.

Leider ist den Entwicklern von WordPress nicht sonderlich daran gelegen, die Datenbank so zu strukturieren, dass bestimmte Datenbankzugriffe für gewisse Nutzer nicht notwendig sind. So wurde vor einiger Zeit ein neues Session-Management eingeführt, das es zwar ermöglicht, Sitzungen auf anderen Rechnern zu beenden, dafür jedoch Daten in eine Tabelle schreibt, in der auch wichtige Konfigurationsinformationen abgelegt sind. Das führte zu Problemen bei der Trennung der Datenbanknutzer. Ich überlegte deshalb, wie ich die Trennung der Datenbanknutzer beibehalten und gleichzeitig flexibler bestimmen kann, wann ein höher privilegierter Datenbanknutzer benötigt wird.

Ebenfalls vor einiger Zeit hatte ich einmal gezeigt, wie man in NGINX TLS-Clientzertifikate einsetzen kann. Meine Idee war nun, die Steigerung der Privilegien innerhalb der WordPress-Datenbank an die Nutzung von TLS-Clientzertifikaten zu koppeln. So hat man eine Art Zwei-Faktor-Authentisierung: Mit seinem Zertifikat schaltet man die Nutzerrechte in der Datenbank frei und mit seinem Passwort loggt man sich im Backend ein.

Mein erster Versuch war, mit optionalen Clientzertifikaten zu arbeiten. Der Server fragt die Clientauthentisierung beim Browser an. Wenn der Browser kein Clientzertifikat zurückliefert, wird trotzdem mit der Seitendarstellung mit geringeren Priviliegen fortgefahren. Allerdings stellte sich heraus, dass der Safari-Browser bei jedem einzelnen Seitenaufruf nach dem Clientzertifikat fragt, sobald man eines hinterlegt. Eigentlich sendet der Server das Zertifikat der CA mit, von der das passende Clientzertifikat ausgestellt sein muss. Browser können diese Information nutzen, um die Clientzertifikate herauszufiltern, die nicht verwendet werden können. Doch leider sieht das Apple anders und so fragt der Safari den Nutzer ständig nach dem zu verwendenden Clientzertifikat, selbst, wenn es kein passendes gibt. Also musste eine nutzerverträglichere Lösung her.

In der neuen Lösung ruft man eine separate Seite auf, für die man zwingend eine Clientauthentisierung durchführen muss. Diese überprüft die Authentizität des Zertifikats und leitet dann auf einen Teil der eigentlichen, zu schützenden Seite weiter. Dieser Teil startet die Session, in der hinterlegt wird, dass die höheren Privilegien genutzt werden dürfen. Der Aufruf dieses Teils ist durch ein zeit- und passwortbasiertes Challenge-Response-Verfahren geschützt. Das spannende an diesem Ansatz ist, dass es prizipiell auch dafür geeignet ist, ein Single-Sign-On-Portal für verschiedenste Anwendungen aufzubauen. Kern der Anwendung ist die Datei "auth.php", in der sich die grundlegende Logik befindet:

<?php
  define("SUCCESS_CONTENT", "SUCCESS"); // defines what text should be in TLS_SUCCESS

  define("CHALLENGE_ALGO",   "sha256"); // defines the algorithm used for response HMAC
  define("CHALLENGE_LENGTH", 32);       // defines the length of the random challenge

  define("PARAM_START",   "?"); // should be static
  define("PARAM_VALUE",   "="); // should be static
  define("PARAM_COMBINE", "&"); // should be static

  define("CHALLENGE_PARAM", "challenge"); // name of challenge GET parameter
  define("RESPONSE_PARAM",  "response");  // name of response GET parameter

  define("DN_PARAM",      "TLS_DN");      // name of DN SERVER parameter
  define("SUCCESS_PARAM", "TLS_SUCCESS"); // name of success SERVER parameter

  define("LOGINSESSION_LOGGEDIN", "loginsession_loggedin");

  function getChallenge() {
    return openssl_random_pseudo_bytes(CHALLENGE_LENGTH);
  }

  function getResponse($challenge, $previous = false) {
    // prepare a delta time value for cases where we missed the variance window
    $delta = ($previous) ? CHALLENGE_VARIANCE : 0;

    // use the current time as a replay prevention mechanism
    $time_challenge = dechex((int)((time()-$delta) / CHALLENGE_VARIANCE));

    return hash_hmac(CHALLENGE_ALGO, $challenge.$time_challenge, CHALLENGE_KEY);
  }

  function check_GET() {
    $result = false;

    // check if the parameters are set
    if (isset($_GET[CHALLENGE_PARAM]) && isset($_GET[RESPONSE_PARAM])) {
      // convert challenge parameter to binary
      $challenge = hex2bin($_GET[CHALLENGE_PARAM]);
      // check if the challenge parameter was a hex
      if (false !== $challenge) {
        // calculate response from challenge parameter
        $response = getResponse($challenge, false);
        // check if response calculation was possible
        if (false !== $response) {
          // check if the calculated response matches the response parameter
          $result = (0 === strcasecmp($response, $_GET[RESPONSE_PARAM]));
        }

        // maybe we just passed the time variance window, test the previous one
        if (!$result) {
          // calculate response from challenge parameter
          $response = getResponse($challenge, true);
          // check if response calculation was possible
          if (false !== $response) {
            // check if the calculated response matches the response parameter
            $result = (0 === strcasecmp($response, $_GET[RESPONSE_PARAM]));
          }
        }
      }
    }

    return $result;
  }

  function check_SERVER() {
    $result = false;

    // check if the necessary TLS server values have been set
    if ((isset($_SERVER[DN_PARAM])) && (isset($_SERVER[SUCCESS_PARAM]))) {
      // check if the TLS server values contained the required content
      $result = ((0 === strcasecmp(DN_CONTENT,      $_SERVER[DN_PARAM])) &&
                 (0 === strcasecmp(SUCCESS_CONTENT, $_SERVER[SUCCESS_PARAM])));
    }

    return $result;
  }

  function createSession() {
    // open a session
    session_start();

    // session should exist now
    if (PHP_SESSION_ACTIVE === session_status()) {
      // prevent session fixation
      session_regenerate_id(true);

      $_SESSION[LOGINSESSION_LOGGEDIN] = true;
    }
  }

  function destroySession() {
    // check if there actually is a session to destroy
    if ((isset($_COOKIE[session_name()])) || (isset($_GET[session_name()]))) {
      // open the existing session
      session_start();

      // session should be open now
      if (PHP_SESSION_ACTIVE === session_status()) {
        // remove loggedin value from session
        if (isset($_SESSION[LOGINSESSION_LOGGEDIN])) {
          unset($_SESSION[LOGINSESSION_LOGGEDIN]);
        }

        // check if the session is empty
        if (0 === count($_SESSION)) {
          // destroy the session if it is
          session_unset();
          session_destroy();

          // unset the session cookie
          if (isset($_COOKIE[session_name()])) {
            $params = session_get_cookie_params();
            setcookie(session_name(), "", time()-3600, $params["path"], $params["domain"], $params["secure"], $params["httponly"]);
          }
        } else {
          // prevent session fixation
          session_regenerate_id(true);
        }
      }
    }
  }

  function doRedirect($target) {
    header("HTTP/1.1 302 Moved Temporarily");
    header("Location: ".$target);
  }

  function triggerChallengeResponse() {
    // generate new challenge
    $challenge = getChallenge();
    // check if challenge generation worked
    if (false !== $challenge) {
      // calculate response from challenge
      $response = getResponse($challenge, false);
      // check if response calucation worked
      if (false !== $response) {
        // convert challenge from binary to hex
        $challenge = bin2hex($challenge);

        // redirect to challenge-response
        doRedirect(PARAM_PATH.PARAM_START.CHALLENGE_PARAM.PARAM_VALUE.$challenge.PARAM_COMBINE.RESPONSE_PARAM.PARAM_VALUE.$response);
      }
    }
  }
?>

Die eigentliche Aufgabe, das Clientzertifikat zu prüfen, zur eigentlichen Webseite zu redirecten und dort die Session zu starten, übernimmt die Datei "logon.php". Diese gibt es zwei Mal, einmal auf dem Server, der die TLS-Clientauthentisierung übernimmt und einmal auf dem Server, auf dem WordPress installiert ist. Die in der Datei enthaltene Konfiguration sollte auf beiden Servern identisch sein:

<?php
  define("DN_CONTENT", "/cn=example.com"); // defines what text should be in TLS_DN

  define("CHALLENGE_VARIANCE", 300); // defines how long the challenge will be usable
  define("CHALLENGE_KEY",      "V3RY 5TR0NG P455W0RD"); // defines the password used for the response HMAC

  define("PARAM_PATH", "https://example.com/auth/logon.php"); // redirect path

  define("FINAL_PATH", "https://example.com/wp-login.php");

  require_once(dirname(__FILE__)."/auth.php");

  if (check_GET()) {
    createSession();
    print("<a href=\"".FINAL_PATH."\">Logon</a>");
  } else {
    if (check_SERVER()) {
      triggerChallengeResponse();
      exit();
    }
  }
?>

Es kann auch sinnvoll sein, die erhaltenen Privilegien wieder abzugeben, indem man die Session beendet. Diese Aufgabe übernimmt die Datei "logoff.php", die ebenfalls auf beiden Servern vorhanden und dieselbe Konfiguration enthalten sollte:

<?php
  define("DN_CONTENT", "/cn=example.com"); // defines what text should be in TLS_DN

  define("CHALLENGE_VARIANCE", 300); // defines how long the challenge will be usable
  define("CHALLENGE_KEY",      "V3RY 5TR0NG P455W0RD"); // defines the password used for the response HMAC

  define("PARAM_PATH", "https://example.com/auth/logoff.php"); // redirect path

  require_once(dirname(__FILE__)."/auth.php");

  if (check_GET()) {
    destroySession();
    print("logoff");
  } else {
    if (check_SERVER()) {
      triggerChallengeResponse();
      exit();
    }
  }
?>

In der eigentlichen Anwendung kann dann anhand der Session zwischen den Datenbanknutzern unterschieden werden. Im Falle von WordPress habe ich das in der Konfigurationsdatei "wp-config.php" getan:

$loginsession_evaluated = false;
// open the session if there is one
if ((isset($_COOKIE[session_name()])) || (isset($_GET[session_name()]))) {
  session_start();

  if ((isset($_SESSION["loginsession_loggedin"])) && ($_SESSION["loginsession_loggedin"])) {
    $loginsession_evaluated = true;
  }
}

// select user dependent on loginsession
if (($loginsession_evaluated) || (defined("DOING_CRON"))) {
  define("DB_USER",     "<ADMIN USER>");
  define("DB_PASSWORD", "<ADMIN PASSWORD>");
} else {
  define("DB_USER",     "<GUEST USER>");
  define("DB_PASSWORD", "<GUEST PASSWORD>");
}

Da das von der Theorie her vielleicht alles ein bisschen trocken ist, habe ich den Ablauf eines vollständigen Logins als Screencapture aufgenommen und bei Youtube hochgeladenen. Vielleicht wird dadurch klarer, wie die einzelnen Schritte ineinander greifen.

Ich hoffe, dieser kleiner Exkurs bietet dem ein oder anderen eine neue Lösungsmöglichkeit für die Absicherung von Nutzerzugängen. Die gezeigte Implementation habe ich, soweit wie möglich, generisch gehalten. Durch das Challenge-Reponse-Verfahren ist sie beispielsweise auch als Grundlage für Inter-Domain-Single-Sign-On geeignet und durch die Verwendung von einfachen Sessions sollte sie zudem softwareunabhängig einsetzbar sein.


Search

Links

RSS feed

Categories

administration (43)
arduino (12)
calcpw (2)
code (37)
hardware (16)
java (2)
legacy (113)
linux (29)
publicity (7)
review (2)
security (62)
thoughts (22)
update (9)
windows (17)
wordpress (19)