CVE-2022-24796: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection') (CWE-78)
value: RaspberryMatic is a free and open-source operating system for running a cloud-free smart-home using the homematicIP / HomeMatic hardware line of IoT devices. A Remote Code Execution (RCE) vulnerability in the file upload facility of the WebUI interface of RaspberryMatic exists. Missing input validation/sanitization in the file upload mechanism allows remote, unauthenticated attackers with network access to the WebUI interface to achieve arbitrary operating system command execution via shell metacharacters in the HTTP query string. Injected commands are executed as root, thus leading to a full compromise of the underlying system and all its components. Versions after `2.31.25.20180428` and prior to `3.63.8.20220330` are affected. Users are advised to update to version `3.63.8.20220330` or newer. There are currently no known workarounds to mitigate the security impact and users are advised to update to the latest version available.
CWE description: The product constructs all or part of an OS command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended OS command when it is sent to a downstream component.
CVSS v3 base score: 9.8
Published: 2022-03-31T23:15Z
Repository: https://github.com/jens-maus/RaspberryMatic
Language: Other
Files changed: buildroot-external/patches/occu/0031-WebUI-Fix-FileUpload.patch, buildroot-external/patches/occu/0031-WebUI-Fix-FileUpload/occu/WebUI/www/config/fileupload.ccc
Commit date: 2022-03-14 11:36:29 +0100
Commit message: reworked fileupload.ccc in 0031-WebUI-Fix-FileUpload WebUI patch to
contain several security checks for a valid admin session id and
query string checks as well as omitting the critical use of "eval"
to parse the URL query string altogether. This should significantly
improve the security burden, thus fix a security issue raised by @qx-f7

Diff: diff --git a/buildroot-external/patches/occu/0031-WebUI-Fix-FileUpload.patch b/buildroot-external/patches/occu/0031-WebUI-Fix-FileUpload.patch
index 4e3639e6..6c5f3b6a 100644
--- a/buildroot-external/patches/occu/0031-WebUI-Fix-FileUpload.patch
+++ b/buildroot-external/patches/occu/0031-WebUI-Fix-FileUpload.patch
@@ -363,16 +363,22 @@
  
    if {[session_requestisvalid 8] > 0} then action_$action
  }
 --- occu/WebUI/www/config/fileupload.ccc.orig
 +++ occu/WebUI/www/config/fileupload.ccc
-@@ -0,0 +1,61 @@
+@@ -0,0 +1,109 @@
 +#!/bin/sh
-+# shellcheck shell=dash disable=SC2169,SC2034,SC2154
++# shellcheck shell=dash disable=SC3036,SC3010,SC2034,SC3060,SC2116,SC3045 source=/dev/null
 +
 +echo -ne "Content-Type: text/html; charset=iso-8859-1\r\n\r\n"
 +
++# allow only POST requests
++if [[ "${REQUEST_METHOD}" != "POST" ]]; then
++  echo "ERROR: no POST request"
++  exit 1
++fi
++
 +# fake read boundary+disposition, etc.
 +read -r boundary
 +read -r disposition
 +read -r ctype
 +read -r junk
@@ -385,25 +391,67 @@
 +
 +# Due to \n\r line breaks we have 2 extra bytes per line read,
 +# 6 + 2 newlines == 10 junk bytes
 +a=$((a*2+b+c+d+10))
 +
-+# extract all params from QUERY_STRING
-+# shellcheck disable=SC2046,SC2116,SC2086
-+eval $(echo ${QUERY_STRING//&/;})
-+
 +# calculate the expected content length using
 +# HTTP_CONTENT_LENGTH or CONTENT_LENGTH
-+if [ -z "${HTTP_CONTENT_LENGTH}" ]; then
++if [[ -z "${HTTP_CONTENT_LENGTH}" ]]; then
 +  HTTP_CONTENT_LENGTH=${CONTENT_LENGTH}
 +fi
 +SIZE=$((HTTP_CONTENT_LENGTH-a))
 +
++# continue only if SIZE > 0
++if [[ "${SIZE}" -le 0 ]]; then
++  echo "ERROR: POST size <= 0"
++  exit 1
++fi
++
++# extract known params from QUERY_STRING only
++while IFS= read -r -d '&' KEYVAL && [[ -n "$KEYVAL" ]]; do
++  case ${KEYVAL%%=*} in
++    url) url=${KEYVAL#*=} ;;
++    sid) sid=${KEYVAL#*=} ;;
++    action) action=${KEYVAL#*=} ;;
++    downloadOnly) downloadOnly=${KEYVAL#*=} ;;
++  esac
++done <<END
++$(echo "${QUERY_STRING}&")
++END
++
++# check for url and action parameter
++if [[ -z "${url}" ]] || [[ -z "${action}" ]]; then
++  echo "ERROR: missing required URL parameters"
++  exit 1
++fi
++
++# check for a valid ADMIN session id
++if [[ "${#sid}" -eq 12 ]]; then
++  # parse the current version
++  [[ -r /VERSION ]] && . /VERSION
++
++  # use CCU.getVersion which is allowed only for Admins
++  RES=$(/usr/bin/curl http://127.0.0.1/api/homematic.cgi \
++                      -H 'Content-Type: application/json' \
++                      -d "{\"method\":\"CCU.getVersion\",\"params\":{\"_session_id_\": \"${sid//@}\"}}")
++
++  # check the curl result contains the current
++  # version number or not
++  if ! echo "${RES}" | grep -q "${VERSION}"; then
++    echo "ERROR: no valid admin session id"
++    exit 1
++  fi
++else
++  echo "ERROR: invalid session id"
++  exit 1
++fi
++
 +# write out the data
 +filename=$(mktemp -p /usr/local/tmp)
 +if ! /usr/bin/head -q -c ${SIZE} >"${filename}"; then
-+  echo "ERROR (head)"
++  echo "ERROR: head failure"
++  exit 1
 +fi
 +
 +echo "<html>"
 +echo " <head>"
 +echo "  <script>"
diff --git a/buildroot-external/patches/occu/0031-WebUI-Fix-FileUpload/occu/WebUI/www/config/fileupload.ccc b/buildroot-external/patches/occu/0031-WebUI-Fix-FileUpload/occu/WebUI/www/config/fileupload.ccc
index 9a0191cd..4061f310 100755
--- a/buildroot-external/patches/occu/0031-WebUI-Fix-FileUpload/occu/WebUI/www/config/fileupload.ccc
+++ b/buildroot-external/patches/occu/0031-WebUI-Fix-FileUpload/occu/WebUI/www/config/fileupload.ccc
@@ -1,10 +1,16 @@
 #!/bin/sh
-# shellcheck shell=dash disable=SC2169,SC2034,SC2154
+# shellcheck shell=dash disable=SC3036,SC3010,SC2034,SC3060,SC2116,SC3045 source=/dev/null
 
 echo -ne "Content-Type: text/html; charset=iso-8859-1\r\n\r\n"
 
+# allow only POST requests
+if [[ "${REQUEST_METHOD}" != "POST" ]]; then
+  echo "ERROR: no POST request"
+  exit 1
+fi
+
 # fake read boundary+disposition, etc.
 read -r boundary
 read -r disposition
 read -r ctype
 read -r junk
@@ -17,25 +23,67 @@ d=0
 
 # Due to \n\r line breaks we have 2 extra bytes per line read,
 # 6 + 2 newlines == 10 junk bytes
 a=$((a*2+b+c+d+10))
 
-# extract all params from QUERY_STRING
-# shellcheck disable=SC2046,SC2116,SC2086
-eval $(echo ${QUERY_STRING//&/;})
-
 # calculate the expected content length using
 # HTTP_CONTENT_LENGTH or CONTENT_LENGTH
-if [ -z "${HTTP_CONTENT_LENGTH}" ]; then
+if [[ -z "${HTTP_CONTENT_LENGTH}" ]]; then
   HTTP_CONTENT_LENGTH=${CONTENT_LENGTH}
 fi
 SIZE=$((HTTP_CONTENT_LENGTH-a))
 
+# continue only if SIZE > 0
+if [[ "${SIZE}" -le 0 ]]; then
+  echo "ERROR: POST size <= 0"
+  exit 1
+fi
+
+# extract known params from QUERY_STRING only
+while IFS= read -r -d '&' KEYVAL && [[ -n "$KEYVAL" ]]; do
+  case ${KEYVAL%%=*} in
+    url) url=${KEYVAL#*=} ;;
+    sid) sid=${KEYVAL#*=} ;;
+    action) action=${KEYVAL#*=} ;;
+    downloadOnly) downloadOnly=${KEYVAL#*=} ;;
+  esac
+done <<END
+$(echo "${QUERY_STRING}&")
+END
+
+# check for url and action parameter
+if [[ -z "${url}" ]] || [[ -z "${action}" ]]; then
+  echo "ERROR: missing required URL parameters"
+  exit 1
+fi
+
+# check for a valid ADMIN session id
+if [[ "${#sid}" -eq 12 ]]; then
+  # parse the current version
+  [[ -r /VERSION ]] && . /VERSION
+
+  # use CCU.getVersion which is allowed only for Admins
+  RES=$(/usr/bin/curl http://127.0.0.1/api/homematic.cgi \
+                      -H 'Content-Type: application/json' \
+                      -d "{\"method\":\"CCU.getVersion\",\"params\":{\"_session_id_\": \"${sid//@}\"}}")
+
+  # check the curl result contains the current
+  # version number or not
+  if ! echo "${RES}" | grep -q "${VERSION}"; then
+    echo "ERROR: no valid admin session id"
+    exit 1
+  fi
+else
+  echo "ERROR: invalid session id"
+  exit 1
+fi
+
 # write out the data
 filename=$(mktemp -p /usr/local/tmp)
 if ! /usr/bin/head -q -c ${SIZE} >"${filename}"; then
-  echo "ERROR (head)"
+  echo "ERROR: head failure"
+  exit 1
 fi
 
 echo "<html>"
 echo " <head>"
 echo "  <script>"

Vulnerable code: @@ -0,0 +1,61 @@
+# shellcheck shell=dash disable=SC2169,SC2034,SC2154
+# extract all params from QUERY_STRING
+# shellcheck disable=SC2046,SC2116,SC2086
+eval $(echo ${QUERY_STRING//&/;})
+
+if [ -z "${HTTP_CONTENT_LENGTH}" ]; then
+  echo "ERROR (head)"
# shellcheck shell=dash disable=SC2169,SC2034,SC2154
# extract all params from QUERY_STRING
# shellcheck disable=SC2046,SC2116,SC2086
eval $(echo ${QUERY_STRING//&/;})

if [ -z "${HTTP_CONTENT_LENGTH}" ]; then
  echo "ERROR (head)"
Fixed code: @@ -0,0 +1,109 @@
+# shellcheck shell=dash disable=SC3036,SC3010,SC2034,SC3060,SC2116,SC3045 source=/dev/null
+# allow only POST requests
+if [[ "${REQUEST_METHOD}" != "POST" ]]; then
+  echo "ERROR: no POST request"
+  exit 1
+fi
+
+if [[ -z "${HTTP_CONTENT_LENGTH}" ]]; then
+# continue only if SIZE > 0
+if [[ "${SIZE}" -le 0 ]]; then
+  echo "ERROR: POST size <= 0"
+  exit 1
+fi
+
+# extract known params from QUERY_STRING only
+while IFS= read -r -d '&' KEYVAL && [[ -n "$KEYVAL" ]]; do
+  case ${KEYVAL%%=*} in
+    url) url=${KEYVAL#*=} ;;
+    sid) sid=${KEYVAL#*=} ;;
+    action) action=${KEYVAL#*=} ;;
+    downloadOnly) downloadOnly=${KEYVAL#*=} ;;
+  esac
+done <<END
+$(echo "${QUERY_STRING}&")
+END
+
+# check for url and action parameter
+if [[ -z "${url}" ]] || [[ -z "${action}" ]]; then
+  echo "ERROR: missing required URL parameters"
+  exit 1
+fi
+
+# check for a valid ADMIN session id
+if [[ "${#sid}" -eq 12 ]]; then
+  # parse the current version
+  [[ -r /VERSION ]] && . /VERSION
+
+  # use CCU.getVersion which is allowed only for Admins
+  RES=$(/usr/bin/curl http://127.0.0.1/api/homematic.cgi \
+                      -H 'Content-Type: application/json' \
+                      -d "{\"method\":\"CCU.getVersion\",\"params\":{\"_session_id_\": \"${sid//@}\"}}")
+
+  # check the curl result contains the current
+  # version number or not
+  if ! echo "${RES}" | grep -q "${VERSION}"; then
+    echo "ERROR: no valid admin session id"
+    exit 1
+  fi
+else
+  echo "ERROR: invalid session id"
+  exit 1
+fi
+
+  echo "ERROR: head failure"
+  exit 1
# shellcheck shell=dash disable=SC3036,SC3010,SC2034,SC3060,SC2116,SC3045 source=/dev/null
# allow only POST requests
if [[ "${REQUEST_METHOD}" != "POST" ]]; then
  echo "ERROR: no POST request"
  exit 1
fi

if [[ -z "${HTTP_CONTENT_LENGTH}" ]]; then
# continue only if SIZE > 0
if [[ "${SIZE}" -le 0 ]]; then
  echo "ERROR: POST size <= 0"
  exit 1
fi

# extract known params from QUERY_STRING only
while IFS= read -r -d '&' KEYVAL && [[ -n "$KEYVAL" ]]; do
  case ${KEYVAL%%=*} in
    url) url=${KEYVAL#*=} ;;
    sid) sid=${KEYVAL#*=} ;;
    action) action=${KEYVAL#*=} ;;
    downloadOnly) downloadOnly=${KEYVAL#*=} ;;
  esac
done <<END
$(echo "${QUERY_STRING}&")
END

# check for url and action parameter
if [[ -z "${url}" ]] || [[ -z "${action}" ]]; then
  echo "ERROR: missing required URL parameters"
  exit 1
fi

# check for a valid ADMIN session id
if [[ "${#sid}" -eq 12 ]]; then
  # parse the current version
  [[ -r /VERSION ]] && . /VERSION

  # use CCU.getVersion which is allowed only for Admins
  RES=$(/usr/bin/curl http://127.0.0.1/api/homematic.cgi \
                      -H 'Content-Type: application/json' \
                      -d "{\"method\":\"CCU.getVersion\",\"params\":{\"_session_id_\": \"${sid//@}\"}}")

  # check the curl result contains the current
  # version number or not
  if ! echo "${RES}" | grep -q "${VERSION}"; then
    echo "ERROR: no valid admin session id"
    exit 1
  fi
else
  echo "ERROR: invalid session id"
  exit 1
fi

  echo "ERROR: head failure"
  exit 1