CVE-2020-26124 (published 2020-10-02T09:15Z)

openmediavault before 4.1.36 and 5.x before 5.5.12 allows authenticated PHP code injection attacks, via the sortfield POST parameter of rpc.php, because json_encode_safe is not used in config/databasebackend.inc. Successful exploitation allows arbitrary command execution on the underlying operating system as root.

Severity: HIGH
CVSS v2 base score: 9.0 (AV:N/AC:L/Au:S/C:C/I:C/A:C)
CVSS v3 base score: 8.8, HIGH (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-94
cwe_name: Improper Control of Generation of Code ('Code Injection')
description: The product constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment.
extended_description: 
            When a product allows a user's input to contain code syntax, it might be possible for an attacker to craft the code in such a way that it will alter the intended control flow of the product. Such an alteration could lead to arbitrary code execution.
            Injection problems encompass a wide variety of issues -- all mitigated in very different ways. For this reason, the most effective way to discuss these weaknesses is to note the distinct features which classify them as injection weaknesses. The most important issue to note is that all injection problems share one thing in common -- i.e., they allow for the injection of control plane data into the user-controlled data plane. This means that the execution of the process may be altered by sending code in through legitimate data channels, using no other mechanism. While buffer overflows, and many other flaws, involve the use of some further issue to gain execution, injection problems need only for the data to be parsed. The most classic instantiations of this category of weakness are SQL injection and format string vulnerabilities.
         
         

Fix information:
repo_url: https://github.com/openmediavault/openmediavault
commit_details: repo_url: https://github.com/openmediavault/openmediavault
author: Volker Theile
author_date: 2020-09-26 22:36:04+02:00
msg: Cleanup code, add unit tests.

Signed-off-by: Volker Theile <votdev@gmx.de>
repository: repo_url: https://github.com/openmediavault/openmediavault
repo_name: openmediavault/openmediavault
description: openmediavault is the next generation network attached storage (NAS) solution based on Debian Linux. Thanks to the modular design of the framework it can be enhanced via plugins. openmediavault is primarily designed to be used in home environments or small home offices.
repo_language: PHP
owner: openmediavault
file_changes: filename: test_openmediavault_config_databasebackend.php
change_type: ModificationType.ADD
diff: @@ -0,0 +1,58 @@
+#!/usr/bin/phpunit -c/etc/openmediavault
+<?php
+/**
+ * This file is part of OpenMediaVault.
+ *
+ * @license   http://www.gnu.org/licenses/gpl.html GPL Version 3
+ * @author    Volker Theile <volker.theile@openmediavault.org>
+ * @copyright Copyright (c) 2009-2020 Volker Theile
+ *
+ * OpenMediaVault is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * any later version.
+ *
+ * OpenMediaVault is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with OpenMediaVault. If not, see <http://www.gnu.org/licenses/>.
+ */
+require_once("openmediavault/autoloader.inc");
+
+class test_openmediavault_config_databasebackend extends \PHPUnit\Framework\TestCase {
+	private $databaseBackend;
+
+	public function setUp() {
+		$this->databaseBackend = new \OMV\Config\DatabaseBackend(
+			sprintf("%s/../data/config.xml", getcwd()),
+			FALSE);
+		$this->databaseBackend->load();
+	}
+
+	public function test_compare_1() {
+		$this->assertEquals($this->databaseBackend->compare(
+			"//system/time",
+			[
+				"timezone" => "Europe/Berlin",
+				"ntp" => [
+					"enable" => 0,
+					"timeservers" => "pool.ntp.org",
+					"clients" => ""
+				]
+			]
+		), 0);
+	}
+
+	public function test_compare_2() {
+		$this->assertEquals($this->databaseBackend->compare(
+			"//system/powermanagement",
+			[
+				"cpufreq" => 0,
+				"powerbtn" => "nothing"
+			]
+		), -1);
+	}
+}

programming_language: PHP
filename: test_openmediavault_functions.php
change_type: ModificationType.MODIFY
diff: @@ -335,4 +335,32 @@ class test_openmediavault_functions extends \PHPUnit\Framework\TestCase {
 	public function test_array_boolval_13() {
 		$this->assertTrue(array_boolval("a", "c", TRUE));
 	}
+
+	public function test_array_sort_key_1() {
+		$d = [
+			['id' => 3, 'text' => 'b'],
+			['id' => 1, 'text' => 'a'],
+			['id' => 2, 'text' => 'c']
+		];
+		$this->assertTrue(array_sort_key($d, "id"));
+		$this->assertEquals($d, [
+			['id' => 1, 'text' => 'a'],
+			['id' => 2, 'text' => 'c'],
+			['id' => 3, 'text' => 'b']
+		]);
+	}
+
+	public function test_array_sort_key_2() {
+		$d = [
+			['id' => 3, 'text' => 'b'],
+			['id' => 1, 'text' => 'a'],
+			['id' => 2, 'text' => 'c']
+		];
+		$this->assertTrue(array_sort_key($d, "text"));
+		$this->assertEquals($d, [
+			['id' => 1, 'text' => 'a'],
+			['id' => 3, 'text' => 'b'],
+			['id' => 2, 'text' => 'c']
+		]);
+	}
 }

programming_language: PHP
filename: databasebackend.inc
change_type: ModificationType.MODIFY
diff: @@ -331,6 +331,17 @@ class DatabaseBackend {
 		// Convert all values to strings before comparison. This is necessary
 		// because the values returned from the configuration database are
 		// strings.
+		if (FALSE === array_walk_recursive($data, function(&$item, $key) {
+			if (is_string($item)) {
+				if (!mb_check_encoding($item, "UTF-8")) {
+					$item = utf8_encode($item);
+				}
+			} else {
+				$item = utf8_encode(strval($item));
+			}
+		})) {
+			return FALSE;
+		}
 		if (FALSE === array_walk_recursive($data,
 		  create_function('&$item, $key', 'if (is_string($item)) { '.
 		  'if (!mb_check_encoding($item, "UTF-8")) { '.
@@ -338,8 +349,12 @@ class DatabaseBackend {
 		  '$item = utf8_encode(strval($item)); }')))
 			return FALSE;
 		// Compare the arrays.
-		if (0 == count(array_diff($result, $data)))
+		// Note, we can not use `array_diff` here because this function
+		// does not support multidimensional arrays.
+		if (0 === strcmp(json_encode_safe($result),
+				json_encode_safe($data))) {
 			return 0;
+		}
 		return -1;
 	}
 

programming_language: PHP
filename: functions.inc
change_type: ModificationType.MODIFY
diff: @@ -84,9 +84,11 @@ function array_sort_key(array &$array, $key) {
 	if (!is_multi_array($array))
 		return FALSE;
 	// Sort the array.
-	if (FALSE === uasort($array, create_function('$a, $b',
-		"return strnatcmp(strval(\$a['$key']), strval(\$b['$key']));")))
+	if (FALSE === uasort($array, function($a, $b) use($key) {
+		return strnatcmp(strval($a[$key]), strval($b[$key]));
+	})) {
 		return FALSE;
+	}
 	// Re-index the array.
 	$array = array_values($array);
 	return TRUE;

programming_language: PHP