8

几则NodeJS的安全问题 – Wupco's Blog

 4 years ago
source link: http://www.wupco.cn/?p=4520
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

笔者在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()结果可控

发现问题的一句是

xJWZfZO.png
这里的id是完全可以由client控制的,我就在想会不会经过ObjectId之后结果仍然可控呢? 于是带着这份好奇就跟了进去,发现在ObjectId的函数流程中,有使用Javascript的特定方法取得输入的长度。
https://github.com/mongodb/js-bson/blob/V1.0.4/lib/bson/objectid.js#L28

5yHSHqK.png

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

To illustrate

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

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

然后接下来我们继续看

BRt4FOG.png

如果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的相应库很有意思

zYAH8MC.png

他检测了数据是否有_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
相关代码如下

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: '[😁] New password',
      text: 'Your new password: ' + newpass

    };

      const info = await transporter.sendMail(message);
      return new HttpResponseRedirect('/signin');
JavaScript

我们已经知道MongoDb可以通过传入不存在的bsontype来选中第一个用户,但是这里ctx.request.body.email如果是一个Object,怎么让它把邮件发送到你可以控制的邮箱里呢?

通过审计nodemailer(https://github.com/nodemailer/nodemailer)的代码,我发现如果to这个object存在address这个属性,那么就会向address对应的地址发送邮件。

NRndW9T.png

然而这个用法并没有在官方文档提及到,所以开发者自然不会意识到有这种问题。

从而这个利用payload就可以为{"email":{"address":"your email address","_bsontype":"a"},...}
任意修改第一个用户密码

class-validator

这个库的问题其实很有趣。早在0CTF2019 Final,我们就可以看到RR师傅出了一个有关nestjs的漏洞的题。
https://github.com/zsxsoft/my-ctf-challenges/blob/master/calcalcalc-family/2.md

https://github.com/nestjs/nest/commit/b3f51cc9c110017ebc8f6ec12da6b92d96f37caa#diff-11537d189f48488671d5be4d1c5daffeR97

但实际上,造成这个问题的本身原因是class-validator. 我们也可以看到nestjs的patch仅仅为

O3wZOQ8.png

class-validator这个数据类型检测器被广泛使用在各个Web框架里,通常与Body解析器构成触发式验证。但是如果Body中存在proto这个键的话,就会直接跳过验证。

这里有个我之前针对calcgame的分析
https://hackmd.io/@ZzDmROodQUynQsF9je3Q5Q/S1kUedPdS

这一点我也应用到RealworldCTF 2019中了

request

https://github.com/request/request
request库存在参数har可以覆盖请求方式等参数。

2wobaAs.png

RealWorldCTF 2019 Final中

async checkstatus(ctx: Context) {
    await rp.head(ctx.request.body.url).then(() => { this.status = true; },
        () => { this.status = false; } );
    return new HttpResponseRedirect(this.status ? '/admin?alive=true' : '/admin?error=true');
  }
JavaScript

可以通过传入 {"url":{"har":"POST"}} 更改head请求方式为POST

pomelo

pomelo是一个非常老的网易的HTML5游戏框架
https://github.com/NetEase/pomelo
但是其handler存在比较明显的问题,可导致服务器崩溃或其他问题。
https://github.com/NetEase/pomelo/blob/5f7bec1c9be5fa2f88a366b15085cf64d4e786d7/lib/common/service/handlerService.js#L59
它根据客户端传过来的route a.b.c以及msg d,用来调用任意一个a.b.c(d)。于是存在很多可以down掉服务端的办法,甚至可能是RCE。
我当时寻找到的例子是
https://github.com/NextZeus/lordofpomelo

通过pomelo.request('connector.entryHandler.constructor', { get:{} }
调用到下面这段app初始化代码

var Handler = function(app) {
    this.app = app;

    if(!this.app)
        logger.error(app);
};
JavaScript

this.app赋值为{"get":{}}
这样服务器每当调用get方法的时候,就会报错。因为这里的get覆盖掉了app原生的get方法。

json-sql

json-sql是一个database前置中间件,它为开发者提供了可以形成SQL查询语句的接口
https://github.com/2do2go/json-sql
但是通过审计代码笔者发现这个库存在着问题,如果用户输入是一个Object,那么将有机会形成可以SQL注入的语句。(注意这个库正常是将动态查询参数作为预编译进行处理的)

下面是例子,注意name和id同为查询的condition,正常来说,应该是name所对应的情况,也就是应该做预编译处理。
但是如果我对id输入一个Object,同时指定了两个hidden parameters (cast 或 alias),就形成了可以SQL注入的机会(直接改变原始SQL语句)

var jsonSql = require('json-sql')();

function testselect(query) {
    var sql = jsonSql.build({
        type: 'select',
        table: 'users',
        fields: ['name', 'age'],
        condition: query
    });
    console.log(sql);
}

testselect({"name":"wupco","id":{"cast":"aaa'\"bbb"}});
console.log("\n[*] result of hipar : alias\n");
testselect({"name":"wupco","id":{"alias":"aaa'\"bbb"}});
/*
[*] result of hipar : cast
{
  query: `select "name", "age" from "users" where "name" = $p1 and cast("id" as aaa'"bbb);`,
  values: { p1: 'wupco' },
  prefixValues: [Function: prefixValues],
  getValuesArray: [Function: getValuesArray],
  getValuesObject: [Function: getValuesObject]
}
[*] result of hipar : alias
{
  query: `select "name", "age" from "users" where "name" = $p1 and "id" as "aaa'"bbb";`,
  values: { p1: 'wupco' },
  prefixValues: [Function: prefixValues],
  getValuesArray: [Function: getValuesArray],
  getValuesObject: [Function: getValuesObject]
}
*/
JavaScript

上面几个问题都是Javascript语言特性带来的问题。当然大部分和当代Node Web框架脱离不了干系,可以很轻松的转换数据传输方式(大部分Web框架接受多种数据传输方式,特别是只需要更改request头部的content-type为json就可以使用json来传输数据)。这样存在的问题是大部分Web框架其实是通过Object来处理信息的(因为都是将json解析成Object)。

不合理使用node modules,或者各个modules官方文档没有给出足够的说明,就会导致漏洞的产生,这种漏洞大多是dos漏洞或者逻辑漏洞。

在使用nodejs进行开发的时候,要注意时刻检查输入类型,不能轻信第三方validator或check,这些说不定本身就存在问题,例如class-validator,是使用最多的数据检查器,但却被轻而易举的绕过,并且还不修复。

其实上面很多bug都很简单,简单到不能算作漏洞,但是组合起来可能就造成严重漏洞。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK