目录
案例一
思考
源码分析
查找问题
实现
案例二
查看源码
问题查找
实现
实验环境:DOM clobbering | Web Security Academy (portswigger.net)
案例一
里面是一篇篇的博客,点击进去里面是一些评论
思考
尝试一些常规的xss
没什么效果...
他将我后面的给过滤掉了
我们需要分析他的他的源码
源码分析
又是这个框架
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);
}
}
};
这是第二个里面的源代码,里面有三个方法
分别是载入,过滤和展示,第一个是将请求的数据进行一个提交然后使用displaycomments方法进行展示,widow.location使我们请求的地址栏而后面的search是我们请求的参数
提交完之后他会先提取我们的头像然后又创建了一个div将我们的标签放进去,这些东西它都会放进一个p标签里面去
查找问题
这里头像有两个,当我们头像不存在的时候他有一个默认的值
他这里会写进一个img后面是如果我们有头像就用我们的如果没有他就用默认的然后会走进defaultAvatar.avatar,如果我们能够走到window.defaultAvatar,就可能闭合我们的src,那么我们就得想如何创造它
只要我们原来没有头像就 可以⽤⼀个构造⼀个defaultAvatar.avatar进⾏XSS了。
我们可以⽤HTMLCollection来操作
<a id=defaultAvatar><a id=defaultAvatar
name=avatar href="1:"onerror=alert(1)//"
注意"需要进⾏HTML实体编码,⽤URL编码的话浏览器会报错1:%22οnerrοr=alert(1)//
实现
用我们之前的payload会被编码导致失败,我们需要将1:"换成一个不存再的协议,否侧会被当作url地址栏会将它进行编码成%22导致我们无法进入到后面的语句,这里我换成的是cid
案例二
他的一个界面还是跟我们之前的一个相似
查看源码
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();
let janitor = new HTMLJanitor({tags: {input:{name:true,type:true,value:true},form:{id:true},i:{},b:{},p:{}}});
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 avatarImgElement = document.createElement("img");
avatarImgElement.setAttribute("class", "avatar");
avatarImgElement.setAttribute("src", comment.avatar ? comment.avatar : "/resources/images/avatarDefault.svg");
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 + janitor.clean(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(avatarImgElement);
commentSection.appendChild(firstPElement);
if (comment.body) {
let commentBodyPElement = document.createElement("p");
commentBodyPElement.innerHTML = janitor.clean(comment.body);
commentSection.appendChild(commentBodyPElement);
}
commentSection.appendChild(document.createElement("p"));
userComments.appendChild(commentSection);
}
}
};
这里面没有用我们之前很明显的window.x了,相对就比较安全
再评论的大框里面进行了一个过滤
⼀开始就初始化了HTMLJanitor,只能使⽤初始化内的标签及其属性,对于重要的输⼊输出地⽅都使 ⽤了janitor.clean 进⾏过滤。看起来我们没办法很简单地进⾏XSS,那我们就只能来看 看resources/js/htmlJanitor.js 这个过滤⽂件了
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define('html-janitor', factory);
} else if (typeof exports === 'object') {
module.exports = factory();
} else {
root.HTMLJanitor = factory();
}
}(this, function () {
/**
* @param {Object} config.tags Dictionary of allowed tags.
* @param {boolean} config.keepNestedBlockElements Default false.
*/
function HTMLJanitor(config) {
var tagDefinitions = config['tags'];
var tags = Object.keys(tagDefinitions);
var validConfigValues = tags
.map(function(k) { return typeof tagDefinitions[k]; })
.every(function(type) { return type === 'object' || type === 'boolean' || type === 'function'; });
if(!validConfigValues) {
throw new Error("The configuration was invalid");
}
this.config = config;
}
var blockElementNames = ['P', 'LI', 'TD', 'TH', 'DIV', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'PRE'];
function isBlockElement(node) {
return blockElementNames.indexOf(node.nodeName) !== -1;
}
var inlineElementNames = ['A', 'B', 'STRONG', 'I', 'EM', 'SUB', 'SUP', 'U', 'STRIKE'];
function isInlineElement(node) {
return inlineElementNames.indexOf(node.nodeName) !== -1;
}
HTMLJanitor.prototype.clean = function (html) {
const sandbox = document.implementation.createHTMLDocument('');
const root = sandbox.createElement("div");
root.innerHTML = html;
this._sanitize(sandbox, root);
return root.innerHTML;
};
HTMLJanitor.prototype._sanitize = function (document, parentNode) {
var treeWalker = createTreeWalker(document, parentNode);
var node = treeWalker.firstChild();
if (!node) { return; }
do {
if (node.nodeType === Node.TEXT_NODE) {
// If this text node is just whitespace and the previous or next element
// sibling is a block element, remove it
// N.B.: This heuristic could change. Very specific to a bug with
// `contenteditable` in Firefox: http://jsbin.com/EyuKase/1/edit?js,output
// FIXME: make this an option?
if (node.data.trim() === ''
&& ((node.previousElementSibling && isBlockElement(node.previousElementSibling))
|| (node.nextElementSibling && isBlockElement(node.nextElementSibling)))) {
parentNode.removeChild(node);
this._sanitize(document, parentNode);
break;
} else {
continue;
}
}
// Remove all comments
if (node.nodeType === Node.COMMENT_NODE) {
parentNode.removeChild(node);
this._sanitize(document, parentNode);
break;
}
var isInline = isInlineElement(node);
var containsBlockElement;
if (isInline) {
containsBlockElement = Array.prototype.some.call(node.childNodes, isBlockElement);
}
// Block elements should not be nested (e.g. <li><p>...); if
// they are, we want to unwrap the inner block element.
var isNotTopContainer = !! parentNode.parentNode;
var isNestedBlockElement =
isBlockElement(parentNode) &&
isBlockElement(node) &&
isNotTopContainer;
var nodeName = node.nodeName.toLowerCase();
var allowedAttrs = getAllowedAttrs(this.config, nodeName, node);
var isInvalid = isInline && containsBlockElement;
// Drop tag entirely according to the whitelist *and* if the markup
// is invalid.
if (isInvalid || shouldRejectNode(node, allowedAttrs)
|| (!this.config.keepNestedBlockElements && isNestedBlockElement)) {
// Do not keep the inner text of SCRIPT/STYLE elements.
if (! (node.nodeName === 'SCRIPT' || node.nodeName === 'STYLE')) {
while (node.childNodes.length > 0) {
parentNode.insertBefore(node.childNodes[0], node);
}
}
parentNode.removeChild(node);
this._sanitize(document, parentNode);
break;
}
// Sanitize attributes
for (var a = 0; a < node.attributes.length; a += 1) {
var attr = node.attributes[a];
if (shouldRejectAttr(attr, allowedAttrs, node)) {
node.removeAttribute(attr.name);
// Shift the array to continue looping.
a = a - 1;
}
}
// Sanitize children
this._sanitize(document, node);
} while ((node = treeWalker.nextSibling()));
};
function createTreeWalker(document, node) {
return document.createTreeWalker(node,
NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT,
null, false);
}
function getAllowedAttrs(config, nodeName, node){
if (typeof config.tags[nodeName] === 'function') {
return config.tags[nodeName](node);
} else {
return config.tags[nodeName];
}
}
function shouldRejectNode(node, allowedAttrs){
if (typeof allowedAttrs === 'undefined') {
return true;
} else if (typeof allowedAttrs === 'boolean') {
return !allowedAttrs;
}
return false;
}
function shouldRejectAttr(attr, allowedAttrs, node){
var attrName = attr.name.toLowerCase();
if (allowedAttrs === true){
return false;
} else if (typeof allowedAttrs[attrName] === 'function'){
return !allowedAttrs[attrName](attr.value, node);
} else if (typeof allowedAttrs[attrName] === 'undefined'){
return true;
} else if (allowedAttrs[attrName] === false) {
return true;
} else if (typeof allowedAttrs[attrName] === 'string') {
return (allowedAttrs[attrName] !== attr.value);
}
return false;
}
return HTMLJanitor;
}));
创建了⼀个新的HTML⽂档⽤作sandbox ,然后对于sandbox内的元素进⾏_sanitize过滤
在_sanitize函数⼀开始调⽤了createTreeWalker函数创建⼀个TreeWalker,这个类表示⼀个当 前⽂档的⼦树中的所有节点及其位置。
.SHOW_TEXT表示 DOM 树中的一个文本节点。.SHOW_COMMENT,筛选注释,.SHOW_ELEMENT会调用子元素
如果此⽂本节点只是空⽩,并且上⼀个或下⼀个元素同级是`blockElement`,则将其删除 } 移除所有的注释
问题查找
如果你传的是黑名单里面的那么我直接删掉,只有form和input才能走到下面的函数
这种黑名单里面的函数和嵌套都会被过滤掉,如果是白名单里面的但属性不是里面的也会被过滤掉