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:
function curry() {
if (!arguments.length) return this;
var __method = this, args = slice.call(arguments, 0);
return function() {
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:
Array.implement({
erase: function(item){
for (var i = this.length; i--;){
if (this[i] === item) this.splice(i, 1);
}
return this;
},
empty: function(){
this.length = 0;
return this;
},
})
Both Array.prototype.erase and Array.prototype.empty functions return this, so the following two methods can get the window:
[].erase.call()
[].empty.call()
Then try it immediately to see if the CSP bypass is successful:
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.
const axios = require('axios')
const fs = require('fs');
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
function write(content) {
fs.writeFileSync('./data/libDetail.json', content)
}
if (!fs.existsSync('./data/libDetail.json')) {
write('[]')
}
const existMap = {}
let detailItems = JSON.parse(fs.readFileSync('./data/libDetail.json', 'utf8'))
for(let item of detailItems) {
existMap[item.name] = true
}
async function getDetail(libName, version) {
const url = `https://api.cdnjs.com/libraries/${encodeURIComponent(libName)}/${version}`
try {
const response = await axios(url)
return response.data
} catch(err) {
console.log(url)
console.log('failed:', libName, err.message)
//process.exit(1)
}
}
async function getLib(libraries, lib) {
console.log('fetching:', lib.name)
const detail = await getDetail(lib.name, lib.version)
if (!detail) return
detailItems.push(detail)
write(JSON.stringify(detailItems, null, 2))
console.log(`progress: ${detailItems.length}/${libraries.length}`)
}
async function getFiles() {
const libraries = JSON.parse(fs.readFileSync('./data/libs.json', 'utf8'))
for(let lib of libraries) {
if (existMap[lib.name]) continue
await sleep(200)
getLib(libraries, lib)
}
}
async function main() {
getFiles()
}
main()
Find the libraries we want
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.
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.For details, please refer to these two articles:
The bypass is as follows:
I will explain how it works step by step.
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 withwindow
, 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 doconstructor.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: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 returnthis
directly.In JavaScript, if you use
call
orapply
to call a function, the first parameter can specify the value ofthis
, if not passed it will be the default value, in non-strict mode it iswindow
.This is why
$on.curry.call()
will bewindow
, because$on
is a function, so when$on.curry.call()
is called without any parameters,curry
function will returnthis
, which iswindow
, 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:
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
becausethis
iswindow
by default when calling a function via.call()
without providingthisArg
.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 renamedflat
.Will MooTools also meet our above conditions?
Manually find alternatives - MooTools
We can find various prototypes modified by MooTools in this folder: https://github.com/mootools/mootools-core/tree/master/Source/Types
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:Both
Array.prototype.erase
andArray.prototype.empty
functions returnthis
, so the following two methods can get thewindow
:Then try it immediately to see if the CSP bypass is successful:
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:
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 implementation of the code looks like this:
After running, we can get a list of all the cdnjs libraries with their names.
Find all JS files for each library
The basic information of the library is placed in algolia, but some details are placed in cdnjs's own API.
The rules of this API are also very simple. The URL is: https://api.cdnjs.com/libraries/${package_name}/${version}, so we can get the details of every libraries.
Find the libraries we want
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.
The HTML use for detection looks like this:
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.
The complete code is a bit longer, you can check it: https://github.com/aszx87410/cdnjs-prototype-pollution/blob/main/scan.js
But the process is roughly:
Result
Among the 4290 libraries, 74 (1.72%) libraries pollute your prototype. The list is as follows:
6to5@3.6.5
Colors.js@1.2.4
Embetty@3.0.8
NicEdit@0.93
RGraph@606
ScrollTrigger@1.0.5
TableExport@5.2.0
ajv-async@1.0.1
angular-vertxbus@6.4.1
asciidoctor.js@1.5.9
aurelia-script@1.5.2
blendui@0.0.4
blissfuljs@1.0.6
bootstrap-calendar@0.2.5
carto.js@4.2.2
cignium-hypermedia-client@1.35.0
core-js@3.24.1
custombox@4.0.3
d3fc@11.0.0
d3plus@2.0.1
datejs@1.0
deb.js@0.0.2
defiant.js@2.2.7
eddy@0.7.0
ext-core@3.1.0
extjs@6.2.0
fs-tpp-api@2.4.4
highcharts@10.2.0
inheritance-js@0.4.12
jo@0.4.1
jquery-ajaxy@1.6.1
jquery-ui-bootstrap@0.5pre
js-bson@2.0.8
jslite@1.1.12
json-forms@1.6.3
keras-js@0.3.0
kwargsjs@1.0.1
leaflet.freedraw@2.0.1
lobipanel@1.0.6
melonjs@1.0.1
metro@4.4.3
mo@1.7.3
monet@0.9.3
mootools@1.6.0
oidc-client@1.11.5
opal@0.3.43
prototype@1.7.3
qcobjects@2.3.69
qoopido.demand@8.0.2
qoopido.js@3.7.4
qoopido.nucleus@3.2.15
quantumui@1.2.0
rantjs@1.0.6
rita@2.8.1
rivescript@2.2.0
scriptaculous@1.9.0
should.js@13.2.3
simple-gallery-js@1.0.3
simplecartjs@3.0.5
strapdown-topbar@1.6.4
string_score@0.1.22
survey-angular@1.9.45
survey-jquery@1.9.45
survey-knockout@1.9.45
survey-react@1.9.45
survey-vue@1.9.45
tablefilter@2.5.0
tmlib.js@0.5.2
tui-editor@1.4.10
typeis@1.1.2
uppy@3.0.0
vanta@0.5.22
waud.js@1.0.3
zui@1.10.0
And of these 74 libraries, 12 (16.2%) meet our criteria, the list is as follows:
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.
Source code is available on GitHub: https://github.com/aszx87410/cdnjs-prototype-pollution