YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
Tribuo β Remote Code Execution via Unfiltered Java Deserialization on Model Load (LibLinearClassificationModel.deserializeFromProto)
Target: the Tribuo model format (huntr "Tribuo β Oracle") β the .tribuo protobuf model loader.
Repository: github.com/oracle/tribuo (module tribuo-classification-liblinear).
Version / pin: confirmed on org.tribuo:tribuo-classification-liblinear:4.3.2 (latest release, resolved from Maven Central) + tribuo-core/tribuo-json 4.3.2, liblinear-2.44, protobuf-java-3.25.6, temurin JDK 17. The sink is present in every published Tribuo LibLinear release; the fix exists only on unreleased main.
Vulnerable file: Classification/LibLinear/src/main/java/org/tribuo/classification/liblinear/LibLinearClassificationModel.java (deserializeFromProto).
CWE: CWE-502 (Deserialization of Untrusted Data) β arbitrary code execution.
CVSS 3.1: AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H = 8.8 (High) β a victim who loads an attacker-supplied .tribuo model gets RCE; 9.8 for a service that auto-loads models from an untrusted source (UI:N).
Status: POC_EXECUTED β arbitrary code execution confirmed on the released loader (ran id).
1. Summary
A .tribuo model is a protobuf ModelProto. Tribuo migrated model serialization to protobuf specifically to replace its legacy Java-serialization path (see SECURITY.md, which deprecates the old java.io.Serializable model I/O). But the LibLinear model implementation is not protobuf-native: its inner de.bwaldvogel.liblinear.Model is embedded as raw Java-serialized bytes inside the LibLinearProto.models(0) field. When loading, LibLinearClassificationModel.deserializeFromProto unpacks those bytes with a raw new ObjectInputStream(...).readObject() and no ObjectInputFilter/allow-list. So the "safe" recommended protobuf load path (Model.deserializeFromFile / deserializeFromStream / deserialize) still performs unfiltered Java deserialization of attacker-controlled bytes β the classic RCE primitive. Merely loading an untrusted .tribuo model triggers code execution β exactly the huntr Model File Vulnerabilities threat model.
This is an incomplete-fix: the protobuf migration was the security remediation, yet it left the LibLinear embedded-object path unguarded. main now adds ois.setObjectInputFilter(LIBLINEAR_FILTER) at this exact method β an upstream acknowledgment that this is a bug, not by-design β but that fix is unreleased, so all Maven Central artifacts are affected.
2. Vulnerable code (LibLinearClassificationModel.java, tribuo-classification-liblinear @ 4.3.2)
deserializeFromProto reads the serialized liblinear model and does (confirmed in the released bytecode β see poc/evidence/sink_disassembly.txt, javap -c -p):
// LibLinearModelProto proto -> models(0) is a raw java-serialized de.bwaldvogel.liblinear.Model:
byte[] modelBytes = proto.getModels(0).toByteArray(); // attacker-controlled
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(modelBytes));
de.bwaldvogel.liblinear.Model m = (de.bwaldvogel.liblinear.Model) ois.readObject(); // <-- unfiltered readObject
The javap disassembly shows the opcode order verbatim: new ObjectInputStream β invokevirtual ObjectInputStream.readObject() β then checkcast de/bwaldvogel/liblinear/Model. ObjectInputFilter count in the whole class = 0. The cast happens after readObject() returns, so a gadget fires regardless of the declared type (the cast merely throws ClassCastException afterward β too late).
The sibling org.tribuo.regression.liblinear.LibLinearRegressionModel.deserializeFromProto (line 129) is structurally identical (same embedded-model pattern, same unfiltered readObject, shared tribuo-common-liblinear LibLinearModelProto handling) β and is independently confirmed exploitable (see Β§3). This is a shared-infrastructure defect, not a single-module bug.
3. Proof of Concept
poc/ contains a minimal, self-checking PoC (all run against the released Maven Central jars β see poc/cp.txt, which is 100 % repo1.maven.org):
TrainAndPatch.javaβ trains a real minimalLibLinearClassificationModelon toy data, serializes it to a valid.tribuo(orig.tribuo) via the public API, then surgically replaces the embeddedmodels[0]serialized bytes with a serialized attacker object, producingpatched.tribuo.Evil.javaβ the attacker's serialized class; itsreadObjectdrops/tmp/tribuo_pwnedand runsRuntime.exec("id")(stands in for a gadget payload).Load.java/LoadVerbose.javaβ loadpatched.tribuothe way a victim would, via the documented publicorg.tribuo.Model.deserializeFromFile.orig.tribuo(benign) andpatched.tribuo(malicious) β the model files.
Observed result (poc/evidence/rce.txt):
[*] victim: Model.deserializeFromFile(patched.tribuo)
... LibLinearClassificationModel.deserializeFromProto(LibLinearClassificationModel.java:144)
Caused by: java.lang.ClassCastException: class Evil cannot be cast to class de.bwaldvogel.liblinear.Model
marker file /tmp/tribuo_pwned: CREATED
--- Runtime.exec(id) output captured by Evil.readObject: ---
uid=1000(wey) gid=1000(wey) groups=1000(wey),4(adm),20(dialout),24(cdrom),...
readObject() executed attacker code (id) on the standard public load path β RCE confirmed. The ClassCastException at deserializeFromProto:144 proves the code ran before the type check.
Weaponization without an attacker class (CONFIRMED, poc/upgrade/): no attacker class is needed. Tribuo's released transitive classpath (poc/cp.txt / poc/upgrade/stockcp.txt) bundles commons-beanutils-1.9.4, commons-collections-3.2.2, commons-collections4-4.4, commons-logging-1.2 β the full ysoserial CommonsBeanutils1 gadget chain. poc/upgrade/evil2.tribuo embeds a stock ysoserial CommonsBeanutils1 payload (no bespoke class); loading it via Model.deserializeFromFile with a classpath of only the 32 released Maven jars + a bare loader (verified: 0 Evil* classes, 0 ysoserial jars on the classpath) executed touch+id:
marker /tmp/tribuo_ysoserial_pwned: CREATED
uid=1000(wey) gid=1000(wey) groups=1000(wey),4(adm),...
(poc/evidence/rce_ysoserial_noplant.txt). This closes the "you'd need to plant a gadget class" objection β the RCE runs on stock Tribuo dependencies alone.
Honest caveat on the ysoserial variant:
CommonsBeanutils1isTemplatesImpl-based, so on JDK 16+ default modules the reflectivedefineClassis blocked β the no-plant payload above was run on JDK 17 with 5--add-opens(seepoc/evidence/). It is drop-in on JDK 8/11 (no opens needed), or any JVM already launched with those opens; a non-TemplatesImplchain (plausible givencommons-collections 3.2.2is present) would remove even that condition but was not demonstrated. The core sink RCE (the markerEvilPoC in Β§3 above) executes on any JDK β it is a plainreadObjecton aSerializableclass and does not depend on modules/reflection; the ysoserial variant only strengthens the "no attacker class" story.
Sibling confirmed executed: poc/upgrade/evil_reg.tribuo reproduces the identical RCE through LibLinearRegressionModel.deserializeFromProto (marker + id captured, poc/evidence/rce_regression_sibling.txt), proving the defect is shared across tribuo-common-liblinear consumers. Other liblinear-based Tribuo modules (e.g. anomaly detection) are likely-affected siblings by the same pattern.
4. Control test (negative test)
Loading a benign orig.tribuo (whose models[0] is a legitimate serialized de.bwaldvogel.liblinear.Model) loads with no code execution and no marker file β the RCE is attributable specifically to deserializing the attacker-controlled embedded bytes, not to normal model loading.
Remediation / Fix recommendation (5)
Install a strict ObjectInputFilter allow-list on the ObjectInputStream in LibLinearClassificationModel.deserializeFromProto (and the LibLinearRegressionModel sibling), permitting only de.bwaldvogel.liblinear.** (and required JDK primitives/arrays) and rejecting everything else β which is exactly what unreleased main now does (ois.setObjectInputFilter(LIBLINEAR_FILTER)). Ship it in a release. Better still, migrate the liblinear model to a genuine protobuf representation so no Java ObjectInputStream is on the load path at all.
6. Dedup / upstream status
Checked NVD/CVE, the GitHub Advisory Database (0 advisories for org.tribuo), and huntr (huntr.com/repos/oracle/tribuo β no reports): no prior disclosure of this sink. The bug class (unfiltered readObject) is well-known, but no filed advisory covers Tribuo's LibLinear protobuf loader. The unreleased main fix corroborates that this is a real, acknowledged, still-unpatched-in-release bug. Not a duplicate.
7. Reproduction environment
Linux, JDK 17, internet (coursier/Maven resolves org.tribuo:tribuo-classification-liblinear:4.3.2 + tribuo-json:4.3.2). poc/repro.sh is self-contained. No GPU.