Pimcore 平台提供用于集中管理企业数据的软件。它在 56 个国家拥有超过 100,000 个客户,其中包括一些主要供应商,已成为全球企业值得信赖的选择。提供企业订阅版和开源社区版,开发人员和用户社区不断壮大。
我们通过频繁扫描开源项目并评估结果,不断努力增强清洁代码解决方案的技术支持。就 Pimcore 而言,我们的引擎报告了一个有趣的有限目录遍历漏洞。分析结果后,我们在同一端点中发现了另一个 SQL 注入漏洞。利用这两个漏洞,点击攻击者精心设计的链接的管理员将在服务器上执行任意代码。
10.5.19 之前的 Pimcore 版本容易受到跟踪为 CVE-2023-28438 的端点中的路径遍历和SQL 注入漏洞的影响。create-csv
这两个漏洞可以通过单个 GET 请求来利用。因此,攻击者可以创建恶意链接,该链接可能导致管理员访问时 执行任意代码。
在本节中,我们将讨论这些漏洞的技术细节,并解释攻击者如何将它们组合起来创建一键式攻击,从而在服务器上部署 Web shell。
使用 SonarCloud 扫描 Pimcore 发现了一个有趣的路径遍历问题,该问题是由于将用户控制的数据作为fopen
. 您可以直接在 SonarCloud 上检查结果:
在 SonarCloud 上亲自尝试一下!
下划线的功能位于 Pimcore 的管理面板中,可以显示网站各个方面的统计报告。管理员可以创建自定义报告,直接从面板查看它们,或下载 CSV 格式的数据:
通过进一步检查漏洞函数createCsvAction
,我们发现用户控制的数据是通过admin/reports/custom-report/create-csv
端点的exportFile
参数传递的。尽管此端点只能由管理员访问,但它是一个没有 CSRF 保护的 GET 请求端点,因此操纵管理员单击链接就足够了。
该exportFile
参数的值会附加到 Web 根路径,而无需事先进行清理,从而允许攻击者控制扩展程序并遍历回文件夹路径。
继续检查代码,我们可以看到用户控制的路径最终将以“追加”模式打开文件。getData
使用fputcsv
:Github 中的文件将函数的输出写入其中
public function createCsvAction(Request $request) { //... $filters = $request->get('filter') ? json_decode(urldecode($request->get('filter')), true) : null; $drillDownFilters = $request->get('drillDownFilters', null); //... $result = $adapter->getData($filters, $sort, $dir, $offset * $limit, $limit, $fields, $drillDownFilters); if (!($exportFile = $request->get('exportFile'))) { $exportFile = PIMCORE_SYSTEM_TEMP_DIRECTORY . '/report-export-' . uniqid() . '.csv'; @unlink($exportFile); } else { $exportFile = PIMCORE_SYSTEM_TEMP_DIRECTORY.'/'.$exportFile; } $fp = fopen($exportFile, 'a'); if ($includeHeaders) { fputcsv($fp, $fields, ';'); } foreach ($result['data'] as $row) { $row = Service::escapeCsvRecord($row); fputcsv($fp, array_values($row), ';'); } //... }
到目前为止,攻击者可以控制 CSV 输出文件的路径、名称和扩展名。尽管这允许在服务器上创建 PHP 文件,但攻击者还需要控制文件内容才能执行任意代码。这里进入了第二个漏洞,函数中的 SQL 注入getData
。
查看createCsvAction
之前的函数,攻击者可以控制的输入是$drillDownFilters
和$filters
,它们被传递到getBaseQuery
:
Github 中的文件
public function getData($filters, $sort, $dir, $offset, $limit, $fields = null, $drillDownFilters = null) { $db = Db::get(); $baseQuery = $this->getBaseQuery($filters, $fields, false, $drillDownFilters); //... if ($baseQuery) { $total = $db->fetchOne($baseQuery['count']); //... $sql = $baseQuery['data'] . $order; //... $data = $db->fetchAllAssociative($sql); //... }
使用该函数的结果发出两个 SQL 查询getBaseQuery
:
$baseQuery[‘count’]
:返回结果数的查询COUNT(*)
将用于$db->fetchOne.
$baseQuery[‘data’]
: 将最终进入$db->fetchAllAssociative
并获取结果。
getBaseQuery
准备这两个查询的函数如下所示:Github
中的文件
protected function getBaseQuery($filters, $fields, $ignoreSelectAndGroupBy = false, $drillDownFilters = null, $selectField = null) { //... $sql = $this->buildQueryString($this->config, $ignoreSelectAndGroupBy, $drillDownFilters, $selectField); //... foreach ($filters as $filter) { $operator = $filter['operator']; //.. switch ($operator) { //.. case '=': $fields[] = $filter['property']; $condition[] = $db->quoteIdentifier($filter['property']) . ' = ' . $db->quote($value); //... $total = 'SELECT COUNT(*) FROM (' . $sql . ') AS somerandxyz WHERE ' . $condition; if ($fields && !$extractAllFields) { $data = 'SELECT `' . implode('`,`', $fields) . '` FROM (' . $sql . ') AS somerandxyz WHERE ' . $condition; }//... return [ 'data' => $data, 'count' => $total, ]; }
乍一看,我们注意到$data
参数注入,SQL 查询的SELECT
字段没有被清理。可以implode('`,`', $fields)
简单地用反引号来转义。
为了控制$fields
参数,我们需要相应地设置$filters['operator']
属性(在代码片段中仅显示“=”,但还有其他选项),然后'property'
将属性附加到它。紧接着$condition
将创建一个字符串。因此,为了控制该字符串将出现的 $fields
值。$condition
然而,虽然看起来在 处有一个简单的 SQL 注入$data
,但该变量被连接到两个查询 (和)$condition
的末尾。由于引号转义(使用函数和完成),任何包含反引号字符 (`) 的字段都将加倍,从而使查询的语法无效。count
data
$db->quoteIdentifier
$db->quote
我们当然可以注释掉查询的其余部分(使用--
or ;
)以避免语法破坏$condition
。但是该$total
查询也有损坏的, 并稍后在使用 SQL 注入查询获取之前的$condition
行中使用,从而引发异常并且不执行 SQL 注入。$db->fetchOne($baseQuery['count'])
data
所以我们有一个 SQL 注入,但是利用它总是会导致语法错误。还有其他方法可以忽略该$condition
字符串吗?
你们中的一些人可能已经注意到,每个前面$condition
都有一个$sql
参数,该参数是从$this->getBaseQuery(...)
. 如果该函数中也存在 SQL 注入,我们可以在语法错误之前结束查询。
protected function buildQueryString($config, $ignoreSelectAndGroupBy = false, $drillDownFilters = null, $selectField = null) { //... if ($drillDownFilters) { $havingParts = []; $db = Db::get(); foreach ($drillDownFilters as $field => $value) { if ($value !== '' && $value !== null) { $havingParts[] = "$field = " . $db->quote($value); } } if ($havingParts) { $sql .= ' HAVING ' . implode(' AND ', $havingParts); } } return $sql; }
审核该buildQueryString
函数后,我们发现了另一个 SQL 注入接收器,但现在使用该$drillDownFilters
参数。尽管该值已被引用,但该字段并未被引用。攻击者可以使用此同步注释掉损坏的内容$condition
并执行任意 SQL 查询。
因此,攻击者可以控制输出文件并将 SQL 注入到获取结果的函数中,结果最终将出现在该文件中。使用以下命令可以直接将导出文件路径指向 Web 根目录中的 PHP 文件:
../../../../../../../../var/www/html/public/webshell.php
如果文件中随机存在 PHP 声明,PHP 文件也会执行,这意味着文件不必以 开头<?php
,因此我们不必担心这一点。
但是攻击者如何利用 SQL 注入来生成任意内容呢?
有多个查询,一个插入自定义数据,另一个获取数据是可能的,但会使漏洞利用变得更加复杂。回到我们的 SQL 查询,注入是在 SELECT 字段中,因此我们可以使用CASE 表达式。
最后,get请求需要两个参数:
headers=true
是将字段名称输出到CSV
name=Quality_Attributes
是演示应用程序中报告的默认名称(为了执行易受攻击的函数,该名称必须是有效的报告)
将来自 3 个接收器的这 2 个漏洞组合到 1 个 GET 请求中,攻击者可以创建一个恶意链接,该链接将在服务器上部署 Web shell。
这两个漏洞均已在 Pimcore 版本 10.5.19 中修复:
通过添加字段名称也修复了SQL 注入。db->quoteIdentifier(...)
$havingParts[] = ($db->quoteIdentifier($field) ." = " . $db->quote($value));
路径遍历通过以下方式修复:
验证扩展名是否为“.csv”
规范化路径以防止遍历
$exportFileName = basename($exportFileName);if(!str_ends_with($exportFileName, ".csv")) { throw new InvalidArgumentException($exportFileName . " is not a valid csv file.");}return PIMCORE_SYSTEM_TEMP_DIRECTORY . '/' . $exportFileName;
Copyright © 2022 All Rights Reserved. 地址:上海市浦东新区崮山路538号808 苏ICP123456 XML地图