译文转载声明
本文是翻译文章,原作者 Soroush Dalili
原文地址:
https://www.thezdi.com/blog/2020/9/9/performing-sql-backflips-to-achieve-code-execution-on-schneider-electrics-ecostruxure-operator-terminal-expert-at-pwn2own-miami-2020 译文仅作参考,具体内容表达请见原文
简介
在本文中,我们将为读者详细介绍Claroty Research团队的Amir Preminger和Sharon Brizinov是如何组合利用两个漏洞,来触发施耐德工控软件EcoStruxure Operator Terminal Expert的代码执行漏洞,从而在首届举办的Pwn2Own迈阿密大赛上赢得了2.5万美元奖金。这里所述的漏洞存在于施耐德电气公司的EcoStruxure Operator Terminal Expert软件的V3.1.100.267(SP 1)和之前版本(以前称为Vijeo XD)中。
实际上,攻击者只要引诱受害者打开(双击)EcoStruxure Operator Terminal Expert软件的项目文件,就能够利用默认配置发动相应的漏洞。这时,将触发应用程序上下文中的代码执行漏洞。总的来说,要想成功利用这个代码执行漏洞,需要组合利用下文描述的两个已知的漏洞。在本文中,我们先按照漏洞被发现的顺序来描述其详细信息,然后,给出组合运用这些漏洞并实现命令执行攻击所需的具体步骤。下面,我们先给出相应的演示视频的地址:https://youtu.be/SAmhljE9ZlE。
漏洞详情
EcoStruxure Control Terminal Expert是一个软件环境,用于设计人机界面(HMI)设备的图形用户界面。这些用户界面用于控制工业部署中可编程逻辑控制器 (PLC) 的操作。
图1 使用EcoStruxure Control Terminal Expert设计水流控制
在这里,所有的项目信息,包括各种设置和图形组件的信息,都会被保存到一个后缀为.VXDZ的EcoStruxure Control Terminal Expert项目文件中。实际上,.VXDZ项目文件就是一个存放各种文件的压缩目录,这些文件包含了程序还原项目所需的全部信息,以便工程师在以后可以继续工作。项目文件主要包括以下几种文件类型:
- .db:SQLite3数据库文件,包括各种项目配置和设置。
- .inf/.dat:JSON文件,用于存储数据和设置。例如,每个屏幕及其图形组件都是用JSON表示的。
图2 项目目录
当工程师打开项目文件时,压缩后的目录会被解压到一个临时目录下,路径如下所示:
代码语言:javascript复制C:usersUSERAppDataLocalEcoStruxureTempSchneiderCURRENT_VERSION_FULLGUIDProjectFiles
为了便于后面进行参考,我们将路径中与环境有关的组件用红色进行了相应的标记。另外,橙色显示的是GUID,它是在每次打开一个项目时随机生成的,即使这个项目之前已经打开过,亦是如此。这意味着这个路径无法提前预测,因为它取决于当前登录的用户、当前具体的版本名以及一次性随机生成的GUID。例如,下面就是我们打开一个项目文件时生成的一个有效的路径:
代码语言:javascript复制C:UsersAdministratorAppDataLocalEcoStruxureTemp SchneiderImagine3.1ServicePackA1A98F0B-9487-41B3-84A2-2195ECAA11F5ProjectFiles
此外,由于所使用的.NET zip库能够防止路径遍历企图,因此,只能提取随机生成的目录。
高级功能
与任何安全研究一样,我们需要尽量熟悉目标产品,并寻找那些可能没有被厂商深入检查过的复杂/先进功能。在把玩一阵EcoStruxure Control Terminal Expert后,我们发现了一个名为“Drivers”的功能。由于HMI是智能屏幕,呈现的数据是从工厂内的现场控制器收集的,所以必须具备查询功能,才能从PLC中获取数据。为了达到这个目的,施耐德提供了这样一种机制,即在项目中添加一个特定厂商的驱动程序,该驱动程序能够查询PLC以获取所需数据。我们知道,PLC有许多不同的型号,并且每个PLC都是通过自己的协议进行通信的。正因为如此,施耐德提供了许多的驱动程序,工程师可以根据他们需要集成的PLC自行选用。
图3 驱动程序是帮助HMI与所需控制设备(PLC)进行通信的组件。每个供应商及其特定设备(生态系统、协议栈等)都会提供许多不同的驱动程序。
有关特定项目文件使用的驱动程序的所有信息都位于一个名为DriverConfig.db的SQLite3数据库文件中,我们可以在项目目录中找到这个文件。我们在项目中添加了一个新的驱动程序,并检查了DriverConfig.db文件,发现其中有三个数据表:
- Driver_X:空表。
- Driver_X_Configuration_X:关于驱动程序的详细信息,如设置和元数据。这其中包括将要加载的驱动程序/模块名称。
图4 DriverConfig.db的内容
- Driver_X_Equipment_X:关于HMI将与之通信的PLC的详细信息。其中,会包括与PLC相关的信息,如IP地址、型号、协议等。
其中,X代表驱动索引,由于我们只添加了一个驱动,所以在我们的例子中X为0。
通过.NET反射器,我们研究了相关的中间语言(IL)代码。我们发现,ModuleName字段实际上就是驱动程序DLL,它将从预定义的目录中进行加载,并处理HMI和PLC之间的通信。例如,如果我们有一个Rockwell Automation公司的PLC,我们就需要加载Rockwell公司相应的驱动程序——它通过EtherNet/IP CIP协议与PLC进行通信。具体这里来说,需要加载驱动程序RockwellEIP.dll。为此,我们可以在该项目中的SQLite3数据库文件DriverConfig.db中的Driver_0_Configuation_0表的ModuleName列(字段)中加以指定。
图5 打开DriverConfig.db数据库的SQLite3查看器。ModuleName字段是驱动DLL的名称,它将被加载并处理HMI和PLC之间的通信。
Bug No. 1:通过路径遍历以获取DLL加载原语
为了更好地理解如何从DriverConfig.db数据库中提取信息,我们钻进了一个“兔子洞”:DriverConfig.db的连接。我们可以看到,这里的代码会查询并提取Driver_x_configuration_0表中的所有属性。然后,它将一个新的Driver对象实例化,并根据表中找到的相应值设置ModuleName字段。最后,它使用 ModuleName字段指定的路径加载相应的驱动程序DLL文件。
由于数据库(包括ModuleName字段)在我们的掌控之下,我们可以提供一个带有一些 ../../../字符的自定义ModuleName,以便从包含合法驱动程序的应用程序定义目录中导航出来。换句话说,我们能够从系统中加载任意DLL。
图6 我们将ModuleName字段改为../../../../claroty.dll,并使用procmon来监控系统。
然而,我们的攻击要想成功,必须满足下面两个条件:
- 如果一个名为driver.xml的文件没有出现在将要加载的DLL旁边,那么该DLL将不会被加载。
- 加载的DLL必须位于同名的目录中。
例如,如果我们将ModuleName改为Claroty,软件将进入预定义的驱动程序目录C:Program FilesSchneider ElectricEcoStruxure Operator Terminal Expert 3.1 Service PackDriversDrivers,并寻找名为Claroty的目录,然后在该目录中搜索Claroty.dll和Driver.xml。如果这两个文件都找到了,就会加载里面的DLL,在本例中就是C:Program FilesSchneider ElectricEcoStruxure Operator Terminal Expert 3.1 Service PackDriversDriversClarotyClaroty.dll。
我们通过目录遍历实现了加载任意DLL的原型,这真是太棒了。但是,现在面临的问题是,我们如何才能提供自己的DLL,并使其运行呢?
好吧,在一定程度上说,我们还需要一个具有“任意文件写入”功能的原语。回想一下,我们的项目文件实际上就是一个包含文件和目录的压缩容器。也就是说,我们可以添加我们的文件和目录,然后再重新打包项目文件。当软件打开项目文件并提取所有文件时,我们添加的文件也会和其他文件一起被提取出来(并保存到临时目录中)。现在唯一的问题是:我们如何才能提前知道我们的文件会被解压到哪里,这样我们就可以在DriverConfig.db数据库下的ModuleName属性中设置相应的路径了。
下面,我们来总结一下:我们可以利用目录遍历漏洞来跳出正常的驱动程序的目录,同时,我们也可以在我们的项目文件被提取的时候,把一些文件和目录保存到硬盘上。但是,这些文件会被提取到一个随机的临时目录,我们无法提前预知,因为GUID每次都是随机生成的。
Bug No. 2:未进行严格安全过滤导致敏感数据信息泄露
我们对这些问题思考了很久,后来终于想到了一个解决方案。这个解决方案来自于一个意想不到的领域:SQLite的魔术!我们使用SQL pragma和SQL views数据库功能实时生成提取目录的完整路径。因此,我们可以让Terminal Expert软件直接找到我们的恶意DLL。我们之所以能够做到这一点,是因为Terminal Expert软件加载了我们所控制的项目文件中提供的数据库,并在没有对数据进行适当安全过滤的情况下查询表格。
什么是PRAGMA?
PRAGMA语句是一个依赖于具体实现的SQL扩展。它可以用来修改SQLite库的操作,或者查询SQLite库的内部(非表)数据。例如,pragma database_list命令将返回当前连接数据库的列表。
而SELECT file FROM pragma_database_list命令则会产生当前加载数据库的完整路径。
图7 显示当前加载的数据库的完整路径
这意味着我们可以在实时加载数据库之后生成数据库的完整路径。同样,这也是在将数据库保存到新建的、具有随机路径的临时目录之后完成的。现在,我们只需要一种方法来获取该查询的结果,并将其插入到软件即将查询的ModuleName属性中即可。
什么是视图?
为了达到上述目的,我们使用了数据库的一个不太常用的功能:视图。在数据库中,视图是一个存储查询的结果集。换句话说,视图就像一个动态创建的表,它是在客户端查询时实时生成的。当客户端查询视图时,数据库会查询为视图定义的实际表,并根据视图的设置对生成的数据进行重组,最后将完整的结果反馈给客户端——整个过程对客户端而言是透明的。从客户端的角度来看,似乎正在查询数据库中找到的常规表。
图8 数据库视图和我们实时影响查询的抽象方案
在我们的案例中,客户端是EcoStruxure Operator Terminal Expert软件,它查询驱动程序数据库以获取ModuleName属性,从而可以加载驱动程序DLL。我们的计划是在数据库被提取到临时位置后,实时修改ModuleName属性,最终让ModuleName保存我们数据库的实际路径。
1 2=RCE:组合两个漏洞,实现代码执行攻击
在项目文件中,我们需要准备一个名为ClarotyModule的目录,其中含有如下所示的两个文件:
- Driver.xml
- ClarotyModule.dll
我们将按照以下步骤准备DriverConfig.db:
- 我们将原来的Driver_0_Configuration_0表重命名为Driver_0_Configuration_0_ORIG。
- 我们将创建一个名为Driver_0_Configuration_0的VIEW表。
当客户端查询“原来的”表Driver_0_Configuration_0时,实际上会查询我们新建的VIEW表。在查询到ModuleName字段后,我们将VIEW表的内部处理设置为返回SELECT file FROM pragma_database_list的结果,并对其进行必要的修改,以构成正确的目录遍历语法。通过这种方式,我们可以在文件夹结构中向上、向下导航,直到抵达当前的临时目录中,也就是我们的payload DLL所在的位置。
图9 精心构造一个驱动数据库,使其实时包含我们DLL的路径。
最后,我们把所有的部分重新打包成一个VXDZ项目文件。当受害者双击该文件时,我们的DLL将被加载,之后,我们的代码也将被执行。
图10 POC运行时,会打开项目文件,并执行相应的代码
小结
在本文中,我们为读者详细介绍了如何利用EcoStruxure Operator Terminal Expert读取给定项目文件的方式,通过执行一些SQL backflips操作,诱导软件加载我们提供的DLL,从而在打开项目文件时发动任意代码执行攻击。施耐德电气公司已经修复了这些漏洞,并将其分配了相应的编号:CVE-2020-7494与CVE-2020-7496。