*CTF2019-996game复现/nodejs+mongodb使用

前言

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

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

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


环境搭建

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

1
docker pull node:12-alpine

然后:

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

接着把 mongodb 装上:

1
2
apk update
apk add mongodb

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

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

所以先把目录建好:

1
mkdir -p /data/db

然后运行:

1
mongod &

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

1
2
3
4
5
6
7
8
9
10
/ # 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 数据库:

1
2
3
4
5
6
7
8
9
10
11
> 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,可以看到现在已经可以远程访问了:

1
2
3
4
/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 把依赖装好):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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 即可看到成功查询:

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

题目分析

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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);
});
};

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

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

我们在 mongodb shell 试一试:

1
2
3
4
5
6
7
8
9
10
11
12
13
> 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”} 试一试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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}},即可看到回显报错可控:

1
{"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


*CTF2019-996game复现/nodejs+mongodb使用
http://yoursite.com/2019/05/05/CTF2019-996game复现-nodejs-mongodb使用/
作者
Aluvion
发布于
2019年5月5日
许可协议