Einfacher vulnerability check für den "Apache Killer"-Exploit

Veröffentlicht als Einblick
von Joschi Kuphalam

Soeben wurde unter dem Titel "Apache Killer" eine Schwachstelle des populären Apache-Webservers bekannt und veröffentlicht (z.B. unter golem.de), die ganz akkut den Betrieb vieler Internetseiten gefährden bzw. ganze Server vorübergehend außer Gefecht setzen kann. Zwar arbeiten die Apache-Entwickler mit Hochdruck an einer schnellen Lösung (die noch innerhalb der nächsten 48 Stunden fertiggestellt sein soll), dennoch empfiehlt es sich für gewissenhafte Server-Administratoren, bereits vorher zu reagieren und ihre Server zumindest auf deren Verwundbarkeit für diesen 0-Day-Exploit hin zu untersuchen.

Das besonders gemeine am "Apache Killer" finde ich: Geht man mal davon aus, dass im typischen Hosting-Betrieb mit sog. "Virtuellen Hosts" gearbeitet wird – also viele Internetpräsentationen bzw. Accounts teilen sich einen physikalischen Server – ist es fatal, dass eine einzige angreifbare bzw. nicht ausreichend geschützte Internetpräsentation den kompletten Server in den Abgrund reißen kann.

Gottseidank gibt es eine Reihe von Behelfsmaßnahmen, mit denen man seinen Server kurzfristig absichern kann (Google-Suche). Unter den verschiedenen Ansätzen scheint es mir – zumindest in unserem Fall – am praktikabelsten, unsere Internetseiten über das Apache-Modul mod_rewrite zu schützen.

Es ist nicht unbedingt gesagt, dass jede Internetseite automatisch angreifbar ist. Um festzustellen, ob und welche unserer Internetpräsentation überhaupt betroffen sind, habe ich uns schnell ein eigenes kleines Prüfskript in PHP geschrieben, weil mir die bereits kursierenden Skripte zu ungenau arbeiten. So prüft beispielsweise dieses alternative Skript lediglich, ob auf einen HEAD-Aufruf mit Range-Header hin der "206 Partial Content"-HTTP-Status zurückgeliefert wird. Dies kann zwar ein Hinweis auf eine potenzielle Verwundbarkeit sein, aber:

  • Auch per GET-Aufruf kann der Server lahm gelegt werden, und meine Untersuchungen haben gezeigt, dass für GET und HEAD (der ja ohnehin eher unüblich ist) bisweilen unterschiedliche Ergebnisse für ein und dieselbe Internetseite geliefert werden. Sich auf eine reine HEAD-Prüfung zu verlassen halte ich für riskant.
  • Auch nach dem Sichern des Servers per mod_rewrite (und zwar mit der Methode, bei der der Range-Header nicht komplett verboten, sondern nur auf eine bestimmte Maximalanzahl an Ranges beschränkt wird) wird der "206 Partial Content"-HTTP-Status zurückgeliefert, sofern nicht ein illegaler Range-Header geschickt wird. Die kursierenden Prüfskripte werden derart gesicherten Seiten also dennoch Verwundbarkeit attestieren.

Ich hab also schnell ein Prüfskript geschrieben, das folgendes tut:

  • Es werden insgesamt vier Abfragen ausgeführt, zwei per HEAD, zwei per GET.
  • Zunächst werden "einfache" Anfragen mir einem Range-Header mit nur einer Range gestellt. Die Antwort auf diese Anfragen zeigt, ob die geprüfte Präsentation grundsätzlich überhaupt anfällig wäre.
  • Anschließend werden Range-Header mit 6 Ranges geschickt. Wurde der Server mit der mod_rewrite-Methode abgesichert, so sollten diese Anfragen blockiert werden (403-Status).
  • Sofern eine Internetpräsentation als generell verwundbar eingestuft wurde, wird am Ende des Skriptablaufs beispielhafter .htaccess-Code gezeigt, mit dem man per mod_rewrite Abhilfe schaffen kann.

Ich veröffentliche mein Quick'n'Dirty-Skript an dieser Stelle "as is", vielleicht kann es ja dem einen oder anderen hilfreich sein. Es erhebt keinerlei Anspruch auf Vollständigkeit, und schon gleich gar nicht kann ich garantieren, dass der Einsatz für irgendwen Sinn macht oder nicht gar irgendetwas schlimmes passiert (das Skript könnte sich z.B. selbst entzünden). Ich verwende das Skript auf der Kommandozeile (CLI-SAPI), aber mit ein paar Handgriffen ließe es sich sicher auch für den Aufruf im Browser umschreiben ... Viel Spaß damit.

<?php
// Command line argument check
if ($argc == 1) {
	die("Usage: ".$argv[0]." host <port>\n");
}
$host						= $argv[1];
$port						= ($argc > 2) ? intval($argv[2]) : 80;
echo "Checking $host:$port... \n\n";
// Defining different checks
$checks						= array(
	'HEAD (Single range)'	=> "HEAD / HTTP/1.1\r\nHost: $host\r\nRange:bytes=0-4\r\nAccept-Encoding: gzip\r\nConnection: close\r\n\r\n",
	'GET (Single range)'	=> "GET / HTTP/1.1\r\nHost: $host\r\nRange:bytes=0-4\r\nAccept-Encoding: gzip\r\nConnection: close\r\n\r\n",
	'HEAD (6 ranges)'		=> "HEAD / HTTP/1.1\r\nHost: $host\r\nRange:bytes=0-,5-0,5-1,5-2,5-3,5-4\r\nAccept-Encoding: gzip\r\nConnection: close\r\n\r\n",
	'GET (6 ranges)'		=> "GET / HTTP/1.1\r\nHost: $host\r\nRange:bytes=0-,5-0,5-1,5-2,5-3,5-4\r\nAccept-Encoding: gzip\r\nConnection: close\r\n\r\n",
);
$vulnerabilities			= 0;
$check						= 0;
// Running checks
foreach ($checks as $id => $request) {
	echo str_pad('Running check: '.$id.' ...', 40, ' ');
	$status					= null;
	$vulnerable				= false;
	// Creating socket connection ...
	$socket					= socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
	if ($socket === false) {
		echo "Could not create socket to $host:$port\n";
		continue;
	} else {
		socket_connect($socket, $host, $port);
	}
	socket_write($socket, $request, strlen($request));
	while ($out = socket_read($socket, 2048)) {
		if ($status === null) {
			$status			= preg_match("%^HTTP/\d\.\d\s(\d+)%", $out, $status) ? $status[1] : '???';
		}
		if (!$vulnerable && (stripos($out, "partial") !== false)) {
			$vulnerable		= true;
		}
	}
	socket_close($socket);
	echo ($vulnerable ? '[!!] possibly vulnerable' : '[**] possibly not vulnerable \o/')." (HTTP status: $status)\n";
	$vulnerabilities		|= ($vulnerable ? pow(2, $check) : 0);
	++$check;
}
echo "\n";
// If the server seems to be generally vulnerable
if ($vulnerabilities & 3) {
	echo "In general, your server seems to be vulnerable.\n";
	if (!($vulnerabilities & 12)) {
		echo "However, it seems like it has already been patched against the exploit (at least partially)!\n";
	}
	echo "\nYou can protect your server by using mod_rewrite:\n\n# \"Apache Killer\" 0-day Exploit (http://seclists.org/fulldisclosure/2011/Aug/175)\nRewriteEngine On\nRewriteCond %{HTTP:range} !(^bytes=[^,]+(,[^,]+){0,4}$|^$)\nRewriteRule .* - [F]\n\n";
} else {
	echo "Congratulations! Your server doesn't seem to be vulnerable.\n";
}
?>