Who pollutes your prototype? Find the libs on cdnjs in an automated way
2022-9-1 19:31:10
Author: blog.huli.tw(查看原文)
阅读量:23
收藏
When it comes to CSP bypass, a kind of technique using AngularJS is well-known. One of it’s variant requires another library called Prototype.js to make it works.
After understanding how it works, I began to wonder if there are other libraries on cdnjs that can do similar things, so I started researching.
This article will start with the CSP bypass of cdnjs, talk about why prototype.js is needed, and then mention how I found its replacement on cdnjs.
cdnjs + AngularJS CSP bypass
Putting https://cdnjs.cloudflare.com in the CSP is actually a very dangerous thing, because there is a way that many people know to bypass this CSP.
First, because cdnjs is allowed in CSP, we can import any libraries hosting on cdn.js. Here we choose AngularJS so that we can use CSTI to inject the following HTML:
What is $on.curry.call()? You can replace it with window, and you will find that it’s not working. This is because the expression of AngularJS is scoped in a local object, and you cannot directly access window or properties on window.
Another important thing is that CSP does not contain unsafe-eval, so you can’t directly do constructor.constructor('alert(1)')().
As we can see from the result, $on.curry.call() seems to be equivalent to window, why is that? This is where prototype.js comes in handy, let’s take a look at some of its source code:
1 2 3 4 5 6 7 8
functioncurry() { if (!arguments.length) returnthis; var __method = this, args = slice.call(arguments, 0); returnfunction() { var a = merge(args, arguments); return __method.apply(this, a); } }
This function will be added to Function.prototype, and we can focus on the first line: if (!arguments.length) return this;, if there is no parameter, it will return this directly.
In JavaScript, if you use call or apply to call a function, the first parameter can specify the value of this, if not passed it will be the default value, in non-strict mode it is window.
This is why $on.curry.call() will be window, because $on is a function, so when $on.curry.call() is called without any parameters, curry function will return this, which is window, according to the conditional statement in the first line.
To summarize, the reason why AngularJS needs the help of prototype.js is because prototype.js:
Provides a function that added to the prototype
And this function will return this
The first point is very important, because as mentioned earlier, there is no way to access the window in the expression, but prototype.js puts things on the prototype, so it can be accessed through prototype.
The second point is also very important. We can access window because this is window by default when calling a function via .call() without providing thisArg.
After knowing how this works, you should know how to find a replacement, as long as you find one with the same function structure.
I suddenly thought of an article I wrote before: Don’t break the Web: Take SmooshGate and keygen as examples, in which I mentioned that because MooTools is used to adding new things to the prototype, the method originally called flatten had to be renamed flat.
Because the files are not large, you can read them one by one. If you want to be faster, you can also use return this as a keyword to search, and you can find two as soon as you search:
1 2 3 4 5 6 7 8 9 10 11 12 13
Array.implement({ erase: function(item){ for (var i = this.length; i--;){ if (this[i] === item) this.splice(i, 1); } returnthis; },
After opening the file, the alert pop up! The bypass works.
It’s time to think about how to automate it.
Find the replacement in an automated way
A simple and intuitive automated process is probably:
Find all libraries on cdnjs
Find all JS files for each library
Use a headless browser (I use puppeteer) to test whether each JS adds a new property to the prototype
Try to call the new property to see if it will return window
Some of the details depend on how you want to deal with it. For example, if you want to be more precisely, you can test all versions of the library, but in that case, the amount of testing may increase by five to ten times.
I don’t want to spent too much time on it, so I will use the latest version only.
In addition to finding a method that can return this, I also want to see which libraries will modidy your prototype, which can be known from the results of the third step.
Find all libraries on cdnjs
I went to the cdnjs website to see how it works, I found that it called the API in algolia to fetch the list of libraries. Algolia provides a method to pull back all the data, but the api key of the official website does not support it, and paging is limited, only returns the first 1000 results.
So, I found the search API, assuming that there are no more than 1000 libraries starting with each letter, I can search for the libraries starting with each letter from a-zA-Z0-9, thereby bypassing the 1000 limitation.
The list of packages is available, and the files for each package are also available. Moving on to our final step: finding eligible libraries.
There are more than 4000 libraries on cdnjs. If we run them one by one, we must run more than 4000 times. But in fact, there should be a few that meet our conditions, so I choose to run the test for every 10 libraries.
If none of these 10 libraries have changed the prototype, then the next group, if any, use a binary search to find out which libraries have changed.
Before the library is loaded, we first record the properties on each prototype. After loading the library, we record it again and compare it with the previous one to find out which properties were added after the library was introduced. Then we can also divide the results into two types, one is the method added to the prototype, and the other is the function that meets our criteria.
Besides prototype.js, we have 11 other libraries that can be used.
Conclusion
By grabbing all the library information on cdnjs and using the headless browser to help verify, we have successfully found 11 alternatives to prototype.js. These libraries will add new methods on the prototype, and those methods will return this after calling it.
It took me a day or two to make this tiny project, because the data format is relatively simple, the verification method is also very simple, and the number is not really much. If you want to speed up, you can open a few more threads to run.
By the way, finding a replacement is mostly for fun, because it doesn’t make sense for a server to block prototype.js in particular(unless it’s a XSS challenge).
Anyway, even it’s not that useful, it’s still a good and fun experience to do such research. At least, we know who pollues our prototype now.