开始之前 这是大约一个月前joomla的漏洞,现在写有点晚了,不得不说假期确实是消磨人性呀。由于对joomla的架构不熟悉,审计过程中走了不少弯路,文章可能写的比较啰嗦,利用方式直接掺在分析过程中一起写了。求大牛轻喷吧,233333。
漏洞点 payload出库 这次出现漏洞的地方代码比较简单,在administrator\templates\hathor\postinstall\hathormessage.php中,这是joomla自带的一套模板。
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 function  hathormessage_postinstall_condition (	$db              = JFactory ::getDbo (); 	$user            = JFactory ::getUser (); 	$globalTemplate  = 'n/a' ; 	$template        = 'n/a' ; 	 	 	 	$adminstyle  = $user ->getParam ('admin_style' , '' ); 	if  ($adminstyle  != '' ) 	{ 		$query  = $db ->getQuery (true ) 			->select ('template' ) 			->from ($db ->quoteName ('#__template_styles' )) 			->where ($db ->quoteName ('id' ) . ' = '  . $adminstyle [0 ]) 			->where ($db ->quoteName ('client_id' ) . ' = 1' ); 		 		$template  = $db ->setquery ($query )->loadResult (); 	} 	 } 
函数的功能注释已经写的很详细了,检查当前后台的模板是否为hathor。那么我们就可以大胆猜测,每次进入后台的时候这个函数都会被调用。事实证明确实是这样,具体的代码暂时不说。
漏洞出现的原因就是在获取到$admin_style之后,没有对其进行类型强转或者过滤等操作,而是用了$admin_style[0]这种奇怪的操作,导致了漏洞的出现,这是这个二次注入的第二步 。
我们先看一下$admin_style是怎么来的。
一路跟踪getParam()来到了libraries/vendor/joomla/registry/src/Registry.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public  function  get ($path , $default  = null 		 		if  (empty ($path )) 		{ 			return  $default ; 		} 		if  (!strpos ($path , $this ->separator)) 		{ 			return  (isset ($this ->data->$path ) && $this ->data->$path  !== null  && $this ->data->$path  !== '' ) ? $this ->data->$path  : $default ; 		}   		 } 
查看参数是否存在分隔符(默认为.),不存在就返回data对象的属性或者默认值(空)。再来看一下data对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public  function  __construct ($data  = null 	 {		 		$this ->data = new  \stdClass ; 		 		if  (is_array ($data ) || is_object ($data )) 		{ 			$this ->bindData ($this ->data, $data ); 		} 		elseif  (!empty ($data ) && is_string ($data )) 		{ 			$this ->loadString ($data ); 		} 	} 
这里把data看做是一个存储数据的全局变量即可,再粗暴一点理解成一个全局数组也行,用法是一样的。
payload入库 既然是$admin_style出了问题,那我们就全局搜一下admin_style
1 2 3 4 5 6 7 F:\PHP\Joomla_3.8.3-Stable-Full_Package\administrator\components\com_admin\models\forms\profile.xml:   102     103  			<field    104: 				name="admin_style"    105  				type="templatestyle"   106  				label="COM_ADMIN_USER_FIELD_BACKEND_TEMPLATE_LABEL"   ...... 
对joomla的架构不熟,这个地方花了不少时间。这个xml的作用就是post请求时要传递的参数以及他们的默认值,上面说到的获取默认值其实就是获取这里的值。根据xml所在位置可以判定关键点就在后台管理员修改个人信息 的地方(其实还有一处,即超级管理员修改用户信息的地方,但是所需权限太高,前者只需要进入后台的权限即可)。
访问/administrator/index.php?option=com_admin&view=profile&layout=edit&id=925修改个人信息后抓包,我们看到了admin_style参数。
但是如果直接这样构造payload是不管用的,还记得前面的神操作$admin_style[0]吗,他会将payload作为字符串来处理,只取第一个字符,这么看来似乎还有点防御能力?如何让$admin_style[0]取到全部的payload,php里简单的就像喝水一样。
此时payload已经存入数据库了
以上便是这个二次注入的第一步 。
关于joomla对于sql语句的处理,涉及到框架低层的东西,也不是这次的重点,所以没有深入。
漏洞触发点 一通操作之后回到主页,发现payload已经生效了:
到这里差不多就该结束了,但是我感觉这属于比较特殊的情况,正好在主页就有触发点,一般的二次注入都比较隐蔽,所以我决定再深入一下下,当然也不出意外的踩了许多坑。。。
访问administrator/index.php默认路由到com_cpanel这个包
administrator/includes/helper.php
1 2 3 4 5 6 7 8 9 10 11 public  static  function  findOption (	 {		 		if  (empty ($option )) 		{ 			$option  = 'com_cpanel' ; 		} 		$app ->input->set ('option' , $option ); 		return  $option ; 	} 
再到administrator/components/com_cpanel/views/cpanel/view.html.php
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 public  function  display ($tpl  = null      JToolbarHelper ::title (JText ::_ ('COM_CPANEL' ), 'home-2 cpanel' );   JToolbarHelper ::help ('screen.cpanel' );   $input  = JFactory ::getApplication ()->input;      $input ->set ('tmpl' , 'cpanel' );      $this ->modules = JModuleHelper ::getModules ('cpanel' );   try    {     $messages_model  = FOFModel ::getTmpInstance ('Messages' , 'PostinstallModel' )->eid (700 );     $messages        = $messages_model ->getItemList ();   }   catch  (RuntimeException  $e )   {     $messages  = array ();          JFactory ::getApplication ()->enqueueMessage ($e ->getMessage (), 'error' );    }   $this ->postinstall_message_count = count ($messages );   parent ::display ($tpl ); } 
到这里目标就很明显了,继续跟getItemList()
libraries/fof/model/model.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public  function  &getItemList ($overrideLimits  = false , $group  = '' 		if  (empty ($this ->list )) 		{ 			$query  = $this ->buildQuery ($overrideLimits ); 			if  (!$overrideLimits ) 			{ 				$limitstart  = $this ->getState ('limitstart' ); 				$limit  = $this ->getState ('limit' ); 				$this ->list  = $this ->_getList ((string ) $query , $limitstart , $limit , $group ); 			} 			else  			{ 				$this ->list  = $this ->_getList ((string ) $query , 0 , 0 , $group ); 			} 		} 		return  $this ->list ; } 
这里构造的sql语句为
1 SELECT  `#__postinstall_messages`.*  FROM  `#__postinstall_messages` WHERE  `extension_id` =  '700'  AND  `enabled` =  '1'  ORDER  BY  `postinstall_message_id` ASC 
执行结果
到这里整个流程就比较清晰了,后面就比较简单了。
1 2 3 4 5 6 7 8 9 protected  function  &_getList ($query , $limitstart  = 0 , $limit  = 0 , $group  = '' 		$this ->_db->setQuery ($query , $limitstart , $limit ); 		$result  = $this ->_db->loadObjectList ($group ); 		$this ->onProcessList ($result ); 		return  $result ; } 
获取到postinstall_messsage,下面是$result的部分输出结果
继续
administrator/components/com_postinstall/models/messages.php
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 protected  function  onProcessList (&$resultArray  )		$unset_keys           = array (); 		$language_extensions  = array (); 		 		$resultArray  = array_reverse ($resultArray ); 		foreach  ($resultArray  as  $key  => $item )  		{ 			 			if  (!empty ($item ->condition_file) && !empty ($item ->condition_method)) 			{ 				jimport ('joomla.filesystem.file' ); 				$file  = FOFTemplateUtils ::parsePath ($item ->condition_file, true ); 				if  (JFile ::exists ($file )) 				{ 					require_once  $file ;  					 					$result  = call_user_func ($item ->condition_method);  					if  ($result  === false ) 					{ 						$unset_keys [] = $key ; 					} 				} 			} 			 			 		} 		 } 
遍历结果集,包含指定文件,执行函数,打完收工~
防御 当然是升级啦。
joomla 3.8.4 做了如下处理
1 2 3 4 5 6 7 8 9 10 11 $adminstyle  = $user ->getParam ('admin_style' ); if  ($adminstyle ){   $query  = $db ->getQuery (true )     ->select ('template' )     ->from ($db ->quoteName ('#__template_styles' ))     ->where ($db ->quoteName ('id' ) . ' = '  . (int ) $adminstyle )      ->where ($db ->quoteName ('client_id' ) . ' = 1' );      $template  = $db ->setquery ($query )->loadResult (); } 
参考文章 分析CVE-2018-6376 – Joomla!二阶SQL注入  文章还介绍了利用sqlmap进行自动化攻击
从补丁到漏洞分析 –记一次joomla漏洞应急