安全开发-web应用-JS-Nodejs
上期我们学习了关于原生开发的DOM树和学习了断点调试,又知道了关于dom-xss的漏洞捏,简单的利用了js调试来尝试加密数据包,好用捏
今天我们来看看关于第三方库的使用
什么事nodejs呢,他是运行在服务端的js,和我们运行在前端的有什么区别呢
首先我们前面写过了原生的js代码,可以知道当你在浏览器中运行的时候检查源代码的时候会发现他会把整个的源代码都显露出来,而nodejs呢,我们举个例子看看
创建一个no-test.js文件
// 引入 express 框架 const express = require('express'); const app = express(); // 设置端口 const PORT = 3000; // 定义根路径的路由 app.get('/', (req, res) => { console.log('MIKUKU'); res.send('发生甚么事了'); }); // 启动服务器 app.listen(PORT, () => { console.log(`Server is running on http://localhost:${PORT}`); });
启动的时候会在本地3000端口开启一个web网页,然后输出我们敲的信息,然后在终端中输出MIKUKU捏
可以看到效果了捏,而且他在浏览器中没有显示任何的代码,虽然他也是属于js代码,但是这里没有任何输出,只有你想输出的文字,就像和php一样,属于后端语言
这就是他简单的例子,首先如果想使用的话,去官网下载一下即可,安装即用,然后如果要安装里面的库用使用
npm install 库名
使用这个代码即可
现在准备工作已经完成
我们现在来用他来实现一个登录操作
首先创建登录主页,代码依旧老登
no-login.html
然后这里我们就要用js来处理后台登录了,用我们前面实验用的js文件修改一下即可
然后直接原地启动
const express = require('express'); const app = express(); app.get('/login',function(req, res){//get路由 console.log('login'); res.send('登录'); }) const server = app.listen(3000,function(){ console.log('Server is running on http://localhost:3000'); });
可以看的虽然运行了,但是怎么没有内容呢,因为我们的触发值是login,我们再访问login看看
访问成功了,现在我们就引用前面的前端页面
const express = require('express'); const path = require('path'); // 引入 path 模块 const app = express(); app.get('/login', function (req, res) { console.log('login'); res.send('登录'); }); app.get('/', function (req, res) { // 使用 path.join 构造绝对路径 const filePath = path.join(__dirname, 'no-login.html'); res.sendFile(filePath); }); const server = app.listen(3000, function () { console.log('Server is running on http://localhost:3000'); });
可以看到确实渲染成功了捏,现在我们就然他传参过去,在html里的表单里把表单提交加回去
<form action="no-test.js" method="post">
发送之后可以看到他成功的接收到了发送的数据,但是我们由于还没有写接受呢,他这里暂时肯定是无法请求的,所以我们来写一下后台判断
app.post("/login", function (req, res) { const username = req.query.username; const password = req.query.password; console.log(username); console.log(password); if(username == "admin" && password == "123456"){ res.send("byd登录成功了捏"); }else{ res.send("byd账号密码错了,检查一下ok?"); } });
简单的接收前面传过来的值然后进行一个判断
可以看到我们输入账号密码的时候通过post传入我们写的login用法后就进行了判断,然后他就g了哈哈
可是当我们输入正确的账号密码时
这里却显示的是这个,不和我们的判断条件匹配这怎么办,这里是因为我们使用的是
他在官方手册中说的是获取url中的参数,也是就是通过get传参时才能正常的获取到参数
我们来试试
通过get传参,他就能正常的获取到值,所以成功了捏,但是这样传值不是很安全,所以我们还是要使用post,仔细理解一下其实就明白了,post传参的时候,你通过抓包,可以知道他是在那个包的body中,所以说其实我们真正要返回的是这里的值,得用对用的参数
所以我们来试一下post传参,首先使用之前先安装库
npm install body-parser //中间件,用来处理json,Raw,Text和URL编码的数据
如果你的powershell禁用脚本,记得输入一个这个
Set-ExecutionPolicy RemoteSigned
然后即可以正常安装了,安装好之后我们引用他,和创建他
const bodyParser = require('body-parser');//引入模块 const urlencodeParser = bodyParser.urlencoded({extended: false}) //post路由 app.post("/login",urlencodeParser,function (req, res) { const username = req.body.username; //接收body值 const password = req.body.password; console.log(username); console.log(password); if(username == "admin" && password == "123456"){ res.send("byd登录成功了捏"); }else{ res.send("byd账号密码错了,检查一下ok?"); } });
然后我们来访问看看
现在他就能正常的接收了,非常好用~既然我们现在判断逻辑完成了,那肯定是要连接数据库咯,而这里面我们又该怎么连接数据库呢
首先当然是安装库咯
npm install mysql //安装相关库
然后同样的我们要在前面调用一下
const mysql = require('mysql'); //连接数据库 var connection = mysql.createConnection({ host : 'localhost', user : 'mikukuku123', password : 'mikukuku123', database : 'demo01' }); connection.connect(); const sql = 'select * from admin'; connection.query(sql,function(error,data){ if(!error){ console.log("数据库连接成功!") } });
先简单的测试一下功能是否正常,只要判断不为错误,就显示成功
由于我本机没开数据库哈,我就依旧用另一台机器,不影响,可以看到他已经连接成功了,查看一下他data的sql查询的输出
查询到了我们账号信息,接下来我们就可以走正常流程了
由于他这里是一个列表对象,我们取值的时候就要带上他的指针
console.log(data[0]['username']); console.log(data[0]['password']);
这样就正常的取出了值,然后我们对这个值进行判断
现在我们把完整的代码写出来
const express = require('express'); const path = require('path'); // 引入 path 模块 const bodyParser = require('body-parser');//引入模块 const mysql = require('mysql'); const app = express(); const urlencodeParser = bodyParser.urlencoded({extended: false}) //连接数据库 var connection = mysql.createConnection({ host : 'localhost', user : 'mikukuku123', password : 'mikukuku123', database : 'demo01' }); connection.connect(); // 登录接口(POST) app.post("/login", urlencodeParser, function (req, res) { const username = req.body.username; const password = req.body.password; // 构造 SQL 查询 const sql = `SELECT * FROM admin WHERE username='${username}' AND password='${password}'`; // 查询数据库 connection.query(sql, function (error, results) { if (error) { console.log("数据库查询失败!", error); res.send("服务器错误"); return; } if (results.length > 0) { // 用户存在且密码正确 res.send("byd登录成功了捏"); } else { // 用户不存在或密码错误 res.send("byd账号密码错了,检查一下ok?"); } }); }); app.get('/', function (req, res) { // 使用 path.join 构造绝对路径 const filePath = path.join(__dirname, 'no-login.html'); res.sendFile(filePath); }); const server = app.listen(3000, function () { console.log('Server is running on http://localhost:3000'); });
通过用户输入的参数然后进行对数据库的比对,如果数据库查询到了正确的数据,就会进入正确的判断,然后就实现了登录操作捏
没有任何问题呢
还有一个小细节
if (error) { console.log("数据库查询失败!", error); res.send("服务器错误"); return; }
我们在这里添加了一个服务器错误的返回,这是容错处理,如果前面用户输入了错误对的用户名或者密码导致判断流程到后面就一直报错,然后程序就会崩溃,只要是用户输入错误后导致sql语句查询失败然后直接return,不会让他继续执行后面的语句了,这样就不会导致程序崩溃
既然又说到数据库了,那必须再来看看这个sql注入了,首先我们前面写好了登录的逻辑,通过sql语句来查询账号信息,我们输出一下登录的时候sql语句他干了啥
可以看到下面sql语句的输出,你在前面通过表单提交过来的数据他这里就接收到了,然后我们去数据库中实验一个语句
首先我们正常的查询的时候,数据库会返回一个正确的结果
而错误的呢,就会什么都查不到,但是如果我们通过构造语句来让他不判断后面的password呢
就比如添加一个#号来注释掉后面的判断,可以看到他也是能够成功的查询,要是我们让账号永远为真呢
具体的用法后面再说,现在我们可以看到完整的语句就是 123' or 1=1 # 简单概括一下就是让条件永远为真,也就是说不管你前面输入的啥账号他后面永远都为真,那么数据库就会正常的接收查询,是不是非常的害怕捏,我们来实践一下
因为代码中我们没有对符号进行过滤,直接在登录框中输入即可,加单引号是因为要闭合前面的username''的这里的两个引号,不然你输入的全部都在username这个字段中,然后登录
直接是给我干哪来了,这还是普通用户吗,甚至随便输入的账号都能登录,可以看到在我们输入的调试语句中,他都能正常的接收,说明我们的语句没有任何问题,这就是sql注入的危害啊
再来一次 哦~可怕的sql注入
这是由于我们查询数据库的时候语言写的过于简单导致的,然后还没有对用户的输入进行检测,正常来说一个输入框肯定不能接受这种奇奇怪怪的的符号啊,所以我们有好几种解决方案
一个就是预编译,还有就是前面说过的白黑名单策略,具体我们后面说到具体的sql注入的时候再说。
我们继续来说关于文件管理的,依旧依旧哈,当然是首先创建一下文件啦
首先关于文件的话先安装模块
npm install fs
no-file.js
const fs = require('fs'); fs.readdir('./',function(error,files){ console.log(files); })
先简单的看一下他的使用,./读取当前目录,然后通过files输出
可以看到当前的这个js目录的所有文件哈,然后我们在浏览器中看一下
const fs = require('fs'); const express = require('express'); const { fileURLToPath } = require('url'); const app = express(); app.get('/file',function(req,res){ const dir = req.query.dir; console.log(dir); filemanage(dir); }) var server = app.listen(3000,function(){ console.log("网站已开:"); }) function filemanage(dir){ fs.readdir(dir,function(error,files){ console.log(files); })}
通过调用dir值来让用户选择查看文件
当你输入./的时候就是读取当前文件夹,而../呢,那就是上级文件夹了
没有任何问题捏,当然我们没有对用户输入进行任何限制,所以这里有我们前面说过的目录遍历的安全风险
这个就没啥好说的
然后就是他关于命令执行的问题
命令执行分两种,我们前面做过一个rce的实验,执行了系统命令直接传了个木马上去,他还有一个是代码执行,我们来看看
首先创建一个rce.js文件
第一步当然是导入库拉
npm install child_process
然后首先来尝试一下
const rce=require('child_process'); rce.exec('calc');
众所周知啊,exec是调用系统命令,calc是计算器命令,你win+R输入这个命令就会直接弹出计算器,我们运行这个代码试试
可以看到他也是直接把我计算器给干出来了捏当然还有nodepad记事本,这些自己了解一下命令就知道了
还有一个系统命令执行的参数
也是能实现同样效果的哈
还有一个就是代码执行
eval('console.log(1);');
这个eval我们前面也见识过了,就是让他执行里面的代码,我们之前也使用过然后上传木马
运行后让控制台输出1,假如我们要上传木马呢
eval(` const fs = require('fs'); fs.writeFileSync('hacked.php', '<?php eval($_POST[1]);?>'); console.log('你完了,你电脑被黑了'); `);
直接是全部写进去就完事
然后你的电脑里就被我生成木马了,嘻嘻嘻
简单的了解一下这个命令执行的作用,具体使用呢我们还是以后再说
关于他的安全问题我们上面已经提到过几个,sql注入啊,文件遍历啊,还有刚刚的一个rce
他还有一个比较有特点的安全问题:原型链污染 不过这个是ctf比赛里才喜欢考的题,平常我们基本遇不到
而他是什么意思呢
如果攻击者控制而且修改了一个对象的原型(_proto_)
那么他就可以直接影响所有跟这个对象来自用一个类,父祖类的对象
在面向对象的中的开发中,总是会提到继承这种说法,举个例子就明白了
//创建一个js对象 let foo = {bar : 1} //此时foo.bar为1 console.log(foo.bar)
//修改foo的原型,即Object foo.__proto__.bar = 2 //由于查找的顺序原因,foo.bar仍然为1 console.log(foo.bar)
//此时再创建一个空的对象 let zoo = {} //查看zoo.bar ,此时为2 console.log(zoo.bar)
这时候就有疑惑了,我明明前面设置的是foo的变量,为什么这里zoo是空的却把上面的bar值给继承过来了呢
首先我们要明白他查找值的逻辑
自己有没有这个属性?
→ 没有就去原型对象(__proto__
)上找
→ 再没有就去原型的原型找,直到 null
然后我们修改 foo
的原型
其实也就是变成了
foo.__proto__.bar = 2; ------> Object.prototype.bar = 2;
给所有对象的默认原型加了一个 bar = 2
zoo
是个空对象,自己没有 bar
属性
所以 JS 会去 zoo.__proto__
查找(也就是 Object.prototype
)
你之前在 Object.prototype.bar = 2
,所以就会输出2
再来看一个例子
let foo = {bar: 1} console.log(foo.bar) foo.__proto__.bar = `require('child_process').execSync('calc');` let zoo = {} console.log(eval(zoo.bar))
和上面差不多,通过修改对象原型,来污染所有的bar,虽然zoo没有赋值,但是他通过查找原型然后接受到了这个代码,通过eval把他执行了,这就是经典的原型链污染
前面也说过了,这个node.js他是一个写在后端的,前端我们无法看到代码,所以说关于他的安全问题就是基本上做的都是黑盒测试,首先框架识别的话我们前面也说过,直接小插件基本都能识别出来
比如我们前面写的文件遍历,他就可以直接识别到时这个框架搭建的,白盒更不用多说了,代码审计一下看有没有一些不安全的写法这个我们在前面开发的时候已经见识到很多了哈
那么这期也差不多了,简单的学习了一下使用框架来开发web应用,然后知道了对应的安全问题,现在如果给你一份代码现在也能看的懂一点咯,这也是我们学习开发的作用哈,这期就结束咯
未完待续~~