MongoDB操作语句及注入

  8 mins to read  

List [CTL]

    首发于安恒公众号

    这几天读了一本《MongoDB权威指南》的动物书,简单了解了这类nosql(Not Only SQL)数据库的操作语句。书上的业务层面以及sql优化部分我就略过了。书上是2.x版本的MongoDB,部分语句已经废弃了,在踩坑后我把那些删掉了

    Mongo的最大特点就是存储数据格式的多样化和反关系范式:

    • 相对于关系化数据库的”矩形”存储格式,Mongo可以存储任意的json格式数据,例如{'u': 'admin', 'pwd': '***'}。当然有利必定有弊,假如存储的是格式化的数据,如用户表这样每一行都是相同字段,那么每一行都需要存储一遍字段名,解决办法?字段名取短一点,就像我举得例子一样
    • 反关系范式需要好好设计,根据读写比例,在数据冗余的数据引用之间做好平衡。比如用户表里的用户粉丝,我会这样设计:{'u': 'admin', 'e': 'x@x.cc', 'tel': '110', 'sex': 1, 'fs': [{'u': 'user1', 'sex': 1}, {...}]}。经常和用户一起查询出的粉丝信息内嵌入文档中(用户名,性别),避免跨表查询;而其余信息则使用用户引用查询(邮箱,电话)

    核心思想

    MongoDB中,没有表、列的概念,取而代之的是集合(collection)、文档(document)

    • 库(DB):
      • 数据库,包含多个集合
      • 当前数据库名存储在全局变量db
      • admin库,库中用户有数据库所有权限local库,不可复制,本地数据库config库
    • 集合(Collection):
      • 一组文档
      • 以名称进行标识,不能为空字符串
      • 不能以system.开头,不能使用字符$(特殊含义字符)
    • 文档(Document):
      • 键值对的一个有序集,即有序的哈希表
      • 键值不能有'\0'
      • 不能存在.$
      • 区分类型("1"1)与大小写

    MongoDB内置JavaScript解释器,它的文档是JS中的对象({...}),就是那种没成员函数的对象

    MongoDB运行于27017端口,且启动后会在28017端口启动一个基本的Http服务器。启动后会连接到test数据库,use [database]切换当前数据库

    MongoDBBSON格式保存数据,即Binary Json


    CRUD操作

    观察发现:MongoDB中的方法函数基本都是以JavaScript命名习惯的,即word1Word2Word3,如addEventListener

    文档插入后会自动添加一个_id属性,为唯一标识符{"_id": ObjectId("56064886ade2f21f36b03134")}


    Create

    创建数据库

    > use [newdbname]		//不存在则会创建
    

    创建collections

    db.createCollection()
    

    创建集合

    集合的insert方法,插入单个文档

    db.collection.insert({
        key1: value1,
        ley2: value2
    })
    

    Read

    集合的find方法,第一个参数是用来确定返回的文档,第二个参数确定返回的键值的过滤条件

    db.collection.find()	// 返回全部文档
    db.collection.find({"age": 20})		// 按条件查询
    db.collection.find({
    	"age": 20,
        "name": "ivan"
    })		// condition1 && condition2
    

    返回指定键值

    db.collection.find({}, {key1: 1})		// 会返回"_id"key1
    db.collection.find({}, {key1: 0})		// 不返回key1
    

    查询条件

    $lt$lte$gt$gte$ne
    <<=>>=!=
    db.collection.find({
    	"age": {"$gt": 18, "$lt": 30}                   
    })			// 返回年龄18< <30
    
    $in$nin$or$not$size
    指定列表,成员可为不同类型不在列表中条件逻辑或元操作符,可用于其余任何条件大小
    db.collection.find({
    	"number": {"$in": [1, 2, 3]}
    })
    db.collection.find({
    	"$or": [{key1: value1}, {key2: value2}]                   
    })
    db.collection.find({
    	"$not": {"age": 20}                   
    })
    db.collection.find({
    	"name": {"$size": 4}                   
    })
    

    null值

    db.collection.find({key1: null})		// 会返回所有无key1键的文档
    

    RegExp

    db.collection.find({"name": /^[a-z]{0,4}$/i})		// 可加入正则flag位,如i忽略大小写
    

    $slice,返回键中数组切片

    db.collection.find({}, 
                      {"comments": {"$slice": 10}})		// 返回前十切片
    db.collection.find({}, 
                      {"comments": {"$slice": 10}})		// 返回后十切片
    db.collection.find({}, 
                      {"comments": {"$slice": [23, 10]}})		// 返回24~33切片,神奇的左开右闭
    

    $where,危险语句,可执行任意JS函数

    db.collection.find({"$where": function(){
                       for (var i in this){
                           for (var j in this){
                               if (i!=j && this[i]==this[j])
                                   return true;
                           }
                       }
                       return false;
                       }})
    

    其实就是传入一个返回值为bool的匿名函数,对文档进行选择

    游标

    var cursor = db.collection.find();
    while (cursor.hasNext()){
        //do sth by use cursor.next()
    }
    

    limit、skip、sort方法

    db.collection.find().limit(3)
    db.collection.find().skip(3)
    db.collection.find().sort({key1: 1, key2: -1})			//键值1为升序,-1降序
    

    Update

    修改器

    $inc,原子操作,并发安全

    db.collection.update({key: value}, {"$inc": {a: 1}})		//find操作后键值a会自增1
    

    $set,存在则修改,不存在则创建,就像sql语句的REPLACE一样

    db.collection.update({"_id": "xxxxxx"}, {"$set": {key: value}})
    

    $unset,set的逆操作

    $push,修改文档数组,因为JavaScript的数组增删元素就是用push&pop

    $each,批量修改数组

    db.collection.update({}, {"$update": {key: {"$each": [val1, val2, val3]}}})
    

    $addToSet,同一般语言中set,去重

    upsert操作:指定update的第三个参数进行原子操作

    db.collection.update({}, {"$inc": {a: 1}}, true)		//trueupsert操作
    

    更新多个文档,指定第四个参数

    db.collection.update({}, {"$set": {key: 1}}, false, true)		//true为更新多个文档
    

    Delete

    db.collection.remove()		//删所有文档
    db.collection.remove({key: value})		//指定查询条件
    db.collection.drop()		//删集合
    db.dropDatabase()		删库
    

    NoSQL注入

    我将MongoDB装在我的Debian 9虚拟机上,创建了以下内容:

    > use sqli
    switched to db sqli
    > db
    sqli
    > db.createCollection("users")
    { "ok" : 1 }
    > db.users.insert({"uname": "admin", "passwd": "admin"})
    WriteResult({ "nInserted" : 1 })
    > db.users.insert({"uname": "iv4n", "passwd": "iv4n"})
    WriteResult({ "nInserted" : 1 }
    > db.users.find()
    { "_id" : ObjectId("5ba3412314139eac63f891f7"), "uname" : "admin", "passwd" : "admin" }
    { "_id" : ObjectId("5ba3424114139eac63f891f8"), "uname" : "iv4n", "passwd" : "iv4n" }
    

    以下是PHP代码,这里是PHP 7.x,和5.x的mongoDB库使用有差异:

    <?php
    // init mongoDB engine
    $server = new MongoDB\Driver\Manager("mongodb://localhost:27017");
    
    $uname = $_POST["uname"];
    $passwd = $_POST["passwd"];
    $filter = ["uname" => $uname,
    		  "passwd" => $passwd,
    ];
    $option = [
    	"projection" => ["_id" => 0],
    	"sort" => ["_id"=> -1],
    ];
    
    $query = new MongoDB\Driver\Query($filter, $option);
    $cursor = $server->executeQuery("sqli.users", $query);
    foreach ($cursor as $doc){
    	if ($doc->passwd == $passwd){
    			echo "Login Successfully!<br>";
    			echo "username is: ".$doc->uname."<br>";
    			echo "password is: ".$doc->passwd."<br>";
    	} else {
    			echo "Invalid user and pass";													}
    }
    ?>
    

    index页面为一个post方法登录

    这里的filter为查询条件,语句为db.users.find({"uname": $uname, "passwd": $passwd})

    正常登录,用户名密码错误无回显


    $ne选择器注入,返回所有不等于的document,传入数据库的语句实际是db.users.find({"uname": {"$ne": "a"}, "passwd": {"$ne": "a"}})

    可以看到,返回了数据库的全部信息


    $lt/$gt注入:

    前面说过,MongoDB内置的是Javascript的解释器,所以它在字符串的大小判断也遵循JS的逻辑

    JS的字符串大小判断逻辑:按字节从左比较ascii码,假如相等则比较下一字节,不等则返回当前位的比较结果

    所以我们可以利用大小操作符来注入:


    正则注入:

    $regex,传入数据库的语句实际为db.users.find({"uname": {"$regex": "^a"}, "passwd": {"$ne": "a"}})

    可以看到,返回了以a开头的用户信息,实际上它和SQL的正则盲注是一样的道理


    未授权访问

    MongoDB最初安装部署后是不会添加auth选项的,一般的初始化步骤是:

    1. 不开启auth选项时连接数据库,添加管理员账户
    2. 开启auth,利用管理员账号登录连接,添加数据库账户

    但是很多开发者并不知道这些Tips,没有开启auth选项,且数据库监听了公网,就导致了MongoDB的未授权访问

    其实MongoDB的未授权访问和Redis数据库是差不多的,这里利用一个工具NoSQLMap来进行数据库信息枚举,有SQLMap那么也就有针对NoSQL数据库的NoSQLMap,它可以注入以及利用未授权访问漏洞

    我将数据库不开启auth启动,然后NoSQLMap的1选项设置想关信息

    接着点击2选项,提示存在未授权访问漏洞

    点击枚举数据库信息

    这个工具目前来说还不是很完善,动不动就会直接exception退出,和SQLMap相比差距还是很大,