本人在2019年对一些NodeJS问题的研究
2020-02-21 10:19:01 Author: xz.aliyun.com(查看原文) 阅读量:430 收藏

笔者在2019年为StarCTF(*CTF)出题的时候,发现了MongoDB提供的原生接口存在一些问题。在这之后经过与参赛选手的探讨,发现以上的问题其实比较有趣。接下来我将把产生这个问题的根源进行剖析,并记录一下由此发现的其他几个node modules的问题。

starctf-996game & calcgame (RCTF/0CTF/De1CTF) & MarxJS(RealworldCTF 2019)

996game这道题目我出完最开始觉得并不怎么好,因为说实话他是一个半成品(因为出题时间deadline,不能去深入挖掘)。HTML5游戏近几年也是比较火爆,因为它具有很强的跨平台兼容性。我当时就秉承着这么一个思想,想把游戏安全引入CTF Web题目中(当然,我并不是第一个这么干的人,之前很多CTF比赛都有游戏安全,举个国内的例子HCTF,笔者也是当年有幸拿到一血)。于是我便开始搜索比较热门的HTML5游戏框架或者源代码,最终在github锁定了phaserquest,虽然不是很热门,但因为比较老而且很久没更新,我觉得我很可能会发现其中存在的问题。一开始就发现了很多可以致使游戏崩溃的bug,但是我最后是被MongoDB有关的数据交互吸引了。

ObjectId()结果可控

发现问题的一句是

这里的id是完全可以由client控制的,我就在想会不会经过ObjectId之后结果仍然可控呢? 于是带着这份好奇就跟了进去,发现在ObjectId的函数流程中,有使用Javascript的特定方法取得输入的长度。

https://github.com/mongodb/js-bson/blob/V1.0.4/lib/bson/objectid.js#L28

我们都知道Javascript的变量类型String和Array,具有length这个原生属性,它标志着字符串的长度或数组的大小。那么,如果变量类型是Object呢? Object不存在原生的length属性,但如果开发者没有判断变量的类型,盲目的取objectxxx.length的话,就会取objectxxx对应键length下的内容。

To illustrate

var a = {"length":888, "name":"wupco"};
console.log(a.length);
//888

所以正是因为开发者没有考虑这个问题,可能带来很多逻辑上的问题,比如上面这个ObjectId的函数流程中,如果id.length == 12,就会直接返回true,代表是一个合法的ObjectID的格式,实际上,我们可以传入一个Object {"id":{"length":12}}来绕过这个函数。

然后接下来我们继续看

如果id存在toHexString方法的话,就直接返回id原来的内容。于是经过ObjectId这个函数的变化,我们的任何数据都没有被更改转化。(注意上面一句else if 用的是 id.length===12,所以可以用"12"字符串形式来绕过)

MongoDb的bson序列化问题

MongoDb的用户查询语句是如何进行传递的呢?这中间其实经过了一步bson序列化过程,这也是MongoDb独有的序列化过程。
它的具体代码可以在 https://github.com/mongodb/js-bson/blob/V1.0.4/lib/bson/parser/serializer.js
看到。
这个bson序列化的过程是遵循统一的一个标准来进行的,不止NodeJS存在这样的库,PHP,Python等常见编程语言都提供了这样的库。他们的具体流程都大致为

  1. 判断查询语句类型。
  2. 根据类型进行封装,同时加上特有的数据头部标识。

其中NodeJs的相应库很有意思

他检测了数据是否有_bsontype这个属性,然后根据这个属性来进行不同的序列化过程。于是我们很容易想到可以利用不同数据的格式特性,来打造不同的Object,序列化成我们想要的结果。例如上面的ObjectId函数处理结果仍然是一个Object,但是我们这样丢到Object对应的序列化函数中,产生的结果再传入MongoDb引擎里进行query解析,结果一定是会出错的,我们就需要换一种思路让它不会报错,就是用_bsontype控制它到其他分支去,利用序列化过程中的信息剔除,将我们欲绕过ObjectId构造的length等信息去掉。

在RCTF,0CTF等一系列的calc题目中,我们可以看到它所利用的点就是这里的问题。

https://github.com/zsxsoft/my-ctf-challenges

一个比较有趣的新问题 (应用在realworldCTF2019线下赛MarxJS)

StarCTF之后,在和选手的讨论中得知选手可以在996game中任意登陆第一个用户的账号,我们一起对原因进行了检查。

发现是由于在bson序列化的过程中,他传入的是一个不存在的_bsontype,然后在那些分支都走过之后,因为没有对应的bsontype,所以最终没有序列化任何query,于是造成了findone({})这种查询条件为空的情况,成功选中第一个用户

这个Bug十分有意义。例如在一个找回密码的场景,需要输入一个比较特别的id的时候findone({"userid":userinput)
这时候我们就可以利用这个技巧,让查询语句变成findone({}),从而更改第一个用户的密码,而第一个用户大多是admin用户。

nodemailer

有了上面这个case之后,我又在和别人的一个合作项目中负责看了下别的库的相似问题。接下来我会讲讲我发现的nodemailer这个库。

我是在寻找使用MongoDb的一些上层框架,在一个Web框架中发现它使用了nodemailer搭配MongoDb来实现找回密码的功能。

大致的情况与我在RealWorld CTF2019出的题目相似。
题目附件
https://github.com/5lipper/ctf/tree/master/rwctf19-final/marxjs
相关代码如下

```javascript
const user = await getMongoRepository(User).findOne({ email: ctx.request.body.email });
if (!user) {
return new HttpResponseBadRequest('user not found');
}
const newpass = await generateToken();
const passhash = await hashPassword(newpass);
const res = await getMongoRepository(User).updateOne(
{ email: ctx.request.body.email }, { $set: { password: passhash}});
if (!res) {
return new HttpResponseInternalServerError('something error.');
}
const transporter = createTransport(
Config.get('mailserver')
);
const message = {
from: Config.get('mailfrom'),
to: ctx.request.body.email,
subject: '[


文章来源: http://xz.aliyun.com/t/7237
如有侵权请联系:admin#unsafe.sh