WordPress, concrete5, ZF2, PHP, mysql database dump

March 22nd, 2017

Here is a very simple script that once uploaded locks onto various architectures and extracts the database using the captured details.

For example, wordpress and concrete5 have been detected here:

Screenshot from 2017-03-22 11-18-40

When nothing is detected it will still offer the manual entering of the username and password.

Screenshot from 2017-03-22 11-18-47

It currently supports Concrete5 and WordPress, but I can easily upgrade it to support many more like ZF1, ZF2, Magento, Drupal, Joomla, etc, if there is any interest.

This is handy when you need to quickly copy a database, you simply upload this script to the doc_root and the script will attempt to handle the rest for you.

The script comes with password protection to prevent unauthorised usage, or it will not just be you downloading the database.

Screenshot from 2017-03-22 12-28-36

The script stores the username and password of those authorised to use it within the code, the password can be entered in plain text, or you can define your own algorithm and store the password encrypted in this way, within the code.

The script can be downloaded from github here and the full source code is below.

/*
 
 
Adrian Callaghan 21 Mar 2017 
 
Very lightweight database dumper that automatically locks access details provided by other PHP frameworks
 
 
 
****** How to use ******
 
Enter your usernames and passwords into the constructor in either plain text (insecure if read by someone else) or encypted
 
 
 
Example 1: allowing bob access with the password 1234 and joe access with password 5678 - no encryption 
 
	dbDump::Init(array(
		'users'	=> array(
			array('username'=>'bob','password'=>'1234'),
			array('username'=>'joe','password'=>'5678'),
		)));
 
 
 
Example 2: allowing bob access with the password 1234 and joe access with password 5678 - with encyption method and hashed passwords that must match the method result
 
	dbDump::Init(array(
		'users'	=> array(
			array('username'=>'bob','password'=>'81dc9bdb52d04dc20036dbd8313ed055'),
			array('username'=>'joe','password'=>'674f3c2c1a8a6f90461e8a66fb5550ba'),
		),
		'passwordEncryption'=>function($pass){
			return md5($pass);
		}
	));
 
*/
 
 
 
 
/******************
 
Constructor
 
*******************/
dbDump::Init(array(
	'users'	=> array(
		array('username'=>'bob','password'=>'81dc9bdb52d04dc20036dbd8313ed055'),
		array('username'=>'joe','password'=>'674f3c2c1a8a6f90461e8a66fb5550ba'),
	),
	'passwordEncryption'=>function($pass){
		return md5($pass);
	}
));
 
 
 
 
 
 
 
/******************
 
Class starts...
 
*******************/
final class dbDump{
 
	const APP_STATE = 'state';
	const APP_AUTH 	= 'MY_TOKEN';
	const APP_SALT  = 'MY_SALT';
 
	private $_users;
	private $_passwordEncryption;
 
	protected function destroySession(){
		$this->initSession();
		session_destroy();
		return $this;
	}
 
	protected function initSession(){
		if (session_status() == PHP_SESSION_NONE) {
		    session_start();
		}
 
		return $this;
	}
 
	protected function getSession($object = true){
		$this->initSession();
		return $object ? (object) $_SESSION : $_SESSION;
    }
 
   	protected function getSessionVar($var = ''){
   		return isset($this->session->{$var}) ? $this->session->{$var} : false;
    }
 
    protected function setSessionVar($key, $val){
    	$session 			= $this->session;
    	$session->{$key} 	= $val;
    	$this->session = (array) $session;
    }
 
    protected function setSession(array $values){
    	$this->initSession();
    	$_SESSION = $values;
    	return $this;
    }
 
	protected function getGet($object = true){
		return $object ? (object) $_GET : $_GET;
	}
 
	protected function getGetVar($var = ''){
   		return isset($this->post->{$var}) ? $this->post->{$var} : false;
    }
 
    protected function isPost(){
    	return empty($this->getPost(false)) ? false : true;
    }
 
	protected function getPost($object = true){
		return $object ? (object) $_POST : $_POST;
	}
 
	protected function getPostVar($var = ''){
   		return isset($this->post->{$var}) ? $this->post->{$var} : false;
    }
 
	protected function getState(){
		return $this->getSessionVar(self::APP_STATE);
	}
 
	protected function setState($state = ''){
		$this->setSessionVar(self::APP_STATE, $state);
		return $this;
	}
 
	protected function is_constant($token) {
	    return $token == T_CONSTANT_ENCAPSED_STRING || $token == T_STRING || $token == T_LNUMBER || $token == T_DNUMBER;
	}
 
	protected function strip($value) {
	    return preg_replace('!^([\'"])(.*)\1$!', '$2', $value);
	}
 
	protected function getDefinitions($php){
 
		$defines 	= array();
		$state 		= 0;
		$key 		= '';
		$value 		= '';
		$tokens 	= token_get_all($php);
		$token 		= reset($tokens);
		while ($token) {
		//    dump($state, $token);
		    if (is_array($token)) {
		        if ($token[0] == T_WHITESPACE || $token[0] == T_COMMENT || $token[0] == T_DOC_COMMENT) {
		            // do nothing
		        } else if ($token[0] == T_STRING && strtolower($token[1]) == 'define') {
		            $state = 1;
		        } else if ($state == 2 && $this->is_constant($token[0])) {
		            $key = $token[1];
		            $state = 3;
		        } else if ($state == 4 && $this->is_constant($token[0])) {
		            $value = $token[1];
		            $state = 5;
		        }
		    } else {
		        $symbol = trim($token);
		        if ($symbol == '(' && $state == 1) {
		            $state = 2;
		        } else if ($symbol == ',' && $state == 3) {
		            $state = 4;
		        } else if ($symbol == ')' && $state == 5) {
		            $defines[$this->strip($key)] = $this->strip($value);
		            $state = 0;
		        }
		    }
		    $token = next($tokens);
		}
 
		return $defines;
 
	}
 
	protected function generateDbForm($fields = null, $forceDisplay = false){
 
		if ($fields===null && !$forceDisplay){
			return;
		}
 
		$form = "<form method='post' name='dbForm' class='form-horizontal col-vert-20' role='form'>";
 
		$form.= "<div class='form-group'><label class='col-sm-2&#x20;control-label'>Host</label><div class='col-sm-10'><input name='host' type='text' placeholder='Enter host name' required='required' class='form-control' value='".(isset($fields->host) ? $fields->host : '')."'></div></div>";
 
		$form.= "<div class='form-group'><label class='col-sm-2&#x20;control-label'>Table</label><div class='col-sm-10'><input name='table' type='text' placeholder='Enter table name' required='required' class='form-control' value='".(isset($fields->table) ? $fields->table : '')."'></div></div>";
 
		$form.= "<div class='form-group'><label class='col-sm-2&#x20;control-label'>User</label><div class='col-sm-10'><input name='username' type='text' placeholder='Enter user name' required='required' class='form-control' value='".(isset($fields->username) ? $fields->username : '')."'></div></div>";
 
		$form.= "<div class='form-group'><label class='col-sm-2&#x20;control-label'>Password</label><div class='col-sm-10'><input name='password' type='text' placeholder='Enter password' required='required' class='form-control' value='".(isset($fields->password) ? $fields->password : '')."'></div></div>";
 
		$form.= "<div class='form-group'><div class='col-sm-12'><button type='submit' name='button-submit' class='btn&#x20;btn-success btn-lg pull-right' value=''>
		<span class='glyphicon glyphicon-download'></span>&nbsp;Download</button></div></div>";
 
 
		$form.= "</form>";
 
 
		return $form;
	}
 
	protected function getPlatform($options){
 
		$error          = '<div class="pull-right text-warning"><span class="glyphicon glyphicon-warning-sign status"></span></div>';
		$notFound       = '<div class="pull-right text-danger"><span class="glyphicon glyphicon-remove status"></span></div>';
		$found          = '<div class="pull-right text-success"><span class="glyphicon glyphicon-ok status"></span></div>';
		$out			= '<li class="list-group-item">';
		$logoW			= isset($options->logoW) ? $options->logoW : '213';
		$logoH          = isset($options->logoH) ? $options->logoH : '120';
		$logo 			= "<img src='".(isset($options->logo) ? $options->logo : "holder.js/{$logoW}x{$logoH}")."' height='{$logoH}' width='{$logoW}' alt='platform logo' longdesc='logo representing the platform option' />";
		$dbArgs 		= new StdClass(); 
 
		if (isset($options->conf) && file_exists($options->conf)){
			if (($fp 	= fopen($options->conf, "r"))!==false){
				$params = (object) $this->getDefinitions(stream_get_contents($fp)); 
				foreach(array('host','table','username','password') AS $option){
						$dbArgs->{$option} = isset($options->{$option}) && isset($params->{$options->{$option}}) ? $params->{$options->{$option}} : '';
				}				
	      		$out	.= $found.$logo.$this->generateDbForm($dbArgs, isset($options->force) ? $options->force : null);
	      		fclose($fp);
	      	} else {
	      		$out	.= $error.$logo.$this->generateDbForm(null, isset($options->force) ? $options->force : null);
	      	}
		} else {
			$out		.= $notFound.$logo.$this->generateDbForm(null, isset($options->force) ? $options->force : null);
		}
		$out.= '</li>';
 
		return $out;
 
	}
 
	protected function getPlatforms(){
 
		$out 			= '<ul class="list-group">';
		$out			.= $this->getPlatform((object) array(
			'conf'		=> 'wp-config.php',
			'logo'		=> 'https://s.w.org/about/images/logos/wordpress-logo-32-blue.png',
			'logoW'		=> '32',
			'logoH'		=> '32',
			'host'		=> 'DB_HOST',
	      	'table'		=> 'DB_NAME',
	      	'username'	=> 'DB_USER',
	      	'password' 	=> 'DB_PASSWORD'
			));
 
		$out			.= $this->getPlatform((object) array(
			'conf'		=> 'config/site.php',
			'logo'		=> 'https://www.concrete5.org/files/3613/5517/8150/concrete5_Wordmark_200x37.png',
			'logoW'		=> '200',
			'logoH'		=> '37',
			'host'		=> 'DB_SERVER',
	      	'table'		=> 'DB_DATABASE',
	      	'username'	=> 'DB_USERNAME',
	      	'password' 	=> 'DB_PASSWORD'
			));
 
		/*$out			.= $this->getPlatform((object) array(
			'conf'		=> 'config/autoload/local.php',
			'logo'		=> 'http://clloh.com/wp-content/uploads/2015/08/zf2-logo-128x128.png',
			'logoW'		=> '128',
			'logoH'		=> '128',
			'host'		=> 'DB_SERVER',
	      	'table'		=> 'DB_DATABASE',
	      	'username'	=> 'DB_USERNAME',
	      	'password' 	=> 'DB_PASSWORD'
			));*/
 
		$out			.= $this->getPlatform((object) array(
			'logo'		=> 'https://www.mysql.com/common/logos/logo-mysql-170x115.png',
			'logoW'		=> '170',
			'logoH'		=> '115',
			'force'		=> true,
			));
 
 
		return $out.'</ul>';
	}
 
	protected function getHeader(){
		return '<html><head><title>Restricted area</title><link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"><script src="https://code.jquery.com/jquery-3.2.1.js" integrity="sha256-DZAnKJ/6XZ9si04Hgrsxu/8s717jcIzLy3oi35EouyE=" crossorigin="anonymous"></script><script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/holder/2.9.4/holder.js"></script><META NAME="ROBOTS" CONTENT="NOINDEX, NOFOLLOW"><style>.col-vert-20{margin-top:20px;}.col-vert-100{margin-top:100px;}.status{font-size:30px;}</style></head><body><div class="container-fluid"><div class="row">';
	}
 
	protected function getFooter(){
		$footer = '</div></div>';
 
		if ($this->authentication) {
 
			$footer .= '<div class="modal fade" id="confirmationModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
				  <div class="modal-dialog modal-sm" role="document">
				    <div class="modal-content">
				      <div class="modal-header">
				        <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
				        <h4 class="modal-title" id="myModalLabel"><span class="glyphicon glyphicon-lock"></span>&nbsp;Log out request.</h4>
				      </div>
				      <div class="modal-body">
				        Are you sure you wish to logout '.$this->authentication->username.'?
				      </div>
				      <div class="modal-footer">
				        <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
				        <a type="button" class="btn btn-primary" href="?logout=true">Confirm</a>
				      </div>
				    </div>
				  </div>
				</div>
				</body></html>';
		}
 
		return $footer;
	}
 
	protected function loginForm($inErrorState = false){
		return '<div class="col-md-6 well well-large col-md-offset-3 col-vert-100"><h3><span class="glyphicon glyphicon-lock"></span> '.($inErrorState ? 'Access Denied' : 'Secure Area' ).'</h3><form method="post" name="login" class="form-horizontal col-vert-20" role="form"><div class="form-group '.($inErrorState ? 'has-error' : '').'"><label class="col-sm-2&#x20;control-label">Username</label><div class=" col-sm-10"><input name="username" type="text" placeholder="Enter&#x20;username" required="required" class="form-control" value="">'.($inErrorState ? '<ul class="help-block"><li>Invalid username &amp; password combination</li></ul>' : '').'</div></div><div class="form-group '.($inErrorState ? 'has-error' : '').'"><label class="col-sm-2&#x20;control-label">Password</label><div class=" col-sm-10"><input name="password" type="password" required="required" placeholder="Password" class="form-control" value="">'.($inErrorState ? '<ul class="help-block"><li>Invalid username &amp; password combination</li></ul>' : '').'</div></div><div class="form-group "><div class=" col-sm-10 col-sm-offset-2"><button type="submit" name="button-submit" class="btn&#x20;btn-default" value="">Login</button></div></div></form></div>';
	}
 
	protected function getDownloadOptions($inErrorState = false){ 
		return '<div class="col-md-6 col-md-offset-3 col-vert-100"><div class="panel panel-primary"><div class="panel-heading"><span class="glyphicon glyphicon-wrench"></span>&nbsp;Export options<button class="pull-right btn btn-danger btn-xs" data-toggle="modal" data-target="#confirmationModal">'.$this->authentication->username.'&nbsp;<span class="glyphicon glyphicon-remove-circle"></span></button></div>'.$this->platforms.'</div></div>'; 
	}
 
	protected function setUsers(array $users = null){
        if ($users==null){
            $users = array();
        }
        foreach($users AS $key=>$user){
        	$users[$key] = (object) $user;
        }
        $this->_users = (object) $users;
        return $this;
    }
 
    protected function getUsers(){
 
        if (!isset($this->_users)){
            $this->setUsers();
        }
        return $this->_users;
    }
 
	protected function setPasswordEncryption($function = null){
        if ($function==null){
            $function = function($val){return $val;};
        }
        $this->_passwordEncryption = $function;
        return $this;
    }
 
    protected function getPasswordEncryption(){
 
        if (!isset($this->_passwordEncryption)){
            $this->setPasswordEncryption();
        }
        return $this->_passwordEncryption;
    }
 
    protected function PasswordEncrypt($pass = ''){
 
    	$encryptor 	= $this->getPasswordEncryption();
    	return call_user_func($encryptor, $pass);
 
    }
 
    protected function generateUserToken(\StdClass $user){
    	return md5((isset($user->username) ? $user->username : uniqid()).self::APP_SALT.(isset($user->password) ? $user->password : uniqid()));
    }
 
    protected function setAuthentication(\StdClass $user){
    	$this->setSessionVar(self::APP_AUTH, $this->generateUserToken($user));
    	return $this;
    }
 
    protected function getAuthentication(){
    	$token 	= $this->getSessionVar(self::APP_AUTH);
    	foreach($this->users AS $validUser){
    		$validToken = $this->generateUserToken($validUser);
    		if ($token==$validToken){
    			return $validUser;
    		}
    	}
 
    }
 
    protected function authenticateUser(\StdClass $user){
 
		foreach($this->users AS $validUser){
 
			if ($user->username==$validUser->username && $this->passwordEncrypt($user->password)==$validUser->password){
				$this->authentication 	= $validUser;
				$this->state 			= 'options';
				return $this->authentication;
			}
		}
 
    }
 
	public static function Init(array $options = array()){
 
		static $inst 	= null;
        if ($inst 		=== null) {
            $inst 		= new dbDump($options);
        }
 
        if (isset($inst->get->logout)){
        	$inst->destroySession();
        	header('Location: '.$_SERVER['PHP_SELF']);
        	die;
        }
 
        if (!$inst->authentication){
        	$inst->state = null;
        }
 
		switch($inst->state){
			case 'options':
 
				//var_dump($inst->authentication);
				if ($inst->isPost()){
 
					$hostname 	= $inst->getPostVar('host');
					$table 		= $inst->getPostVar('table');
					$username 	= $inst->getPostVar('username');
					$password 	= $inst->getPostVar('password');
					$command 	= "mysqldump --add-drop-table --host=$hostname --user=$username --password=$password $table";
 
					header('Content-Description: File Transfer');
					header('Content-Type: application/octet-stream');
					header('Content-Disposition: attachment; filename='.basename($table."_".date("Y-m-d_H-i-s").".sql"));
					system($command);
					exit();
				} else {
					echo $inst->header;					
					echo $inst->downloadOptions;
					echo $inst->footer;
				}
			break;
			default:
 
				if ($inst->isPost()){
					if ($inst->authenticateUser($inst->post)){
						$inst->state = 'options';
						header("Refresh:0");
						die;
					} else {
						echo $inst->header;
						echo $inst->loginForm(true);
						echo $inst->footer;
					}
 
				} else {
					echo $inst->header;
					echo $inst->loginForm();
					echo $inst->footer;
				}
			break;
		}
 
	}
 
    public function __set($name, $value)
    {
        $method = 'set' . $name;
        if (method_exists($this, $method)) {
            $this->$method($value);
            return $this;
        }
 
        throw new \Exception('"'.$name.'" is an invalid property of '.__CLASS__.' assignment failed!');
 
    }
 
    public function __get($name)
    {
 
        $method = 'get' . $name;
        if (method_exists($this, $method)) {
            return $this->$method();
        }
 
        throw new \Exception('Invalid '.__CLASS__.' property '.$name);
    }
 
    public function setOptions(array $options)
    {
        $methods = get_class_methods($this);
        foreach ($options as $key => $value) {
            $method = 'set' . ucfirst($key);
            if (in_array($method, $methods)) {
                $this->$method($value);
            }
        }
        return $this;
    }  
 
    private function __construct(array $options = null)
    {     
    	// env
    	set_time_limit(20);
 
        if (is_array($options)) {
            $this->setOptions($options);
        }
 
    }
 
	private function __clone(){}
}
VN:F [1.9.9_1125]
Rating: 0.0/10 (0 votes cast)
VN:F [1.9.9_1125]
Rating: 0 (from 0 votes)
102 views

passing variables to get_template part in wordpress

July 23rd, 2015

When including part of a theme using the wordpress function get_template_part(), you will notice the included template will not have access to any variables previously defined, this is because they are now out of scope.

For example the template foo.php will not have access to $a:

$a =1;
get_template_part('foo');

There are two main work a rounds:

1. Global Technique

$a=1;
get_template_part('foo');

in foo.php

global $a;

By adding Global $a; into the template foo.php will allow the template to access the variable $a

2. Locate and include

$a=1;
include(locate_template('foo.php'));

By using locate_template to generate a path and php’s include it is possible to include the template as though it was a block of code in line with the existing code thus consequently has access to the current scope.

Both of which I am not keen on, the global fix (1) downside is that you will need to declare global on each and every variable you wish to use, and the use of global is bad.
The second, Locate and include (2) downside is that should the template not exist a warning will be displayed, this is because a check before hand is not being performed, this leads to more code to check for the files existence or suppress the error, furthermore the code included has the potential to modify any variables in the existing scope.

My approach is to use an extra function added to the themes functions.php file

$a=1;
get_partial('foo',array('a'=>$a)); 

The function behaves in the same way as get_template_part() but performs all the correct checks before hand thus stopping any errors, then assigns the variables to the scope, includes the template, and finally returns a success of true or false.

The function to add is below (simply copy and paste into functions.php)

function get_partial($template, array $args = array()){
    $template   = locate_template(preg_replace("/(.+)\.php$/", "$1", $template).'.php'); 
    if (file_exists($template)){
        global $posts, $post, $wp_did_header, $wp_query, $wp_rewrite, $wpdb, $wp_version, $wp, $id, $comment, $user_ID;
        extract($args);
        include($template);
        return true;
    }
}

Happy coding!

VN:F [1.9.9_1125]
Rating: 10.0/10 (1 vote cast)
VN:F [1.9.9_1125]
Rating: +1 (from 1 vote)
1,682 views

wordpress comment count as a button

July 30th, 2009

Styling the wordpress comment count as a button/icon etc can be tricky because the normal call to the comments

1
comments_popup_link(__('Comments (0)'), __('Comments (1)'), __('Comments (%)'));

formats the output as an anchor point with the correct link, alt tags etc already set up.

So in order to intercept this and then add you own anchor point with divs/imagery etc you need to make a custom request

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 
// SQL
$SQL = "SELECT COUNT(*) AS Count FROM $wpdb->comments WHERE comment_approved='1'";
$SQL.= "AND comment_post_ID='".intval($post->ID)."'";
 
// Sort the array into a string prefixed with 'Comments '
list($comments) = $wpdb->get_results($SQL, ARRAY_A); 	
$comments='Comments ('.$comments['Count'].')';
 
// generate the alt and title tags
$alt = 'Comment on '.$post->post_title;
 
// echo the button if this is not a single and not a page
if (!is_single() && !is_page()) {
 
// anchor open
echo '<a href="'.get_permalink($post->ID).'" alt="'.$alt.'" title="'.$alt.'">';
 
// complex formatting rules for the comment pop up, go here
echo $comments;
 
// anchor close
echo '</a>';
}

The above does exactly the same, formats the link etc, but now you can access the anchor point directly and format it your own way.

VN:F [1.9.9_1125]
Rating: 7.0/10 (1 vote cast)
VN:F [1.9.9_1125]
Rating: +2 (from 2 votes)
33,025 views

WordPress blog to MU

April 16th, 2009

Upgrading to wordpress MU manually from an old blog.

At work I had to update an old wordpress installation from 2.3.2 all the way up to the latest version of WordPress Mu (Multi User).

I documented the method I used and placed it below here.
If you do decide to do this it is done entirely at your own risk.
ALWAYS MAKE A BACKUP FIRST

Stage 1

Install Mu on its own database alongside your existing installation and database, within its own directory in the normal way.

Stage 2

Move the data across from Mu performing an upgrade, keeping all the data intact and wordpress Mu functioning correctly.

How this is done

Read the rest of this entry »

VN:F [1.9.9_1125]
Rating: 5.0/10 (1 vote cast)
VN:F [1.9.9_1125]
Rating: +1 (from 1 vote)
49,041 views

clikStats

December 29th, 2008

clikStats is a wordpress plugin that automatically detects the current links within each post.

ClikStats is retrofitable, and requires no special provision from any classes or code.

Once activated, clikStats will compile who, when, what data which can be viewed through the back office.

The beauty of this plugin is in its portability, it can be used straight out of the box, and provide usefull visitor information, without the need to reverse engineer your posts.

Download the latest version (v0.7) of clikStats from here

Simply add the unzipped folder to your pluggins folder, and make it active to start logging clicks.

Have fun

RELEASE NOTES
v0.3 04.01.2009 Provision for pagenation, deletion and better url parsing
v0.4 05.01.2009 Required update, due to a naming convention issue
v0.5 18.01.2009 Fixed security issues, enhanced UI, extra pages
Big thankyou to Alexandre Ara├║jo for his testing
v0.6 15.02.2009 Added search and some nice aesthetics
v0.7 09.05.2009 Added clik page/post sourcename, improved date filter system
v0.8 03.02.2015 Support for hash tags, malformed markup, uppercase lowercase & relative links

Some of the best reviews

VanSantos
Daily Seo Blog
Haunting thunder
Tech Ravings
THUK Media

VN:F [1.9.9_1125]
Rating: 7.8/10 (4 votes cast)
VN:F [1.9.9_1125]
Rating: -1 (from 3 votes)
65,536 views