CVE-2020-23352 (published 2021-01-27T16:15Z)

Z-BlogPHP 1.6.0 Valyria is affected by incorrect access control. PHP loose comparison and a magic hash can be used to bypass authentication. zb_user/plugin/passwordvisit/include.php:passwordvisit_input_password() uses loose comparison to authenticate, which can be bypassed via magic hash values.

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

Weakness information:
cwe_id: NVD-CWE-Other
cwe_name: Other
description: NVD is only using a subset of CWE for mapping instead of the entire CWE, and the weakness type is not covered by that subset.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/zblogcn/zblogphp
commit_details: repo_url: https://github.com/zblogcn/zblogphp
author: zx.asd
author_date: 2020-05-21 17:54:45+08:00
msg: hash_equals函数添加及其应用;CloseTags函数进行了修正;
repository: repo_url: https://github.com/zblogcn/zblogphp
repo_name: zblogcn/zblogphp
description: Z-BlogPHP博客程序
repo_language: PHP
owner: zblogcn
file_changes: filename: phpcs.xml
change_type: ModificationType.MODIFY
diff: @@ -42,6 +42,7 @@
 		<exclude name="PHPCompatibility.FunctionUse.NewFunctions.rrmdir.Found" />
 		<exclude name="PHPCompatibility.FunctionUse.NewFunctions.gzdecode.Found" />
 		<exclude name="PHPCompatibility.FunctionUse.NewFunctions.array_replace_recursive.Found" />
+		<exclude name="PHPCompatibility.FunctionUse.NewFunctions.hash_equals.Found" />
 		<exclude name="PHPCompatibility.Constants.NewConstants.PHP_VERSION_ID.Found" />
 		<exclude name="PHPCompatibility.Constants.NewConstants.FNM_PATHNAME.Found" />
 		<exclude name="PHPCompatibility.Constants.NewConstants.FNM_NOESCAPE.Found" />

programming_language: CoffeeScript
filename: c_system_common.php
change_type: ModificationType.MODIFY
diff: @@ -1218,42 +1218,22 @@ function TransferHTML($source, $param)
  */
 function CloseTags($html)
 {
-    // strip fraction of open or close tag from end (e.g. if we take first x characters, we might cut off a tag at the end!)
-    $html = preg_replace('/<[^>]*$/', '', $html); // ending with fraction of open tag
-
-    // put open tags into an array
-    preg_match_all('#<([a-z]+)(?: .*)?(?<![/|/ ])>#iU', $html, $result);
-    $opentags = $result[1];
-
-    // put all closed tags into an array
+    preg_match_all('#<(?!meta|img|br|hr|input\b)\b([a-z]+)(?: .*)?(?<![/|/ ])>#iU', $html, $result);
+    $openedtags = $result[1];
     preg_match_all('#</([a-z]+)>#iU', $html, $result);
-    $closetags = $result[1];
-
-    $len_opened = count($opentags);
-
-    // if all tags are closed, we can return
-    if (count($closetags) == $len_opened) {
+    $closedtags = $result[1];
+    $len_opened = count($openedtags);
+    if (count($closedtags) == $len_opened) {
         return $html;
     }
-
-    // close tags in reverse order that they were opened
-    $opentags = array_reverse($opentags);
-
-    // self closing tags
-    $sc = array('br', 'input', 'img', 'hr', 'meta', 'link');
-    // ,'frame','iframe','param','area','base','basefont','col'
-    // should not skip tags that can have content inside!
-
-    for ($i = 0; $i < $len_opened; $i++) {
-        $ot = strtolower($opentags[$i]);
-
-        if (!in_array($opentags[$i], $closetags) && !in_array($ot, $sc)) {
-            $html .= '</' . $opentags[$i] . '>';
+    $openedtags = array_reverse($openedtags);
+    for ($i=0; $i < $len_opened; $i++) {
+        if (!in_array($openedtags[$i], $closedtags)) {
+            $html .= '</'.$openedtags[$i].'>';
         } else {
-            unset($closetags[array_search($opentags[$i], $closetags)]);
+            unset($closedtags[array_search($openedtags[$i], $closedtags)]);
         }
     }
-
     return $html;
 }
 

programming_language: PHP
filename: c_system_compat.php
change_type: ModificationType.MODIFY
diff: @@ -446,8 +446,57 @@ if (!function_exists('array_replace_recursive')) {
                 $array = recurse($array, $args[$i]);
             }
         }
-
         return $array;
     }
 
 }
+
+
+if (!function_exists('hash_equals')) {
+
+    /**
+     * Timing attack safe string comparison
+     *
+     * Compares two strings using the same time whether they're equal or not.
+     * This function should be used to mitigate timing attacks; for instance, when testing crypt() password hashes.
+     *
+     * @param string $known_string The string of known length to compare against
+     * @param string $user_string The user-supplied string
+     * @return boolean Returns TRUE when the two strings are equal, FALSE otherwise.
+     */
+    function hash_equals($known_string, $user_string)
+    {
+        if (func_num_args() !== 2) {
+            // handle wrong parameter count as the native implentation
+            trigger_error('hash_equals() expects exactly 2 parameters, ' . func_num_args() . ' given', E_USER_WARNING);
+            return null;
+        }
+        if (is_string($known_string) !== true) {
+            trigger_error('hash_equals(): Expected known_string to be a string, ' . gettype($known_string) . ' given', E_USER_WARNING);
+            return false;
+        }
+        $known_string_len = strlen($known_string);
+        $user_string_type_error = 'hash_equals(): Expected user_string to be a string, ' . gettype($user_string) . ' given'; // prepare wrong type error message now to reduce the impact of string concatenation and the gettype call
+        if (is_string($user_string) !== true) {
+            trigger_error($user_string_type_error, E_USER_WARNING);
+            // prevention of timing attacks might be still possible if we handle $user_string as a string of diffent length (the trigger_error() call increases the execution time a bit)
+            $user_string_len = strlen($user_string);
+            $user_string_len = $known_string_len + 1;
+        } else {
+            $user_string_len = $known_string_len + 1;
+            $user_string_len = strlen($user_string);
+        }
+        if ($known_string_len !== $user_string_len) {
+            $res = $known_string ^ $known_string; // use $known_string instead of $user_string to handle strings of diffrent length.
+            $ret = 1; // set $ret to 1 to make sure false is returned
+        } else {
+            $res = $known_string ^ $user_string;
+            $ret = 0;
+        }
+        for ($i = strlen($res) - 1; $i >= 0; $i--) {
+            $ret |= ord($res[$i]);
+        }
+        return $ret === 0;
+    }
+
+}

programming_language: PHP
filename: zblogphp.php
change_type: ModificationType.MODIFY
diff: @@ -1316,7 +1316,7 @@ class ZBlogPHP
         }
         $m = $this->GetMemberByName($name);
         if ($m->ID != null) {
-            if (strcasecmp($m->Password, $password) == 0) {
+            if (hash_equals($m->Password, $password) === true) {
                 $member = $m;
 
                 return true;

programming_language: PHP
filename: include.php
change_type: ModificationType.MODIFY
diff: @@ -31,7 +31,7 @@ function passwordvisit_input_password(&$template)
     if (isset($_POST['password']) && $_POST['password'] != '') {
         $article = $template->GetTags('article');
         if ($article->Metas->passwordvisit_password != '') {
-            if (GetVars('password', 'POST') == $article->Metas->passwordvisit_password) {
+            if (GetVars('password', 'POST') === $article->Metas->passwordvisit_password) {
                 return;
             } else {
                 echo '<script type="text/javascript">alert("密码错误，请重新输入！");window.location="' . $article->Url . '";</script>';

programming_language: PHP