// der php hacker

// archiv

Workshop: Brawler – Host scannen, Teil 5

Geschrieben am 15. Dez 2009 von Cem Derin

Zuvor:

  1. Workshop: Brawler – The Web Application Security Scanner, Teil 0
  2. Workshop: Brawler – Eine Frage der Lizenz, Teil 1
  3. Workshop: Brawler – Alles im Rahmen, Teil 2
  4. Workshop: Brawler – Not unplugged, Teil 3
  5. Workshop: Brawler – Der Rutengänger, Teil 4
  6. 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(/2009/12/15/workshop_brawler_host_scannen_teil_5/notification/index.html);
		}

		/**
		 * 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(/2009/12/15/workshop_brawler_host_scannen_teil_5/notification/index.html);
		}

		/**
		 * 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(/2009/12/15/workshop_brawler_host_scannen_teil_5/notification/index.html);
				$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(/2009/12/15/workshop_brawler_host_scannen_teil_5/notification/index.html) {
			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
16. Dez 2009
Felix F.

Sehr schöner Beitrag, zeigt das du was drauf hast ;)


#002
18. Dez 2009
onemorenerd

In Brawler_Client_Response::getDom solltest du statt strstr() besser strpos() verwenden. An der Stelle soll schließlich nur geprüft werden, ob ein String in einem anderen vorkommt. strstr() braucht ggf. mehr Speicher und ist langsamer als strpos().

Im phpDoc Comment der Methode getDom fehlt @throws oder @exception. Verwendest du diese Tags grundsätzlich nicht? Ich finde sie sehr nützlich, weil ich dann weiß, ob ich einen Aufruf überhaupt in try-catch packen muss und weil meine IDE den Type Hint im catch automatisch setzen kann.

In Brawler_Client_Response::getContentType verwendest du split(‘;’, …). Das ist aus zwei Gründen nicht gut: Erstens wirft diese Funktion ab PHP 5.3.0 ein E_DEPRECATED Warning und zweitens – ich zitiere einfach mal das PHP Manual: “Wenn Sie die Fähigkeiten regulärer Ausdrücke nicht benötigen, ist die Verwendung von explode() schneller, weil diese Funktion nicht unter der Last der Engine für reguläre Ausdrücke steht.”
In Brawler_Plugin_Pluggable_Abstract::getPluginClassName verwendest du split() abermals ohne echten regulären Ausdruck. Aber hier würde ich nicht explode() empfehlen sondern
public function getPluginClassName($plugin) {
$class = get_class($this);
return $plugin->getBaseName() .’_’. substr($class, strpos($class, ‘_’));
}

In Brawler_Plugin_UserAgent_Client fällt mir “public function _initCurl” auf. Der _ deutet doch normalerweise an, dass die Methode private oder protected ist. Hier ist sie public und hat trotzdem einen _?
Warum überhaupt noch _ verwenden? Das ist doch ein Relikt aus PHP 4, wo es noch keine Visibility gab.

Das war jetzt ziemlich viel Mikrooptimierung. Aber das solltest du positiv sehen: Ich konnte einfach keine Big Smells finden. Weiter so!


#003
18. Dez 2009
Cem Derin

Bzgl. Split() – werde ich ändern.

Bzgl. Exceptions: @throws habe ich bisher noch nie verwendet aber zumindest schon mal in Betracht gezogen.

Das _initCurl Public ist, ist ein Fehler, da habe ich mich schlicht vertan. Den unterstrich muss ich mir noch abgewöhnen, meine IDE hat bis vor kurzem visibility nicht ausgewertet und auch nicht offentliche Methoden angeboten, wo diese nicht sichtbar waren.

Vielen dank für deine Hinweise, ich sehe da nichts schlechtes dran: ich werde auf Dinge aufmerksam gemacht, die ich sonst selbst vielleicht gar nicht (mehr) sehe. Und ich bitte ja auch explizit um Verbesserungsvorschläge :)

… Wenn du willst trage ich dich auch als comitter ein, dann kannst du das selbst machen ;) obwohl: so bekommen die Leser hier auch was davon mit.

Edit: _initCurl hatte ich bereits korrigiert und commitet, wie ich eben festgestellt habe ;) http://code.google.com/p/brawler/source/browse/trunk/src/Brawler/Client.php?spec=svn12&r=12

// kommentieren

// senden
theme von mir, software von wordpress, grid von 960 grid system. funktioniert in allen browsern, aber der safari bekommt das mit der schrift am schönsten hin.