技术分享:如何Hacking MongoDB?

db.jpeg

不管是商业项目还是个人项目,MongoDB都是一个非常好的数据库引擎,国内很多公司也开始用MongoDB。比起传统的数据库,这款数据库比较新,也有很多安全问题是大家还没有意识到的,而这些问题通常可以打得你措手不及。

本篇文章主要向大家介绍我在使用MongoDB的过程中遇到的问题,以及它是如何被用来修改数据库记录的。当然,利用过程很简单,不过其实各种方式的SQL注入技术说破了也就那么回事,但是依然有很多人容易犯这样的错误。

在我们开始前,我想先介绍下关于以下要用到的MongoDB的特性。MongoDB提供的更新机制是先定位到该文档,然后进行更新,如下例子:

{
  name:"John",
  info:{
      age:65
  }
}

如上面的记录,你可以通过以下语句对它进行更新:

db.people.update({"name":"John"}, {"$set":{"info.age":66}})

是不是很酷炫,好吧,知道大家早就懂了Clipboard Image.png

但是,如果子键不是硬编码的,又该如何呢?我们该如何通过变量将内容传进去呢?如下:

keyName = request.form|'keyName'|
keyData = request.form|'value'|
db.people.update({"name":"John"}, {"$set":{"info.{}".format(keyName):keyData}})

后台程序从前端请求中获取到key和value的值以后,通过参数传入MongoDB的更新函数中。那么问题来了,如果前端输入的是一个恶意的参数呢。

以下是我在处理一个未知用户输入时候产生的问题,为了说明,接下来我们写一段用来展示这个漏洞。代码如下:

from flask import *
import pymongo
import bson
import uuid
 
db = pymongo.MongoClient("localhost", 27017).test
 
form = """
<html><head></head><body>
<form method="POST">
<input type="text" name="username" placeholder="Username">
<input type="text" name="password" placeholder="Password">                                                                                                                                           
<input type="text" name="firstname" placeholder="Firstname">
<input type="text" name="lastname" placeholder="Lastname"/>
<input type="text" name="age" placeholder="Age">
<input type="submit" value="Submit">
</form></body></html>
    """
 
 
app = Flask(__name__)
app.secret_key = "secret"
 
@app.route("/logout/")
def logout():
    session.pop("_id")
    return redirect("/login/")
 
@app.route("/")
def index():
    if "_id" not in session:
        return redirect("/login/")
    name = request.args.get("name")
    lastname = request.args.get("lastname")
    if not name:
        return "<h1>Search for someone</h1><form method='GET'><input name='name' type='text' placeholder='First Name'><input name='lastname' type='text' placeholder='Last Name'><input type='submit'></form>"
    else:
        search_results = db.members.find_one({"{}".format(name):lastname})
        if search_results:
            search_results = name + " " + lastname + " is " + search_results['account_info']['age'] + " years old."
        return "{}<form><input name='name' type='text' placeholder='First Name'><input name='lastname' type='text' placeholder='Last Name'><input type='submit'></form>".format(search_results)
 
@app.route("/login/", methods=['GET', 'POST'])
def login():
    if request.method == "POST":
        username = request.form['username']
        password = request.form['password']
        check = db.members.find_one({"username":username, "password":password})
        if check:
            session['_id'] = str(check)
            return rediirect("/?name={}".format)
        else:
            return "Invalid Login"
    return "<h1>Login</h1>" + form 
 
@app.route("/signup/", methods=['GET', 'POST'])
def signup():
    if request.method == "POST":
        username = request.form['username']
        firstname = request.form['firstname']
        lastname = request.form['lastname']
        password = request.form['password']
        age = request.form['age']
        session['_id'] = str(db.members.insert({"username":username, "password":password, firstname:lastname, "account_info":{"age":age, "age":age, "isAdmin":False, "secret_key":uuid.uuid4().hex}}))
        return redirect("/")
    return "<h1>Signup</h1>" + form
 
@app.route("/settings/", methods=['GET', "POST"])
def settings():
    if request.method == "POST":
        username = request.form['username']
        firstname = request.form['firstname']
        lastname = request.form['lastname']
        password = request.form['password']
        age = request.form['age']
        db.members.update({"_id":bson.ObjectId(session['_id'])}, {"$set":{"{}".format(firstname):lastname, "account_info.age":age, "username":username}})
        return "Values have been updated!"
    return "<h1>Settings</h1>" + form
 
@app.route("/admin/", methods=['GET', 'POST'])
def admin():
    if "_id" not in session:
        return redirect("/login/")
    theUser = db.members.find_one({"_id":bson.ObjectId(session['_id'])})
    if not theUser['account_info']['isAdmin']:
        return "You do not have access to this page."
    if request.method == "POST":
        secret = request.form['secret_key']
        return str(db.members.find_one({"account_info.secret_key":secret}))
    return """<h1>Search user by secret key</h1>
    <form method="post"><input type="text" name="secret_key" placeholder="Secret Key"/><input type="submit" value="Serach"/></form>
    """
 
app.run(debug=True)

这个网站很简单。就是一个登陆页面,一个注册页面,一个设置页面,和一个index页面,用户可以在这些页面上输入他/她们的姓名,然后返回年龄,如下图。

Clipboard Image.png

Clipboard Image.png

需要注意的是,这段代码是很容易受到注入攻击的,接下来,我们来看看是如何进行注入的。

我们的目标是获得访问admin页面的权限。从网站代码中我们可以找到,后台是根据isAdmin字段来验证用户权限的,如下图

Clipboard Image.png

看一下后台的数据库大概是这样的:

Clipboard Image.png

其中,Firstname:Lastname是直接插入姓名的,看着很奇怪。

我们先创建一个用户,然后访问下/admin/页面,返回如下:

Clipboard Image.png

很好……果然没有权限访问。回顾下isAdmin可以用来控制该页面,也就是说,该用户在数据库中可能是这样的:

Clipboard Image.png

其中,firstname:lastname这一条是我们可控的,通过settings页面输入进去的,”username”, “password”, “firstname”, 和”lastname”实际上都是我们可以输入的,firstname:lastname在查询的时候是可以搞的,看起来似乎可以搞些文章。

把fistname改成account_info.isAdmin 并且把lastname改成”1 ”,1在python中代表的就是True。

Clipboard Image.png

点击Submint,发现修改成功了。

Clipboard Image.png

访问/admin/页面:

Clipboard Image.png

可以访问admin页面了,:D

同样的,要想用secret key查整个内容的话,可以这么做:

Clipboard Image.png

输入查询:

Clipboard Image.png

成功:

Clipboard Image.png

到此处,事实上我们能做的还有很多很多。我们可以用它来修改其他用户的账号密码,并且查看其他用户。在这里不多介绍。

很显然,这个网站的所有安全措施都没用了,敏感数据也变得危险。当然,当你的网站被攻击后,回过头来看代码,也许你会觉得自己的代码很搞笑,但这是绝对不容忽视的。好吧,其实我也犯过这样的错误,还好及时发现。

和关系型数据库的SQL注入一样,我们要做的就是过滤传入的参数。

好了,就酱紫啦~

此条目发表在未分类分类目录。将固定链接加入收藏夹。