Versandkontrolle

Ein schlampig programmiertes Online-Formular zum Versand von E-Mails macht den eigenen Webserver schnell zur Spamschleuder.

Lesezeit: 8 Min.
In Pocket speichern
vorlesen Druckansicht
Von
  • Herbert Braun
Inhaltsverzeichnis

Einige rätselhaft aussehende E-Mails sind die Vorboten. Die Nachrichten mit unleserlichen Absenderangaben und sinnlosen Betreffzeilen prüfen E-Mail-Formulare in Webseiten auf Verwundbarkeit. Sind sie erfolgreich, wird der Server bald hunderttausende von Spam-Nachrichten durchs Internet schicken. Die Masche ist alt, aber durch skriptgesteuerte Ausbeutungsversuche hat sie sich in letzter Zeit massiv ausgebreitet. Ein unbedarft programmiertes Skript erlaubt es Angreifern, E-Mail-Kopfdaten einzuschleusen. Damit lassen sich zusätzliche Empfänger eintragen, die Mailnachricht verändern oder Anhänge hinzufügen.

Mit wenigen Zeilen PHP (Listing 1, siehe unten) ist eine einfache, aber unsichere Webanwendung für den Mailversand gestrickt. Beim ersten Aufruf der Seite gibt das Skript den HTML-Block mit dem Formular aus. Nach einem Klick auf den Versandknopf ruft der Browser die Seite erneut auf. PHP versucht dann, mit der Standardfunktion mail() eine E-Mail mit den Formulardaten zu verschicken.

Abgeklopft: Ein Angreifer versucht, sich über ein E-Mail-Formular Nachrichten an jrubin3456@aol.de senden zu lassen, um die Anwendung für Spam-Versand zu missbrauchen.

Das ist immerhin nicht die niedrigste Sicherheitsstufe. Das vermutlich populärste Perl-CGI-Skript überhaupt, FormMail von Matt's Script Archive - seit 1995 zwei Millionen Mal von www.scriptarchive.com heruntergeladen- , entnahm in früheren Versionen den Mail-Empfänger einem verborgenen Eingabefeld. Ein Angreifer konnte einfach andere Empfängeradressen an den Server übergeben. Aktuelle Versionen des Skripts gleichen die übermittelte Empfängeradresse mit einer Liste zugelassener Empfänger ab. Im Beispielskript ist der Empfänger dagegen fest eingetragen als erster Parameter von mail(). Der Betreff und der Text der Mail folgen als zweites und drittes Argument, an vierter Stelle erwartet mail() zusätzliche Kopfdaten – im Beispiel den Absender (From). Hier liegt die Schwachstelle: PHP reicht dem Server beliebige E-Mail-Kopfdaten durch, falls der Angreifer es schafft, diese im richtigen Format zu übergeben.

Laut E-Mail-Spezifikation RFC 822 müssen Mailheader durch Zeilenumbrüche getrennt werden. Da das Formular die GET-Methode akzeptiert, gelingt das ganz einfach im Browser:

[Skript-URL]?betreff=sub&text=txt&los=1&absender=test%40te.st%0ABcc:spammer%40nix.ix

Was durch die URL-Kodierung auf den ersten Blick kompliziert aussieht, ist in Wahrheit simpel: Außer den Platzhalterwerten für die Felder betreff, text und los übergibt der Angreifer einen Blindkopie-Header (Bcc) an das Skript. Die Zeichenfolge %40 entspricht dem @-Zeichen, %0A ist die Kodierung für den Zeilenumbruch (Linefeed, oder \l). Der Inhalt des Absenderfelds lautet also:

test@te.st
Bcc:spammer@nix.ix

Mit dem vom Skript vor diesen beiden Zeilen eingesetzten From: geht die Mail auf Reisen. spammer@nix.ix erhält eine Kopie der Nachricht, ohne dass der ursprüngliche Empfänger nobody@heise.de dies der Mail entnehmen kann.

Die Umstellung des Skripts auf die Versandmethode POST bringt keine wirkliche Sicherheit. Zwar nützt es nichts, den obigen String in das Absender-Formularfeld einzugeben, da der Browser die Prozentzeichen zu %25 konvertiert. Allerdings gibt es genügend Möglichkeiten, dem Skript die richtige Zeichenfolge zu übergeben, etwa die Firefox-Erweiterung Live HTTP Headers (http://livehttpheaders.mozdev.org), modifizierte HTML-Formulare oder selbst geschriebene Skripte.

Der Angreifer kann den Nachrichtentext auch dann manipulieren, wenn dieser im Skript fest eingetragen ist. Bei E-Mails ist der Text von den Kopfdaten einfach durch eine Leerzeile getrennt. Mit absender=test%40te.st%0A%0ASpamtext setzt der Angreifer den Text Spamtext über den im dritten Parameter von mail() übergebenen Text. Je nach Einstellungen des empfangenden Mailprogramms kann er sogar verhindern, dass der Empfänger den ursprünglichen Inhalt zu Gesicht bekommt:

absender=test%40te.st%0A
Content-type:multipart/mixed;boundary=xxx%0A%0A
--xxx%0A
Content-type:text/html%0A%0A%
3Ch1%3ESpamtext%3C/h1%3E%0A
--xxx

Die Content-type-Deklaration weist die Nachricht als Mischung verschiedener MIME-Typen aus. Der Mail-Client soll nur anzeigen, was zwischen den beiden Grenzen (boundary) liegt. Zwischen den beiden Grenzen (den Zeilen mit --xxx) steht im Mail-Body der MIME-Typ, in diesem Fall eine HTML-Nachricht. %3C und %3E verschlüsseln die spitzen Klammern -- der Mail-Client sollte das Wort Spamtext als große Überschrift darstellen.

Auf diese Weise kann ein Angreifer der Mail sogar Anhänge unterjubeln. Dazu kennzeichnet man einen Abschnitt einer multipart/mixed-Mail mit Content-Disposition: attachment.

Diese Sicherheitslücke ist nicht PHP-spezifisch, doch ist die mail-Funktion nicht ganz unschuldig an ihrem massenhaften Missbrauch: Die ersten drei Parameter der Funktion gaukeln dem Programmierer eine Sicherheit vor, die der vierte zunichte macht. Bei Python etwa kapselt das email-Paket alle Zugriffe auf die Mailheader. Ähnlich funktionieren diverse Perl-Module wie Mail::Sendmail. Allerdings sind unter Perl-Programmierern noch Praktiken aus den frühen Zeiten des WWW im Schwange:

open(MAIL, '|/usr/lib/sendmail -t') or die;
print MAIL <<EOM;
To: $empfaenger
From: $sender
...
EOM

Dieses Skriptfragment reicht den Inhalt des Here-Dokuments über den Handler MAIL an das Mailversand-Programm. Alle Header-Werte sind hier prinzipiell anfällig für Manipulationen.

Bei diesem wie beim PHP-Beispiel ist eine genaue Prüfung der Benutzereingaben unerlässlich. Eine einfache Sicherheitsvorkehrung wäre, sämtliche Eingabefelder auf Zeilenumbrüche zu prüfen:

if(strpos($_GET['absender'], "\n")) die("Zeilenumbruch!");

Das ist aber nur ein notdürftiger Flicken. Möglicherweise interpretiert das System auch andere Zeichen als Zeilenumbruch, beispielsweise Carriage Return (, \r oder %0D). Sucht man nicht nach verbotenen, sondern nach erlaubten Zeichen, hat das die nützliche Nebenwirkung, dass der Nutzer sinnvolle Werte ins Formular eintragen muss. Beispielsweise lässt der folgende Perl-kompatible reguläre Ausdruck nur gültig aussehende E-Mail-Adressen durch:

/^[\w.+-]{1,64}\@[\w.-]{1,255}\.[a-z]{2,6}$/

Die Metazeichen ^ und $ stellen sicher, dass vor und nach dem Prüfstring nichts kommt. Vor dem Klammeraffen muss mindestens ein Buchstabe, Ziffer, Unterstrich (dafür steht \w), Punkt, Plus- oder Minuszeichen kommen, danach folgt der Domainname (mit eventuellen Subdomains). Die zwei bis sechs Buchstaben nach dem Punkt stehen für die Top-Level-Domain. Das erfasst nicht jeden Sonderfall, genügt aber in der Praxis. Mit der preg_match-Funktion kann man auch die anderen Felder überprüfen -- eleganterweise mittels einer Schleife (siehe Listing 2), die vor der mail-Zeile steht. [:print:] ist eine Posix-Zeichenklasse, die alle möglichen druckbaren Zeichen enthält, also Buchstaben, Ziffern, Leerzeichen und Interpunktion. [:space:] erlaubt zusätzlich noch Zeilenumbrüche und Tabulatoren. Damit das Skript Umlaute und Buchstaben wie ß oder i passieren lässt, fügt man oben die setlocale-Zeile ein. Mit diesen Vorkehrungen dürfte Angreifern schnell die Lust an Missbrauchsversuchen vergehen.

1 <?php
2 if(isset($_GET['los'])) {
3 if(mail('nobody@heise.de', $_GET['betreff'], $_GET['text'],
"From:" . $_GET['absender'])) {
4 echo "<p>Nachricht gesendet!</p>";
5 } else {
6 echo "<p>Nachricht nicht gesendet!</p>";
7 }
8 } else {
9 ?>
10 <h1>Unsicheres Skript! Nicht verwenden!</h1>
11 <p>Senden Sie mir eine Nachricht:</p>
12 <form action="">
13 <p>
14 Absender: <input name="absender"/><br/>
15 Betreff: <input name="betreff"/><br/>
16 Text: <textarea name="text"></textarea><br/>
17 <input type="submit" name="los" value="Abschicken!"/>
18 </p>
19 </form>
20 <?php
21 }
22 ?>
1 error_reporting(E_ALL);
2 setlocale(LC_ALL, 'de_DE');
3 //Falls Formular abgeschickt wurde:
4 if(isset($_POST['los'])) {
5 //Prüfmuster
6 $pruefung = array(
7 'absender' => '/^ [\w.!#%&\*\/=\?\^\`\{\|\}\~+-]{1,64}
\@ [[:alnum:].-]{1,255} \. [a-z]{2,6} $/xi',
8 'betreff' => '/^[[:print:]]{3,}$/',
9 'text' => '/^[[:print:][:space:]]{10,}$/'
10 );
11 //Eingabeprüfung
12 foreach($_POST as $parameter => $wert) {
13 if(isset($pruefung[$parameter])) {
14 if(!preg_match($pruefung[$parameter], $wert))
die('Probleme mit Feld ' . $parameter . ': ' . $wert);
15 } else {
16 unset($_POST[$parameter]);
17 }
18 }
19 //prüft Mailhost (MX)
20 //nur unter Unixen, keine Umlautdomains (sonst Zeile streichen)
21 if(!getmxrr(substr(strstr($_POST['absender'], '@'), 1), $mxhosts))
die("Konnte keine gültige Domain für " . $_POST['absender'] . " finden!");
22 //Mailversand
23 if(mail('nobody@heise.de', $_POST['betreff'], $_POST['text'], "From:" . $_POST['absender'])) {
24 echo "<p>Nachricht von " . $_POST['absender'] . " an nobody@heise.de gesendet!</p>";
25 } else {
26 echo "<p>Nachricht konnte nicht gesendet werden!</p>";
27 }
28 } else {
29 //HTML-Formular ...
30 }

Download der Listings (heb)