在本文中,我将介绍Prototype污染漏洞,并介绍该漏洞是如何绕过常见的HTMLXSS检查器的。prototype是应用最为广泛的Ajax开发框架,其的特点是功能实用而且尺寸较小,非常适合在中小型的Web应用中使用。最后,我也在积极测试通过半自动方法发现Prototype污染攻击的方法。
Prototype污染
Prototype污染是一个安全漏洞,是特定于JavaScript的一个漏洞。它源于称为基于Prototype的继承的JavaScript继承模型。与c++或Java不同,在JavaScript中,你无需定义类即可创建对象。你只需要使用大括号表示法并定义属性,例如:
const obj = { prop1: 111, prop2: 222, }
该对象具有两个属性:prop1和prop2。但是,这些并不是我们可以访问的唯一属性。例如,对obj.toString()的调用将返回“[objectObject]”,toString(以及一些其他默认成员)来自Prototype。JavaScript中的每个对象都有一个Prototype(也可以为null)。如果未指定,默认情况下,对象的Prototype为Object.prototype。
在DevTools中,我们可以轻松地检查Object.prototype的属性列表:
我们还可以通过检查其__proto__成员或调用Object.getPrototypeOf来找出什么对象是给定对象的Prototype:
同样,我们可以使用__proto__或Object.setPrototypeOf设置对象的Prototype:
简而言之,当我们尝试访问一个对象的属性时,JS引擎首先检查对象本身是否包含该属性。如果是,则返回。否则,JS检查Prototype是否具有该属性。如果不是,则JS检查Prototype的Prototype……依此类推,直到Prototype为null,这就是Prototype链。
JS遍历Prototype链这一事实具有重要的作用,如果我们可以某种方式污染Object.prototype(即使用新属性对其进行扩展),那么所有JS对象都将具有这些属性。
比如以下示例:
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 }
这证明了Prototype污染可能会对应用程序的安全性产生巨大影响,因为我们可以定义一些属性来改变它们的逻辑。但是,只有少数几种已知的滥用此漏洞的情况:
1.OlivierArtreau利用它在GhostCMS中获得了RCE;
2.我利用它获得了Kibana的RCE;
3.POSIX表明,通过Prototype污染进行的RCE在ejs以及pug和handlebars中都是可行的。
在继续讨论之前,我需要再介绍一个主题:Prototype污染最初是如何发生的?
此漏洞的入口点通常是合并操作(即将所有属性从一个对象复制到另一个对象)。例如:
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 } }
递归合并的基本流程是:
1.遍历obj2的所有属性,并检查它们是否存在于obj1中;
2.如果存在属性,则对该属性执行合并操作;
3.如果属性不存在,则将其从obj2复制到obj1;
在现实世界中,如果用户对合并的对象有任何控制,那么其中一个对象通常来自JSON.parse的输出。JSON.parse有点特殊,因为它将__proto__视为“常规”属性,也就是说,它没有作为Prototype访问器的特殊含义。如下所示:
在示例中,obj1是使用JS的大括号表示法创建的,而obj2是使用JSON.parse创建的。这两个对象都只定义了一个属性,称为__proto__。但是,访问obj1.__proto__返回Object.prototype(因此__proto__是返回Prototype的特殊属性),而obj2.__proto__包含JSON中给出的值,即:123。这证明相比普通的JavaScript解析,__proto__属性在JSON中得到了不同的对待。
因此,现在想象一个合并两个对象的recursiveMerge函数:
obj1={}
obj2=JSON.parse('{"__proto__":{"x":1}}')
该函数的工作原理大致如下:
1.遍历obj2中的所有属性,唯一的属性是__proto__;
2.检查obj1.__proto__是否存在;
3.遍历obj2.__proto__中的所有属性,唯一的属性是x。
4.分配: obj1.__proto__.x = obj2.__proto__.x。因为obj1.__proto__指向Object.prototype,这样Prototype就被污染了。
在许多流行的JS库(包括lodash或jQuery)中都发现了这种错误。
Prototype污染是如何绕过HTMLsanitizer的?
现在我们知道什么是Prototype污染,以及合并操作如何引入此漏洞,正如我之前提到的,所有公开的利用Prototype污染的示例都集中在NodeJS上,其目的是实现远程代码执行。但是,客户端JavaScript也可能受到此漏洞的影响。因此这会引发一个问题:攻击者能从浏览器的Prototype污染中得到什么?
现在我将把注意力集中在HTMLsanitizer上,HTMLsanitizer程序其实是一个库,其工作是采取不受信任的HTML标记,并删除所有可能引起XSS攻击的标记或属性。通常,它们都基于允许列表;也就是说,它们有一个允许的标记和属性列表,其他所有标记和属性都被删除。
想象一下,我们有一个只允许和标签使用的sanitizer。如果我们给它添加了以下标记:
HeaderThis is some HTML
它应该转换为以下形式:
HeaderThis is some HTML
HTML清理程序需要维护允许的元素属性和元素的列表。基本上,该库通常采用以下两种方式中的一个来存储列表:
1.在一个数组中
该库可能具有包含允许的元素列表的数组,例如:
const ALLOWED_ELEMENTS = ["h1", "i", "b", "div"]
然后,要检查是否允许某些元素,只需调用ALLOWED_ELEMENTS.includes(element)即可。这种方法可以防止Prototype污染,因为我们不能扩展阵列。也就是说,我们不能污染length属性,也不能污染已经存在的索引。
例如,即使我们这样做:
Object.prototype.length = 10; Object.prototype[0] = 'test';
然后,ALLOWED_ELEMENTS.length仍返回4,而ALLOWED_ELEMENTS[0]仍为“h1”。
2.在一个对象中
另一种解决方案是使用允许的元素存储对象,例如:
const ALLOWED_ELEMENTS = { "h1": true, "i": true, "b": true, "div" :true }
然后,为了检查某些元素是否被允许,库可以检查是否存在ALLOWED_ELEMENTS[element]。这种方法很容易被Prototype污染利用,因为如果我们通过以下方式污染Prototype:
Object.prototype.SCRIPT = true;
然后ALLOWED_ELEMENTS[“SCRIPT”]返回true。
已分析的sanitizer清单
我在npm中搜索了HTMLsanitizer,发现了三个最受欢迎的sanitizer:
1.sanitize-html ,每周大约下载80万次,sanitize-html提供了带有清晰API的简单HTML sanitizer。sanitize-html非常适合删除HTML片段,例如由ckeditor和其他富文本编辑器创建的HTML片段。通过Word复制和粘贴时,删除多余的CSS特别方便。sanitize-html允许你指定要允许的标签,以及每个标签的允许属性。
2.每周大约有77万次下载的xss。
3.dompurify每周约有54万次下载。DOMPurify是一个只针对DOM的XSS杀毒软件,适用于HTML、MathML和SVG。它也非常容易和使用,DOMPurify于2014年2月启动,现在已经是2.1.0版本。DOMPurify是用JavaScript编写的,可以在所有的现代浏览器中工作(Safari (10+), Opera (15+), Internet Explorer (10+), Edge, Firefox和Chrome以及几乎所有使用Blink或WebKit的浏览器)。它在MSIE6或其他老版浏览器上不会失效。现在的自动化测试现在已经覆盖了15种不同的浏览器,以后还会有更多。
另外,我还使用了google-closure-library,它在npm中不是很流行,但是在Google应用程序中非常常用。
接下来,我将简要概述所有sanitizer,并说明如何通过Prototype污染绕开所有sanitizer。我假设Prototype在加载库之前就已被污染,另外我还将假定所有sanitizer都在默认配置中使用。
sanitize-html
sanitize-html的调用很简单:
不过你也可以使用备用选项将第二个参数传递给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 };
allowedTags属性是一个数组,这意味着我们不能在Prototype污染中使用它。不过,值得注意的是,允许使用iframe。
继续分析,就会发现allowedAttributes是一个映射,它提供了一个想法,即添加属性iframe:['onload']应该可以通过 < iframe onload=alert(1) >。
在内部,allowedAttributes被重写为变量allowedAttributesMap,这是决定是否允许属性的逻辑(name是当前标记的名称,a是属性的名称):
// 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将是任何标签的有效属性,如下所示:
xss
下一个库xss的调用看起来与以上非常相似:
它还可以选择接受第二个参数,称为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
与以前的sanitizer类似,DOMPurify的基本用法非常简单:
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;
在JavaScript中,运算符会遍历Prototype链。因此,如果Object.prototype中存在此属性,则cfg中的“ALLOWED_ATTR”将返回true。
默认情况下,DOMPurify允许< img >标记,因此该漏洞利用只需要使用onerror和src污染ALLOWED_ATTR。
有趣的是,Cure53发布了新版本的DOMPurify,试图防止这种攻击。如果你认为可以绕过此修复程序,请查看攻击的更新版本。
闭包(Closure)
ClosureSanitizer有一个名为attributewhitelist.js的文件,该文件的格式如下:
goog.html.sanitizer.AttributeWhitelist = { '* ARIA-CHECKED': true, '* ARIA-COLCOUNT': true, '* ARIA-COLINDEX': true, '* ARIA-CONTROLS': true, '* ARIA-DESCRIBEDBY': tru ... }
在此文件中,定义了允许的属性列表。它采用“TAG_NAMEATTRIBUTE_NAME”格式,其中TAG_NAME也可以是通配符(“*”)。因此,绕过就像污染Prototype一样简单,以允许在所有元素上出现onerror和src。
下面的代码就是绕过的全过程:
'; 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">
如何发现Prototype污染的工具
如上所述,Prototype污染可以绕过所有流行的JSsanitizer。为了找到绕过方法,我需要手动分析源。即使所有绕过都非常相似,但仍需要付出一些努力才能执行分析。自然而然,下一步就是考虑使流程更加自动化。
我的第一个想法是使用正则表达式扫描库源代码中的所有可能的标识符,然后将此属性添加到Object.prototype。如果正在访问任何属性,那么我知道可以通过Prototype污染对其进行绕过。
以下代码片段就是我们从DOMPurify中摘录的:
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; } });
此方法有效,但有一些严重的缺点:
1.它不适用于计算的属性名称(例如,对于Closure,我找不到任何有用的内容);
2.它混淆了检查属性是否存在:obj中的ALLOWED_ATTR将返回true;
所以我想出了第二个想法,顾名思义,我可以访问我试图用Prototype污染攻击的库的源代码。因此,我可以使用代码工具将所有属性访问更改为自己的函数,这将检查该属性是否可以到达Prototype。
以下就是我从DOMPurify中摘录的内容:
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]; }
至此,所有属性访问都将转换为对$_GET_PROP的调用,当从Object.prototype中读取属性时,该调用将在控制台中打印一个信息。
为此,我创建了一个工具来执行我也在GitHub上共享的工具。外观如下:
多亏了这种方法,我才能发现另外两个滥用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 })();
缺点是Prototype需要在DOMPurify加载之前被污染。
现在让我们看一下Closure,首先,现在很容易看到Closure尝试检查属性是否在允许列表中:
其次,我注意到一个有趣的外观:
Closure加载了很多具有依赖性的JS文件,CLOSURE_BASE_PATH定义路径。因此,我们可以污染该属性以从任何路径加载自己的JS,sanitizer甚至都不需要被调用!
过程如下:
< script > Object.prototype.CLOSURE_BASE_PATH = 'data:,alert(1)//'; < /script >< script src= >< script > goog.require('goog.html.sanitizer.HtmlSanitizer'); goog.require('goog.dom'); < /script >
多亏了pollute.js,我们才能找到更多的污染方法。
总结
Prototype污染会导致绕过所有流行的HTML杀毒软件,这通常是通过影响元素或属性的允许列表来完成此绕过过程的。
最后要注意的是,如果你在Google搜索中发现了污染的Prototype,那么搜索字段中将包含XSS!视频请点击这里。
本文翻译自:https://research.securitum.com/prototype-pollution-and-bypassing-client-side-html-sanitizers/如若转载,请注明原文地址: