web安全代码基础-JavaScript(NodeJS安全)
目录NodeJS基础概念NodeJS环境搭建1. 安装 NodeJS2. 初始化项目 安装依赖业务代码实现存在漏洞版本Express MySQL 用户登录存在 SQL 注入漏洞文件读取功能路径遍历 / 目录穿越漏洞命令执行 RCE 漏洞exec /eval 危险写法原型链污染NodeJS 重点漏洞黑盒审计思路无源码白盒代码审计思路有源码NodeJS基础概念Node.js是一个开源的可以跨平台运行JavaScript代码的软件NodeJS环境搭建1. 安装 NodeJS官网https://nodejs.org/en安装完成验证node -v npm -v2. 初始化项目 安装依赖新建文件夹终端执行npm init -y npm i express body-parser cookie-parser multer mysql业务代码实现存在漏洞版本Express MySQL 用户登录存在 SQL 注入漏洞login.js文件const express require(express); const bodyParser require(body-parser); const mysql require(mysql); const app express(); app.use(bodyParser.urlencoded({extended:true})); app.use(bodyParser.json()); // 数据库连接配置请自行修改账号密码 const conn mysql.createConnection({ host:127.0.0.1, user:root, password:root, database:test }); conn.connect(); // 登录接口【危险直接拼接SQL存在SQL注入】 app.post(/login,(req,res){ let username req.body.username; let password req.body.password; // ❌ 直接字符串拼接未使用预编译 let sql select * from user where username${username} and password${password}; conn.query(sql,(err,result){ if(err){ return res.send(数据库查询错误); } if(result.length0){ res.send(登录成功); }else{ res.send(账号密码错误); } }) }) app.listen(3000,(){ console.log(服务启动http://127.0.0.1:3000) })攻击 Payload无需密码直接登录POST /login Content-Type:application/json { username:admin or 11#, password:123 }拼接后 SQLselect * from user where usernameadmin or 11# and password123 // # 在 mysql 中代表注释绕过密码校验。修复方案使用占位符预编译禁止直接拼接 SQLlet sql select * from user where username? and password?; conn.query(sql,[username,password],callback);文件读取功能路径遍历 / 目录穿越漏洞file_read.js文件const express require(express); const fs require(fs); const path require(path); const app express(); app.use(express.urlencoded({extended:true})); // 文件读取接口接收文件名参数 app.get(/readfile,(req,res){ let filename req.query.filename; // ❌ 直接拼接路径无过滤 let fullPath path.join(./files/,filename); fs.readFile(fullPath,utf8,(err,data){ if(err){ res.send(文件不存在); return; } res.send(data); }) }) app.listen(3000);攻击 Payloadhttp://127.0.0.1:3000/readfile?filename../../etc/passwd Windows?filename../../C:/Windows/System32/drivers/etc/hosts攻击者可读取服务器任意敏感文件。修复方案路径白名单 规范化路径校验let realPath path.resolve(path.join(./files/,filename)); let basePath path.resolve(./files/); if(!realPath.startsWith(basePath)){ return res.send(非法路径); }命令执行 RCE 漏洞exec /eval 危险写法rce_demo.js文件const express require(express); const {exec, spawnSync} require(child_process); const app express(); app.use(express.json()); // 方式1exec 拼接命令 【高危RCE】 app.get(/exec,(req,res){ let ip req.query.ip; // ❌ 用户可控参数直接拼接系统命令 exec(ping ${ip},(err,stdout){ res.send(stdout) }) }) // 方式2eval 动态执行JS代码【代码注入】 app.get(/eval,(req,res){ let code req.query.code; // ❌ 可控内容传入eval let result eval(code); res.send(result); }) app.listen(3000);攻击Payload1、exec 命令注入http://127.0.0.1:3000/exec?ip127.0.0.1;whoami ; 分隔多条命令执行任意系统命令2、eval 代码注入http://127.0.0.1:3000/eval?coderequire(child_process).execSync(whoami)补充exec会调用 shell 解析命令极易命令注入spawnSync如果参数分开传递是安全如果手动拼接字符串传给 spawnSync 第一个参数依然危险修复方案尽量不用 exec参数不要拼接使用参数数组形式调用exec(ping, [ip]) // 安全写法不会启动shell解析特殊符号 严禁eval执行外部可控输入原型链污染NodeJS 重点漏洞漏洞原理回顾JavaScript 中所有对象默认继承自Object.prototype__proto__等价于对象原型。 如果代码递归合并对象、未过滤__proto__攻击者就可以修改原型实现污染后续代码读取属性时触发异常、RCE、绕过校验。prototype_pollution.js文件const express require(express); const app express(); const bodyParser require(body-parser); app.use(bodyParser.json()); // ❌ 危险函数递归合并对象没有过滤 __proto__ function merge(target, source) { for(let key in source){ if(typeof source[key] object source[key] ! null){ merge(target[key], source[key]); }else{ target[key] source[key]; } } } // 用户初始配置空对象 let userConfig {}; app.post(/setconfig,(req,res){ let input req.body; merge(userConfig, input); res.send(配置更新成功); }) // 业务逻辑判断是否开启管理员权限 app.get(/info,(req,res){ // userConfig本身没有isAdmin正常undefined if(userConfig.isAdmin){ res.send(欢迎管理员); }else{ res.send(普通用户); } }) app.listen(3000,(){ console.log(原型链污染测试启动) })攻击PayloadPOST /setconfig ...... { __proto__:{ isAdmin:true } }攻击分析1、merge 函数遍历 key__proto__2、userConfig.__proto__→ 指向Object.prototype3、给原型添加属性isAdmin:true4、整个程序内所有普通对象都会继承 isAdmintrue访问/info直接变成管理员权限高阶拓展原型链污染经常配合其他漏洞实现 RCE很多模板引擎、渲染库、日志库会读取原型上的属性结合原型污染触发远程代码执行。修复方案对象合并时过滤关键字__proto__、constructor、prototypeconst banKeys [__proto__,constructor,prototype]; for(let key in source){ if(banKeys.includes(key)) continue; // ...后续合并逻辑 }黑盒审计思路无源码1、信息收集Header、报错页面、静态资源判断是否 Node/Express2、寻找所有 URL 参数、POST json 参数、cookie3、通用测试 payload1SQL 注入 # or and2文件读取../ ../../3命令注入; | 4原型污染传入{__proto__:{test:1}}观察业务变化白盒代码审计思路有源码重点搜寻危险关键词eval、new Function exec、execSync、spawn、shell:true query(直接拼接字符串) fs.readFile、fs.createReadStream(路径来自用户) merge、extend 对象合并函数检查是否过滤__proto__