Workshop: Brawler – Host scannen, Teil 5
Geschrieben am 15. Dez 2009 von Cem Derin
Zuvor:
- Workshop: Brawler – The Web Application Security Scanner, Teil 0
- Workshop: Brawler – Eine Frage der Lizenz, Teil 1
- Workshop: Brawler – Alles im Rahmen, Teil 2
- Workshop: Brawler – Not unplugged, Teil 3
- Workshop: Brawler – Der Rutengänger, Teil 4
- Workshop: Brawler – Code Review, Teil 5
Heute beginne ich mit einer der Kernfunktionen von Brawler: Das scannen eines Hosts bzw. eine Seite. Außerdem werde ich heute die Plugin-Schnittstelle implementieren, damit man Brawler direkt mit neuen Funktionen versehen werden kann. Für heute wird das lediglich das setzen eines eigenen User-Agents sein.
Das Scannen
Bevor wir einen Host scannen können, brauchen wir Klassen, die mehr oder minder einen Client simuliert. Dazu habe ich ein simples Interface geschrieben, die konkrete Klasse dann mit cURL. Interessant ist, dass die Klasse direkt ein Response-Objekt zurück gibt, das wir uns mal etwas näher anschauen:
<?php
/**
* A URL request response
*
* @package Brawler
* @subpackage Client
* @author Cem Derin,
* @copyright 2009 Cem Derin,
*/
class Brawler_Client_Response {
/**
* The raw result
*
* @var String
*/
protected $_rawResult;
/**
* The raw curl info array
*
* @var Array
*/
protected $_rawInfo;
/**
* Ctor
*
* @param String $dom
* @param Array $info
* @return void
*/
public function __construct($dom, $info) {
$this->_rawResult = $dom;
$this->_rawInfo = $info;
}
/**
* Returns a DOM representation of the returned document
* @return Brawler_Dom
*/
public function getDom() {
if(strstr($this->getContentType(), 'html')) {
return new Brawler_Dom($this->getRawContent(), $this->_rawInfo['url']);
} else {
throw new Brawler_Client_Response_Exception('Invalid content type');
}
}
/**
* Returns the content type
*
* @return String
*/
public function getContentType() {
$return = split(';', $this->_rawInfo['content_type']);
return trim($return[0]);
}
/**
* Returns the raw content (wo header)
*
* @return String
*/
public function getRawContent() {
return substr($this->_rawResult, $this->_rawInfo['header_size']);
}
/**
* Returns fetched urls
*
* @return Array
*/
public function getUrls() {
$return = array();
$nodeList = $this->getDom()->getUrls();
foreach($nodeList as $node) {
$return[] = $node->attributes->getNamedItem('href')->value;
}
return $return;
}
}
?>
Diese Klasse soll einfachen Zugriff auf die Rückgabe ermöglich, ohne das man sich von außen viel Gedanken um die komplexe Struktur dahinter machen muss. Für das reine scannen wird die Methode getUrls am interessantesten sein. Derzeit ermittelt diese lediglich Ziele von Verweisen und gibt diese zurück.
Desweiteren habe ich einen neuen Controller geschrieben: Brawler_Controller_Scan. Dieser wird mit dem Argument r aufgerufen, dem eine URL zugewiesen sein muss. Was dieser Controller macht, ist im Grunde selbsterklärend: Scannen
<?php
/**
* Scanner controller
*
* @package Brawler
* @author Cem Derin,
* @copyright 2009 Cem Derin,
*/
class Brawler_Controller_Scan extends Brawler_Controller {
/**
* Holds an array with the fetched urls
*
* @var Array
*/
protected $_urls = array();
/**
* Holds the client
*
* @var Brawler_Client
*/
protected $_client;
/**
* Index Action
*
* @return void
*/
public function indexAction() {
$this->_client = new Brawler_Client();
$url = Brawler_Console::getArgument('r')->getValue();
$this->_scanRecursively($url, 0, 5);
$grid = new Brawler_View_Grid();
$grid->setRows(new ArrayObject($this->_urls));
$this->setView($grid);
}
/**
* Recursive method
*
* @param String $url
* @param Int $level
* @param Int $maxlevel
* @return void
*/
protected function _scanRecursively($url, $level, $maxlevel) {
$urls = $this->_client->request($url)->getUrls();
$urls = array_unique($urls);
$urls = $this->_filterKnownUrls($urls);
$urls = $this->_filterForeignHostUrls($urls);
$this->_urls = array_merge($urls, $this->_urls);
if($level < $maxlevel) { foreach($urls as $newUrl) { $this->_scanRecursively($newUrl, $level+1, $maxlevel);
}
}
}
/**
* Filters already known urls
*
* @param Array $urls
* @return Array
*/
protected function _filterKnownUrls($urls) {
$return = array();
foreach($urls as $url) {
if(!in_array($url, $this->_urls)) {
$return[] = $url;
}
}
return $return;
}
/**
* Filters foreign host urls
*
* @param Array $urls
* @return Array
*/
protected function _filterForeignHostUrls($urls) {
$return = array();
$curl = Brawler_Console::getArgument('r')->getValue();
foreach($urls as $url) {
if(parse_url($url, PHP_URL_HOST) == parse_url($curl, PHP_URL_HOST)) {
$return[] = $url;
}
}
return $return;
}
}
?>
Dafür hat der Controller eine Methode, die rekursiv aufgerufen wird. Dieser wird eine URL sowie die aktuelle und die maximal gewünschte Ebene übergeben. Dann macht die Methode nichts weiter als die URL aufzurufen und dort alle Links auszulesen. Diese werden noch gefiltert – bereits bekannte und URLs zu einem Fremden Host werden entfernt – und dann wird, sofern die maximale Ebene noch nicht erreicht ist, die Methode mit den gefundenen URLs aufgerufen. Rekursiv eben. Am Ende gibt der Controller alle URLs aus, die er gefunden hat.
Die Grundfunktionalität wäre damit gelegt. Aber die ist auch alles andere als ausreichen. Spontan fallen mir eine Reihe Dinge ein, die hier noch zu tun sind:
URLs müssen auch aus Grafiken, Formularen, Frames, etc ausgelesen werden können
Nicht absolute URLs müssen umgewandelt werden
Anker müssen ignoriert werden
Die gewünschte Tiefe muss per Argument eingestellt werden können
Der User-Agent muss per Argument eingestellt werden können
und einiges mehr …
Auf das Problem mit dem User-Agent gehe ich direkt ein, und damit komme ich dann auch zu der Plugin-Schnittstelle.
Das erste echte Plugin
Damit die Basisklassen durch Plugins erweitert werden können, müssen sie sich in irgend einer Ableitung der Pluggable-Klasse befinden. Diese stellt ein paar Methoden bereit, mit denen man schnell und einfach jede Methode erweiterbar machen kann.
<?php
/**
* Abstract Pluggable class
*
* @package Brawler
* @subpackage Plugin
* @author Cem Derin,
* @copyright 2009 Cem Derin,
*/
abstract class Brawler_Plugin_Pluggable_Abstract {
/**
* Holds the plugins
*
* @var Brawler_Array_Object
*/
protected $_plugins;
/**
* Ctor – loads the plugins
*
* @return void
*/
public function __construct() {
$this->_plugins = new Brawler_Array_Object();
// fetching plugins
foreach(Brawler_Plugin_Loader::getPlugins() as $plugin) {
// get plug name
$pluginClass = $this->getPluginClassName($plugin);
if(class_exists($pluginClass)) {
$this->registerPlugin(new $pluginClass);
}
}
}
/**
* Registers a plugin
*
* @param Brawler_Plugin_Plugin $plugin
* @return void
*/
public function registerPlugin(Brawler_Plugin_Plug_Abstract $plugin) {
$this->_plugins->append($plugin);
}
/**
* Notifies the Plugins before method starts to act
*
* @param String $method
* @param Array $arguments
* @return void
*/
public function preCallNotify($method, $arguments) {
$notification = new Brawler_Plugin_Notification_Pre($method, $arguments);
return $this->_notify($notification);
}
/**
* Notifies the Plugins after a method has acted
*
* @param String $method
* @param Array $arguments
* @param mixed $return
* @return mixed
*/
public function postCallNotify($method, $arguments, $return) {
$notification = new Brawler_Plugin_Notification_Post($method, $arguments, $return);
return $this->_notify($notification);
}
/**
* General notofication
*
* @param Brawler_Plugin_Notification $notification
* @return unknown_type
*/
protected function _notify(Brawler_Plugin_Notification $notification) {
$i = $this->_plugins->getIterator();
while($i->valid()) {
$i->current()->notify($notification);
$i->next();
}
return $notification;
}
/**
* Returns the name of the plugin class depending on a given plugin
*
* @param Brawler_Plugin $plugin
* @return String
*/
public function getPluginClassName($plugin) {
$return = split('_', get_class($this));
array_shift($return);
return $plugin->getBaseName(). '_'. implode('_', $return);
}
}
?>
Wir sehen, in den Ableitungen darf nicht vergessen werden, den Konstruktur der Eltern-Klasse aufzurufen – aber das sollte ja ohnehin immer gemacht werden. Dort werden nämlich die Plugins geladen und registriert. Wird also der Eltern-Konstruktor nicht aufgerufen, existieren zum einen keine Plugins, zum anderen kommt es aber auch zu einem Fehler, weil das Array Objekt gar nicht existiert (und später abgefrat wird).
Die nächsten Methoden machen klar, es gibt zwei Arten von Benachrichtigungen: Pre und Post. Die Namen sind denke ich selbsterklärend: Die eine wird vor dem durchführen der zu erweiternden Methode verschickt, und die anderen eben danach. Allerdings hat letztere noch die bereits prozessierte Rückgabe im Gepäck, woran sich die Plugins nun auch noch einmal vergreifen dürfen.
Die Plugins selbst müssen sich von der Plug-Klasse ableiten:
<?php
/**
* Abstract Plug class
*
* @package Brawler
* @subpackage Plugin
* @author Cem Derin,
* @copyright 2009 Cem Derin,
*/
class Brawler_Plugin_Plug_Abstract {
/**
* Reveives the notification
*
* @param Brawler_Plugin_Notification $notification
* @return Brawler_Plugin_Notification
*/
public function notify(Brawler_Plugin_Notification $notification) {
if(method_exists($this, $notification->getMethod())) {
call_user_func(array($this, $notification->getMethod()), $notification);
}
}
}
?>
Die ist (noch?) angenehm übersichtlich. Beim Notify wird geschaut, ob die entsprechende Methode überhaupt erweitert werden soll und wenn ja, wird diese aufgerufen.
So wird das Notification-Objekt einmal durch die ganze Bagage gereicht. Derzeit wird noch nichts damit gemacht (weil wir in unserem konkreten Fall lediglich an die cURL-Resource wollen), geplant ist aber, dass die Parameter im PreCall-Notify und die Rückgabe im PostCall-Notify modifiziert werden können, welche dann in der zu erweiternden Methode ausgewertet bzw. ausgegeben werden.
Dazu kommt später auch noch eine kleine Gewichtung der Plugins (,low‘, ,med‘, ,high‘), mit denen diese die Reihenfolge ein wenig beeinflussen können, denn – was auch noch geplant ist – ist die Möglichkeit für ein Plugin zu sagen, dass jede weitere Notification unterbunden werden soll. Das bedeutet im Umkehrschluss zwar, dass bestimmte Plugins in bestimmten Konstellationen sich gegenseitig unbrauchbar machen, bzw. stören, aber so weit muss es ja auch erst mal kommen
Das konkrete Plugin zum ändern des User Agents sieht dann übrigens so aus
<?php
/**
* Just a template plugin
*
* @package Brawler
* @subpackage Plugin_UserAgent
* @author Cem Derin,
* @copyright 2009 Cem Derin,
*/
class Brawler_Plugin_UserAgent_UserAgent extends Brawler_Plugin {
/**
* (non-PHPdoc)
* @see trunk/src/Brawler/Brawler_Plugin#getArguments()
*/
public function getArguments() {
$list = new Brawler_Plugin_Argument_List();
$list->append(new Brawler_Plugin_Argument(
'a',
'Sets the submitted user agent',
true
));
return $list;
}
}
?>
beziehungsweise
<?php
/**
* Client plugin to make user agent settable
*
* @package Brawler
* @subpackage Plugin_UserAgent
* @author Cem Derin,
* @copyright 2009 Cem Derin,
*/
class Brawler_Plugin_UserAgent_Client extends Brawler_Plugin_Plug_Abstract {
public function _initCurl($notification) {
if($notification instanceof Brawler_Plugin_Notification_Post) {
if(Brawler_Console::getArgument('a')->getValue()) {
curl_setopt(
$notification->getReturn(),
CURLOPT_USERAGENT,
Brawler_Console::getArgument('a')->getValue()
);
}
}
}
}
?>
Eigentlich also ganz einfach
Sehen sie demnächst …
Beim nächsten mal werde ich mich dem auslesen von weiteren Adresse aus einem Dokument widmen bzw. die ermittelten Adressen robuster gestalten um auch die Host-Erkennung zu optimieren. Das wird glaube ich auch noch ein bisschen kniffelig …
#001
Sehr schöner Beitrag, zeigt das du was drauf hast