RSS Feed abonnieren

Captcha selbst bauen

Ok, fangen wir mit unserem kleinem Captcha-Tutorial an. Ich benutze dafür 2 Scripte, obwohl es auch relativ simpel wäre das in einem Script zu lösen, aber es erklärt sich besser. Wir nennen das eine Script einfach mal FOO, das andere nennen wir BAR.

FOO hat als erstes folgendes Aufgabe, es erzeugt eine sehr schwer zu erratenden Wert, in dem Fall einen MD5-Hash und übergibt diesen an BAR. Ebenso setzt es diese Variable in das eigene Formular.

BAR macht nun folgendes: Es generiert mehrere Zufallszahlen, zeichnet diese in ein Bild und gibt das Bild an den Browser. Außerdem legt es in einem Ordner eine temporäre Datei an, deren Namen sich aus dem von FOO übergebenen MD5-Hash und den Zufallszahlen zusammensetzt. Als Beispiel sei unser Hash ABC und die Zufallszahlen 1, 2 und 3. Also erstellt es die Datei: ABC123.

Jetzt kommt wieder FOO ins Spiel. Wird das Formular mit dem MD5-Hash abgesendet, prüft FOO ob die Datei namens MD5-Hash (ABC) + eingebenenes Ergebnis (123) existiert. Existiert diese, ist das Captcha gelöst und die Datei wird gelöscht. Existiert diese nicht, muß die eingegebene Lösung falsch gewesen sein, da ja der MD5-Hash gleich war. Ein Cronjob sollte regelmäßig die aus falschen Lösungen übrig gebliebene Tmp-Dateien löschen.

Eine Variante wäre auch das Eintragen der Werte in eine Datenbank, da löst sich das Problem mit den temporären Dateien, siehe dazu das Beispiel weiter unten

Stop! Vor dem Einbauen auf der eigenen Website bitte ausführlich die PHP-FAQ zum Thema Sicherheit lesen und umsetzen. In meinen Beispielen wird nicht auf die notwendige Prüfung der Variablen eingegangen.

foo.php

// Verzeichnis wo die temporären Dateien abgelegt werden
$DIR='/tmp';
 
// prüfe ob Formular gesendet wurde
if (!$_POST['loesung']) {
 
        // formular noch nicht gesendet, erzeuge Hash, schreibe den ins Formular und binde BAR (als Bild) ein, übergib BAR den Hash
        $hash = md5(uniqid (rand()));
        echo '<form action="'.$_SERVER['PHP_SELF'].'" method="post">';
        echo '<p><input type="hidden" name="hash" value="'.$hash.'" /></p>';
        echo '<p><img src="BAR.php?hash='.$hash.'" alt="captcha" /></p>';
        echo '<p>Gib die Lösung ein:<br /><input type="text" name="loesung" size="6" /></p>';
        echo '</form>';
 
} else {
 
        // formular gesendet, prüfe ob datei existiert, wenn dann löschen und dann statusmeldung
        if (file_exists($DIR.'/'.$_POST['hash'].$_POST['loesung'])) {
                @unlink($DIR.'/'.$_POST['hash'].$_POST['loesung']);
                echo '<p>Richtig gelöst</p>';
        } else {
                echo '<p>Falsch gelöst</p>';
        }
 
}

bar.php

// Verzeichnis wo die temporären Dateien abgelegt werden
$DIR='/tmp';
 
// Verzeichnis wo die TTF-Schriften liegen
// die müssen durchnumeriert sein, in dem Beispiel 1.ttf - 7.ttf
// hier aktuelles Verzeichnis
$FONTS = ".";
 
// leeres weißes Bild erzeugen
$image = imagecreatetruecolor(155,40);
$bgColor = ImageColorAllocate($image, 255, 255, 255);
ImageFilledRectangle($image, 0, 0, 155, 40, $bgColor);
 
// Schriftfarbe
$color = imagecolorallocate($image,0,0,0);
 
// Schriftgröße
$size=20;
 
// unsere Zufallszahlen, die 1 ist nicht mit dabei, wegen Verwechslungsgefahr mit der 7
$A = rand(2,9);
$B = rand(2,9);
$C = rand(2,9);
$D = rand(2,9);
$E = rand(2,9);
$F = rand(2,9);
 
// Zahlen auf das Bild zeichnen, Position etwas variieren, zufällig eine Schriftart auswählen (1.ttf-7.ttf)
imagettftext($image, $size, 0, 5,   25+rand(0,10), $color, $FONTS.'/'.rand(1,7).'.ttf',$A);
imagettftext($image, $size, 0, 30,  25+rand(0,10), $color, $FONTS.'/'.rand(1,7).'.ttf',$B);
imagettftext($image, $size, 0, 55,  25+rand(0,10), $color, $FONTS.'/'.rand(1,7).'.ttf',$C);
imagettftext($image, $size, 0, 80,  25+rand(0,10), $color, $FONTS.'/'.rand(1,7).'.ttf',$D);
imagettftext($image, $size, 0, 105, 25+rand(0,10), $color, $FONTS.'/'.rand(1,7).'.ttf',$E);
imagettftext($image, $size, 0, 130, 25+rand(0,10), $color, $FONTS.'/'.rand(1,7).'.ttf',$F);
 
// Header für JPEG-Bild ausgeben
header("Content-type: image/jpeg");
 
// Bild ausgeben und im Verzeichnis abspeichern, Dateiname aus den Zufallszahlen bilden
imagejpeg($image,$DIR.'/'.$_GET['hash'].$A.$B.$C.$D.$E.$F,90);
 
// Speicher vom Bild wieder freigeben, besser ist das ;-)
imagedestroy($image);

FAQ

Du schreibst es müssen Variablen geprüft werden. Warum denn? Was ist so gefährlich daran?

Es gibt im oberen Script 2 offensichtliche Lücken. Zum einen die Abfrage

file_exists($DIR.'/'.$_POST['hash'].$_POST['loesung']

stell Dir vor jemand übergibt Deinem Script folgende Variablen:

$_POST['hash'] = "../etc/" und $_POST['loesung']="passwd"

Sobald es die Datei /etc/passwd gibt ist das Captcha somit gelöst. Auch kann der Angreifer damit die Existenz bestimmter Dateien auf Deinem System testen. Zum anderen, die gefährlichere Lücke, ist

imagejpeg($image,$DIR.'/'.$_GET['hash'].$A.$B.$C.$D.$E.$F,90);

Die Gefahr geht von $_GET[‚hash‘] aus, also wenn wir jetzt mal $A.$B.$C.$D.$E.$F vernachlässigen, da braucht der Angreifer nur $_GET[‚hash‘] entsprechend anpassen und er kann damit Dateien auf Deinem System erzeugen und ggf. sogar überschreiben. Also beispielsweise (wie gesagt $A.$B.$C.$D.$E.$F wird vernachlässigt) $_GET[‚hash‘] = „../etc/passwd“

Und wie prüfe ich die Variablen?

Folgende Zeile prüft ob $_GET[‚hash‘] eine gültige MD5-Summe ist, ansonsten wird eine neue erzeugt.

$_GET['hash']=((strlen($_GET['hash'])==32) && (preg_match('/^[a-f0-9]+$/', $_GET['hash']))) ? $_GET['hash'] : md5(time());

oder

$_POST['hash']=((strlen($_POST['hash'])==32) && (preg_match('/^[a-f0-9]+$/', $_POST['hash']))) ? $_POST['hash'] : md5(time());

Für $_POST[‚loesung‘] sieht das so aus, ansonsten wird auch ein Zufallswert erzeugt.

$_POST['loesung']=((strlen($_POST['loesung'])==6) && (preg_match('/^[2-9]+$/', $_POST['loesung']))) ? $_POST['loesung'] : md5(time());

Warum baust Du das dann nicht gleich oben in das Script ein?

Du sollst es verstehen und nicht nur den Code mit Copy & Paste übernehmen, sonst bringt das nichts.

Bei mir geht das irgendwie nicht.

Du brauchst PHP mit der GD-Bibliothek. Die meisten Provider sollten das anbieten. Das obige Beispiel setzt Version 2.0.1 oder höher vorraus. Auf älteren Versionen sollte es ausreichen die Funktion imagecreatetruecolor() durch imagecreate() auszutauschen. Welche Version Du installiert hast kannst Du mit der Funktion phpinfo() herrausfinden.

Es funktioniert nicht. Wie finde ich den Fehler?

Prüfe zuerst ob alle Variablen wie erwartet gesetzt sind. In foo.php kannst Du Dir die Variablen direkt ausgeben lassen.

echo 'hash = '.$hash;

In bar.php mußt Du zum Debuggen die Funktion header() auskommentieren (besser auch noch imagejpeg()), sonst wird die Ausgabe als „Bild“ interpretiert und somit nicht im Browser angezeigt.

// header("Content-type: image/jpeg");
// imagejpeg($image,$DIR.'/'.$_GET['hash'].$A.$B.$C.$D.$E.$F,90);
echo 'Hash = ' .$_GET['hash'];
echo '<br>';
echo 'Captcha = '.$A.$B.$C.$D.$E.$F;

Was ist sonst noch zu beachten?

Auf das Verzeichnis welches unter $DIR konfiguriert ist muß der Prozess des Webservers schreibenden Zugriff haben. Bei /tmp sollte dies im Regelfall so sein. Ansonsten dem Verzeichnis die Rechte wie folgt setzen: rwxrwxrwx (chmod 777 /verzeichnis), besser noch rwxrwxrwxt (chmod 1777 /verzeichnis)

Wie kann ich mehrfarbige Captchas realisieren?

[...]
$colors=array(
        imagecolorallocate($image,0,0,0),
        imagecolorallocate($image,200,0,0),
        imagecolorallocate($image,0,200,0),
        imagecolorallocate($image,0,0,200),
        imagecolorallocate($image,150,150,150)
);
shuffle($colors);
[...]
imagettftext($image, $size, 0, 5, 25+rand(0,10), $colors[rand(0,(count($colors)-1))], $FONTS.'/'.rand(1,7).'.ttf',$A);
[...]

Ergänzungen

alte Captcha-Dateien löschen (von Misterjack)

Misterjack aka Richard hat mir folgenden Code gesendet, welcher eingebaut im Captcha (bar.php) bei jedem Aufruf nach alten Dateien sucht (hier älter 900 Sekunden) und diese löscht.

if($dir_handle = @opendir($DIR)) {
    while(false !== ($file = readdir($dir_handle))) {
        if((!$file == ".") &&  (!$file == "..")) {
           $time = time() - filemtime($DIR."/".$file);
            if ($time > 900) {
                @unlink($DIR."/".$file);
            }
        }
    }
closedir($dir_handle);
}

Captcha-Script mit Sessions (von Rory)

Folgendes Script hat mir Rory gemailt, danke nochmal. Es unterscheidet sich zu meinem Script oben aus dem Totorial nur in dem Punkt das es keine temporären Dateien benutzt sondern die Session-Funktionalität von PHP.

foo.php

session_start();
// prüfe ob Formular gesendet wurde
if (!$_POST['loesung'])
 {
 // formular noch nicht gesendet, erzeuge Hash, schreibe den ins Formular und binde BAR (als Bild) ein, übergib BAR den Hash
 echo '<form action="'.$_SERVER['PHP_SELF'].'" method="post">';
 //session-identifikation in ein hidden-feld laden, damit der Server weiss zu welcher session der user gehört (ersatz für hash)
 echo '<p><input type="hidden" name="' . session_name() . '" value="' . session_id() . '"></p>';
 //Session-id (SID) an das zweite script übergeben, siehe oben.
 echo '<p><img src="BAR.php?'.SID.'" alt="captcha" /></p>';
 echo '<p>Gib die Lösung ein:<br /><input type="text" name="loesung" size="6" /></p>';
 echo '</form>';
 }
else
 {
 // formular gesendet, prüfe ob datei existiert, wenn dann löschen und dann statusmeldung
 if ($_SESSION['loesung'] == $_POST['loesung'])
 {
 echo '<p>Richtig gelöst</p>';
 }
 else
 {
 echo '<p>Falsch gelöst</p>';
 }
 }

bar.php

@session_start();
 
// Verzeichnis wo die TTF-Schriften liegen
// die müssen durchnumeriert sein, in dem Beispiel 1.ttf - 7.ttf
// hier aktuelles Verzeichnis
$FONTS = ".";
 
// leeres weißes Bild erzeugen
$image = imagecreatetruecolor(155,40);
$bgColor = ImageColorAllocate($image, 255, 255, 255);
ImageFilledRectangle($image, 0, 0, 155, 40, $bgColor);
 
// Schriftfarbe
$color = imagecolorallocate($image,0,0,0);
 
// Schriftgröße
$size=20;
 
// unsere Zufallszahlen, die 1 ist nicht mit dabei, wegen Verwechslungsgefahr mit der 7
$A = rand(2,9);
$B = rand(2,9);
$C = rand(2,9);
$D = rand(2,9);
$E = rand(2,9);
$F = rand(2,9);
 
// Zahlen auf das Bild zeichnen, Position etwas variieren, zufällig eine Schriftart auswählen (1.ttf-7.ttf)
imagettftext($image, $size, 0, 5, 25+rand(0,10), $color, $FONTS.'/'.rand(1,7).'.ttf',$A);
imagettftext($image, $size, 0, 30, 25+rand(0,10), $color, $FONTS.'/'.rand(1,7).'.ttf',$B);
imagettftext($image, $size, 0, 55, 25+rand(0,10), $color, $FONTS.'/'.rand(1,7).'.ttf',$C);
imagettftext($image, $size, 0, 80, 25+rand(0,10), $color, $FONTS.'/'.rand(1,7).'.ttf',$D);
imagettftext($image, $size, 0, 105, 25+rand(0,10), $color, $FONTS.'/'.rand(1,7).'.ttf',$E);
imagettftext($image, $size, 0, 130, 25+rand(0,10), $color, $FONTS.'/'.rand(1,7).'.ttf',$F);
 
// Header für JPEG-Bild ausgeben
header("Content-type: image/gif");
 
// Bild ausgeben und im Verzeichnis abspeichern, Dateiname aus den Zufallszahlen bilden
imagegif($image);
$_SESSION['loesung']=$A.$B.$C.$D.$E.$F;
 
// Speicher vom Bild wieder freigeben, besser ist das ;-)
imagedestroy($image);

Beispielscript für MySQL (von Trulli)

Trulli aka Jan hat mir folgendes Script geschickt (Danke!) um zu demonstrieren wie man dies mit einer Mysql realisieren kann. Das Script erzeugt das Bild und trägt den Hash in eine Mysql-Datenbank ein. Ein Beispiel wie der Hash dann geprüft wird ist nicht dabei, ist aber einfach zu realisieren.

$shash = substr(md5(uniqid (rand())),0,6);
$fonts = "/ttf/";
 
header ("Content-type: image/png");
$image = @imagecreatetruecolor(300,40);
 
for ($i=0;$i<=6;$i++)
    {
        $angel = rand(-20,20);
        $pos = $i*40+20;
        $size = rand(15,25);
        $y = 25+rand(0,10);
        $color = imagecolorallocate($image, rand(100,200),rand(150,255), rand(100,255));
        imagettftext($image,$size,$angel,$pos,$y,$color,$fonts.rand(1,7).'.ttf',substr($shash,$i,1));
    }
 
imagepng($image);
imagedestroy($image);
 
include("dbcon.inc");
/*
        in dbcon.inc werden folgende Variablen gesetzt:
        $host   Datenbank Host
        $user   Datenbank User
        $pass   Datenbank Passwort
        $db     Name der Datenbank
*/
$conn = mysql_connect($host,$user,$pass) or die("Server konnte nicht erreicht werden: " .mysql_error());
mysql_select_db($db,$conn) or die("Die Datenbank wurd auf dem Server nicht gefunden:" .mysql_error());
$strSQL = "DELETE FROM cwd_captcha WHERE TO_DAYS(NOW()) - TO_DAYS(ctime) >= 1";
mysql_query($strSQL);
$strSQL = "INSERT INTO cwd_captcha (hash) VALUES ('" .$shash. "')";
mysql_query($strSQL);
mysql_close();

komplettes Captcha-Script (von Hephaistos)

Hephaistos aka Stefan hat ein Captcha-Script gebaut und mir zur Verfügung gestellt, Danke nochmal. Dieses benutzt Template-Dateien und ist schnell zu implementieren. Hier seine Kommentare zum Script:

Das Gute am Captcha Bild:

  • Zeichen sind ausgerichtet (verändere mal Bildbreite/-höhe)
  • es gibt „nur lesbare“ Zeichen
  • zufällig Farben sollten so rauskommen, dass sie gut vom Hintergrund zu unterscheiden sind unterstützt mehrere Backends (momentan implementiert: session)

Der User müsste nur:

  1. Verzeichnis „__captcha“ kopieren (noch Schriftarten in „fonts“ dazugeben)
  2. in seinem Formular das captchaimg.php als Bild und ein Captcha Textfeld einfügen
  3. somefile.php erstellen, mit den 2 zeilen aus example.php
  4. Ergebnis überprüfen > siehe result.php

Download Script von Hephaistos:

Hinweis: Das Archiv beinhaltet aus urheberrechtlichen Gründen keine Schriftarten. Diese müssen selbst besorgt und ins Verzeichnis „__captcha/fonts“ kopiert werden. Das Script verwendet dann selbstständig alle *.ttf aus diesem Verzeichnis.

tempo@deruwe.de jl@deruwe.de