Table of contents
First introduce the prototype chain
What are JS prototype
and __proto__
JS?
We can think of the prototype prototype
as Foo
an attribute of the class, and all Foo
objects instantiated with the class will have all the contents of this attribute, including variables and methods.
We can Foo.prototype
access Foo
the prototype of the class through , but Foo
the instantiated object cannot access the prototype through the prototype. At this time, it's time __proto__
to show up.
A foo object instantiated from the Foo class can foo.__proto__
access the prototype of the Foo class through attributes, that is to say:
foo.__proto__ == Foo.prototype
in conclusion:
prototype
prototype
It is an attribute of a class, and all class objects will have the attributes and methods in it when they are instantiated- An attribute of an object , pointing to an attribute
__proto__
of the class in which the object is locatedprototype
JavaScript Prototype Chain Inheritance
prototype
All class objects will have properties and methods when they are instantiated . This feature is used to implement the inheritance mechanism in JavaScript.
for example:
function Father() {
this.first_name = 'Donald'
this.last_name = 'Trump'
}
function Son() {
this.first_name = 'Melania'
}
Son.prototype = new Father()
let son = new Son()
console.log(`Name: ${son.first_name} ${son.last_name}`)
The Son class inherits last_name
the properties of the Father class, and the final output is Name: Melania Trump
.
To sum up, for the object son, when calling son.last_name
, the JavaScript engine will actually perform the following operations:
- Find last_name in object son
- If not found,
son.__proto__
look for last_name in - If still not found, then keep
son.__proto__.__proto__
looking for last_name in - Search in turn until
null
the end is found. For example ,Object.prototype
the__proto__
null
Under what circumstances can the prototype chain be polluted
In practical applications, under what circumstances may there be situations where the prototype chain can be modified by an attacker?
Let's think about it, under what circumstances can we set __proto__
the value? In fact, just look for the operation that can control the "key name" of the array (object):
- Object merge combined splicing
- Object clone (in fact, the kernel is to merge the object to be operated into an empty object) copy
Taking the object merge as an example, we imagine a simple merge function:
function merge(target, source) {
for (let key in source) {
if (key in source && key in target) {
merge(target[key], source[key])
} else {
target[key] = source[key]
}
}
}
In the process of merging, there is an assignment operation target[key] = source[key]
, so if the key is true __proto__
, it can pollute the prototype chain
Let's experiment with the following code:
let o1 = {}
let o2 = {a: 1, "__proto__": {b: 2}}
merge(o1, o2)
console.log(o1.a, o1.b)
o3 = {}
console.log(o3.b)
The result is that although the merger is successful, the prototype chain is not polluted:
this is because, let o2 = {a: 1, "__proto__": {b: 2}}
in the process of creating o2 with JavaScript ( ), __proto__
it already represents the prototype of o2. Yes [a, b]
, __proto__
it is not a key, and naturally it will not modify the prototype of Object.
So, how to make __proto__
is considered a key name?
We change the code to the following:
let o1 = {}
let o2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}')
merge(o1, o2)
console.log(o1.a, o1.b)
o3 = {}
console.log(o3.b)
It can be seen that the newly created o3 object also has the b attribute, indicating that the Object has been polluted.
This is because, in the case of JSON parsing, __proto__
it will be considered a real "key name" instead of a "prototype", so when traversing o2 This key will exist when
The merge operation is the most common operation that may control the key name, and it is also the most vulnerable to prototype chain attacks. This problem exists in many common libraries.
example
const express = require('express')
var hbs = require('hbs');
var bodyParser = require('body-parser');
const md5 = require('md5');
var morganBody = require('morgan-body');
const app = express();
var user = []; //empty for now
var matrix = [];
for (var i = 0; i < 3; i++){
matrix[i] = [null , null, null];
}
function draw(mat) {
var count = 0;
for (var i = 0; i < 3; i++){
for (var j = 0; j < 3; j++){
if (matrix[i][j] !== null){
count += 1;
}
}
}
return count === 9;
}
app.use(express.static('public'));
app.use(bodyParser.json());
app.set('view engine', 'html');
morganBody(app);
app.engine('html', require('hbs').__express);
app.get('/', (req, res) => {
for (var i = 0; i < 3; i++){
matrix[i] = [null , null, null];
}
res.render('index');
})
app.get('/admin', (req, res) => {
/*this is under development I guess ??*/
console.log(user.admintoken);
if(user.admintoken && req.query.querytoken && md5(user.admintoken) === req.query.querytoken){
res.send('Hey admin your flag is <b>flag{
prototype_pollution_is_very_dangerous}</b>');
}
else {
res.status(403).send('Forbidden');
}
}
)
app.post('/api', (req, res) => {
var client = req.body;
var winner = null;
if (client.row > 3 || client.col > 3){
client.row %= 3;
client.col %= 3;
}
matrix[client.row][client.col] = client.data;
for(var i = 0; i < 3; i++){
if (matrix[i][0] === matrix[i][1] && matrix[i][1] === matrix[i][2] ){
if (matrix[i][0] === 'X') {
winner = 1;
}
else if(matrix[i][0] === 'O') {
winner = 2;
}
}
if (matrix[0][i] === matrix[1][i] && matrix[1][i] === matrix[2][i]){
if (matrix[0][i] === 'X') {
winner = 1;
}
else if(matrix[0][i] === 'O') {
winner = 2;
}
}
}
if (matrix[0][0] === matrix[1][1] && matrix[1][1] === matrix[2][2] && matrix[0][0] === 'X'){
winner = 1;
}
if (matrix[0][0] === matrix[1][1] && matrix[1][1] === matrix[2][2] && matrix[0][0] === 'O'){
winner = 2;
}
if (matrix[0][2] === matrix[1][1] && matrix[1][1] === matrix[2][0] && matrix[2][0] === 'X'){
winner = 1;
}
if (matrix[0][2] === matrix[1][1] && matrix[1][1] === matrix[2][0] && matrix[2][0] === 'O'){
winner = 2;
}
if (draw(matrix) && winner === null){
res.send(JSON.stringify({
winner: 0}))
}
else if (winner !== null) {
res.send(JSON.stringify({
winner: winner}))
}
else {
res.send(JSON.stringify({
winner: -1}))
}
})
app.listen(3000, () => {
console.log('app listening on port 3000!')
})
First analyze the code.
The condition for obtaining the flag is that the incoming querytoken must be equal to the MD5 value of the admintoken in the user array itself, and both must exist.
It can be seen from the code that the full text does not assign a value to user.admintokn, so theoretically this value does not exist, but there is an assignment statement below:
matrix[client.row][client.col] = client.data
data
, row
, col
, are all the values passed in by our post, and they are all controllable, so prototype chain pollution can be constructed.
Therefore, two variables can be defined.
One variable is used to pass in data, row, and col, so that the original admintoken can be polluted through the prototype chain to make the admintoken exist. The
other variable is used to make the admintoken match the md5 value in the passed-
in URL. Pay attention to use json when setting the value, otherwise __proto__ will not be recognized as an object but a character,
so you can use the prototype chain to pollute and successfully capture the flag