Verwendung von extern kontrollierter Eingabe zur Auswahl von Klassen oder Code
Beschreibung
Die Verwendung von extern kontrollierter Eingabe zur Auswahl von Klassen oder Code tritt auf, wenn Software externe Eingaben verwendet, um zu bestimmen, welche Klassen instanziiert werden, welche Codepfade ausgeführt werden oder welche Methoden aufgerufen werden. Dies wird häufig bei Objekt-Deserialisierung, reflexionsbasierter Instanziierung, dynamischem Klassenladen und Plugin-Systemen beobachtet. Angreifer können diese Eingabe manipulieren, um bösartige Klassen zu laden, beliebigen Code auszuführen oder auf unbeabsichtigte Funktionalität zuzugreifen.
Risiko
Diese Schwachstelle ermöglicht einige der schwersten Angriffe in der Softwaresicherheit. Unsichere Deserialisierung wurde durchgehend in den OWASP Top 10 gelistet und hat zahlreiche hochkarätige Sicherheitsvorfälle verursacht. Angreifer können Remote Code Execution (RCE) erreichen, indem sie bösartige Klassennamen angeben, die Code während der Instanziierung oder Deserialisierung ausführen. Javas Deserialisierungs-Schwachstellen (unter Verwendung von Gadget-Ketten wie in Apache Commons Collections) haben unzählige Unternehmenssysteme kompromittiert. Ähnliche Probleme existieren in PHPs unserialize(), Pythons pickle und .NETs BinaryFormatter.
Lösung
Vermeiden Sie die Verwendung externer Eingaben zur Auswahl von Klassen wann immer möglich. Implementieren Sie strikte Allowlistn für zulässige Klassennamen, wenn dynamisches Laden erforderlich ist. Deserialisieren Sie niemals nicht vertrauenswürdige Daten mit nativen Serialisierungsmechanismen—verwenden Sie sichere Alternativen wie JSON. Validieren und bereinigen Sie alle Eingaben, die bei Reflexion oder Klassenladen verwendet werden. Verwenden Sie typsichere Deserialisierungsbibliotheken. Implementieren Sie Look-Ahead-Deserialisierung, die Klassennamen vor der Instanziierung validiert. Entfernen Sie gefährliche Gadget-Klassen aus dem Classpath. Erwägen Sie die Verwendung signierter serialisierter Daten.
Häufige Auswirkungen
| Auswirkung | Details |
|---|---|
| Vertraulichkeit | Umfang: Remote Code Execution Angreifer können beliebigen Code auf dem Server durch bösartige Klasseninstanziierung ausführen. |
| Integrität | Umfang: Systemkompromittierung Vollständige Systemübernahme durch Codeausführung während der Deserialisierung. |
| Verfügbarkeit | Umfang: Denial of Service Ressourcenerschöpfung durch bösartige Objektkonstruktion. |
Beispielcode
Anfälliger Code
# ANFÄLLIG: Dynamisches Klassenladen aus Benutzereingabe
class VulnerableFactory:
def create_handler(self, handler_type):
# Benutzer kontrolliert welche Klasse instanziiert wird!
module = __import__('handlers')
handler_class = getattr(module, handler_type)
return handler_class()
# Angreifer-Eingabe: "os.system('rm -rf /')"
# ANFÄLLIG: Pickle-Deserialisierung
import pickle
import base64
def load_session_vulnerable(session_data):
# Deserialisierung nicht vertrauenswürdiger Daten!
return pickle.loads(base64.b64decode(session_data))
# Angreifer-Payload:
class Exploit:
def __reduce__(self):
import os
return (os.system, ('id',))
# ANFÄLLIG: exec/eval mit Benutzereingabe
def execute_action_vulnerable(action_code):
# Direkte Codeausführung!
exec(action_code)
# ANFÄLLIG: Dynamischer Import
def load_plugin_vulnerable(plugin_name):
# Benutzer kontrolliert Import-Pfad!
plugin = __import__(plugin_name)
return plugin.run()
# ANFÄLLIG: YAML unsicheres Laden
import yaml
def load_config_vulnerable(yaml_string):
# Unsicheres YAML-Laden erlaubt beliebige Python-Objekte!
return yaml.load(yaml_string) # Kein Loader angegeben!
# Angreifer-YAML:
# !!python/object/apply:os.system ['id']
// ANFÄLLIG: Java-Deserialisierung
public class VulnerableDeserialization {
public Object deserialize(byte[] data) throws Exception {
// Deserialisierung nicht vertrauenswürdiger Daten!
ByteArrayInputStream bis = new ByteArrayInputStream(data);
ObjectInputStream ois = new ObjectInputStream(bis);
return ois.readObject(); // RCE wenn Gadget-Ketten vorhanden!
}
}
// ANFÄLLIG: Dynamische Klasseninstanziierung
public class VulnerableFactory {
public Object createObject(String className) throws Exception {
// Benutzer kontrolliert Klassennamen!
Class<?> clazz = Class.forName(className);
return clazz.getDeclaredConstructor().newInstance();
}
public void invokeMethod(String className, String methodName, Object[] args)
throws Exception {
// Benutzer kontrolliert sowohl Klasse als auch Methode!
Class<?> clazz = Class.forName(className);
Object instance = clazz.getDeclaredConstructor().newInstance();
Method method = clazz.getMethod(methodName, getParameterTypes(args));
method.invoke(instance, args);
}
}
// ANFÄLLIG: XMLDecoder
import java.beans.XMLDecoder;
public class VulnerableXMLDecoder {
public Object decode(InputStream is) {
// XMLDecoder kann beliebige Objekte instanziieren!
XMLDecoder decoder = new XMLDecoder(is);
return decoder.readObject();
}
}
// ANFÄLLIG: Spring Klassenname aus Request
@RestController
public class VulnerableController {
@PostMapping("/execute")
public Object execute(@RequestParam String className) throws Exception {
// Klasse aus Request-Parameter!
Class<?> clazz = Class.forName(className);
return clazz.getDeclaredConstructor().newInstance();
}
}
// ANFÄLLIG: eval mit Benutzereingabe
app.post('/execute', (req, res) => {
const code = req.body.code;
// Direktes eval von Benutzereingabe!
const result = eval(code);
res.json({ result });
});
// ANFÄLLIG: new Function()
app.post('/calculate', (req, res) => {
const formula = req.body.formula;
// Benutzer kontrolliert Funktionskörper!
const fn = new Function('return ' + formula);
res.json({ result: fn() });
});
// ANFÄLLIG: Dynamisches require
app.get('/plugin/:name', (req, res) => {
const pluginName = req.params.name;
// Benutzer kontrolliert require-Pfad!
const plugin = require('./plugins/' + pluginName);
res.json(plugin.run());
});
// ANFÄLLIG: Node.js unserialize
const serialize = require('node-serialize');
app.post('/session', (req, res) => {
const sessionData = req.body.session;
// Deserialisierung nicht vertrauenswürdiger Daten!
const session = serialize.unserialize(sessionData);
res.json(session);
});
// Angreifer-Payload:
// {"rce":"_$$ND_FUNC$$_function(){require('child_process').exec('id')}()"}
// ANFÄLLIG: vm.runInContext
const vm = require('vm');
app.post('/eval', (req, res) => {
const code = req.body.code;
const context = vm.createContext({});
// Benutzercode-Ausführung in VM (kann escaped werden!)
const result = vm.runInContext(code, context);
res.json({ result });
});
Korrigierter Code
# SICHER: Allowlist für zulässige Klassen
class SecureFactory:
ALLOWED_HANDLERS = {
'json': 'handlers.JsonHandler',
'xml': 'handlers.XmlHandler',
'csv': 'handlers.CsvHandler'
}
def create_handler(self, handler_type):
if handler_type not in self.ALLOWED_HANDLERS:
raise ValueError(f"Ungültiger Handler-Typ: {handler_type}")
module_path, class_name = self.ALLOWED_HANDLERS[handler_type].rsplit('.', 1)
module = __import__(module_path, fromlist=[class_name])
handler_class = getattr(module, class_name)
return handler_class()
# SICHER: JSON anstelle von pickle verwenden
import json
def load_session_safe(session_data):
"""JSON für sichere Deserialisierung verwenden."""
return json.loads(base64.b64decode(session_data))
# SICHER: Wenn pickle erforderlich ist, eingeschränkten Unpickler verwenden
import pickle
import io
class RestrictedUnpickler(pickle.Unpickler):
ALLOWED_CLASSES = {
('myapp.models', 'User'),
('myapp.models', 'Session'),
}
def find_class(self, module, name):
if (module, name) not in self.ALLOWED_CLASSES:
raise pickle.UnpicklingError(
f"Verbotene Klasse: {module}.{name}"
)
return super().find_class(module, name)
def restricted_loads(data):
"""Sicheres Unpickle mit Klasseneinschränkungen."""
return RestrictedUnpickler(io.BytesIO(data)).load()
# SICHER: Keine dynamische Codeausführung
def execute_action_safe(action_name, params):
"""Aktions-Mapping anstelle von exec verwenden."""
ACTIONS = {
'send_email': send_email_action,
'generate_report': generate_report_action,
'notify_user': notify_user_action,
}
if action_name not in ACTIONS:
raise ValueError(f"Unbekannte Aktion: {action_name}")
return ACTIONS[action_name](**params)
# SICHER: Plugin-Laden mit Allowlist
ALLOWED_PLUGINS = {'analytics', 'reporting', 'export'}
def load_plugin_safe(plugin_name):
if plugin_name not in ALLOWED_PLUGINS:
raise ValueError(f"Plugin nicht erlaubt: {plugin_name}")
# Bereinigen um Path-Traversal zu verhindern
safe_name = plugin_name.replace('.', '').replace('/', '').replace('\\', '')
plugin = __import__(f'plugins.{safe_name}', fromlist=['run'])
return plugin.run()
# SICHER: YAML sicheres Laden
import yaml
def load_config_safe(yaml_string):
"""safe_load verwenden um beliebige Objektinstanziierung zu verhindern."""
return yaml.safe_load(yaml_string)
// SICHER: Java mit Deserialisierungsfilter
public class SecureDeserialization {
private static final Set<String> ALLOWED_CLASSES = Set.of(
"com.myapp.model.User",
"com.myapp.model.Session",
"java.util.ArrayList",
"java.util.HashMap"
);
public Object deserialize(byte[] data) throws Exception {
ByteArrayInputStream bis = new ByteArrayInputStream(data);
ObjectInputStream ois = new ObjectInputStream(bis) {
@Override
protected Class<?> resolveClass(ObjectStreamClass desc)
throws IOException, ClassNotFoundException {
String className = desc.getName();
if (!ALLOWED_CLASSES.contains(className)) {
throw new InvalidClassException(
"Unbefugter Deserialisierungsversuch",
className
);
}
return super.resolveClass(desc);
}
};
return ois.readObject();
}
// Verwendung von Java 9+ ObjectInputFilter
public Object deserializeWithFilter(byte[] data) throws Exception {
ByteArrayInputStream bis = new ByteArrayInputStream(data);
ObjectInputStream ois = new ObjectInputStream(bis);
ois.setObjectInputFilter(filterInfo -> {
Class<?> clazz = filterInfo.serialClass();
if (clazz == null) {
return ObjectInputFilter.Status.ALLOWED;
}
String className = clazz.getName();
if (ALLOWED_CLASSES.contains(className) ||
className.startsWith("java.lang.") ||
className.startsWith("[")) {
return ObjectInputFilter.Status.ALLOWED;
}
return ObjectInputFilter.Status.REJECTED;
});
return ois.readObject();
}
}
// SICHER: Sichere Factory mit Allowlist
public class SecureFactory {
private static final Map<String, Class<?>> ALLOWED_CLASSES = Map.of(
"json", JsonHandler.class,
"xml", XmlHandler.class,
"csv", CsvHandler.class
);
public Handler createHandler(String type) {
Class<?> clazz = ALLOWED_CLASSES.get(type);
if (clazz == null) {
throw new IllegalArgumentException("Ungültiger Handler-Typ: " + type);
}
try {
return (Handler) clazz.getDeclaredConstructor().newInstance();
} catch (Exception e) {
throw new RuntimeException("Handler-Erstellung fehlgeschlagen", e);
}
}
}
// SICHER: JSON anstelle von Java-Serialisierung verwenden
import com.fasterxml.jackson.databind.ObjectMapper;
public class SafeSerialization {
private final ObjectMapper mapper = new ObjectMapper();
public String serialize(Object obj) throws Exception {
return mapper.writeValueAsString(obj);
}
public <T> T deserialize(String json, Class<T> type) throws Exception {
return mapper.readValue(json, type);
}
}
// SICHER: Spring mit validiertem Enum
@RestController
public class SecureController {
public enum HandlerType {
JSON, XML, CSV
}
@PostMapping("/execute")
public ResponseEntity<?> execute(@RequestParam HandlerType type) {
// Typ wird von Spring validiert - nur Enum-Werte akzeptiert
Handler handler = HandlerFactory.create(type);
return ResponseEntity.ok(handler.process());
}
}
// SICHER: Allowlist anstelle von eval
const OPERATIONS = {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
multiply: (a, b) => a * b,
divide: (a, b) => b !== 0 ? a / b : null
};
app.post('/calculate', (req, res) => {
const { operation, a, b } = req.body;
if (!OPERATIONS[operation]) {
return res.status(400).json({ error: 'Ungültige Operation' });
}
const result = OPERATIONS[operation](Number(a), Number(b));
res.json({ result });
});
// SICHER: Plugin-Laden mit Allowlist
const ALLOWED_PLUGINS = new Set(['analytics', 'reporting', 'export']);
app.get('/plugin/:name', (req, res) => {
const pluginName = req.params.name;
if (!ALLOWED_PLUGINS.has(pluginName)) {
return res.status(400).json({ error: 'Ungültiges Plugin' });
}
// Sicher weil Name aus Allowlist
const plugin = require(`./plugins/${pluginName}`);
res.json(plugin.run());
});
// SICHER: JSON anstelle von serialize
app.post('/session', (req, res) => {
try {
// JSON verwenden - keine Codeausführung
const session = JSON.parse(req.body.session);
// Struktur validieren
if (!session.userId || typeof session.userId !== 'string') {
throw new Error('Ungültiges Session-Format');
}
res.json(session);
} catch (e) {
res.status(400).json({ error: 'Ungültige Session-Daten' });
}
});
// SICHER: Ausdrucksauswertung mit math.js (sandboxed)
const math = require('mathjs');
// Eingeschränkte Math-Instanz erstellen
const limitedMath = math.create(math.all);
limitedMath.import({
import: function () { throw new Error('deaktiviert'); },
createUnit: function () { throw new Error('deaktiviert'); },
evaluate: function () { throw new Error('deaktiviert'); },
parse: function () { throw new Error('deaktiviert'); },
simplify: function () { throw new Error('deaktiviert'); },
derivative: function () { throw new Error('deaktiviert'); }
}, { override: true });
app.post('/math', (req, res) => {
try {
const expression = req.body.expression;
// Nur grundlegende Matheoperationen erlauben
const result = limitedMath.evaluate(expression);
res.json({ result });
} catch (e) {
res.status(400).json({ error: 'Ungültiger Ausdruck' });
}
});
// SICHER: Factory-Pattern mit Registry
class HandlerRegistry {
constructor() {
this.handlers = new Map();
}
register(name, HandlerClass) {
this.handlers.set(name, HandlerClass);
}
create(name, ...args) {
const HandlerClass = this.handlers.get(name);
if (!HandlerClass) {
throw new Error(`Unbekannter Handler: ${name}`);
}
return new HandlerClass(...args);
}
}
const registry = new HandlerRegistry();
registry.register('json', JsonHandler);
registry.register('xml', XmlHandler);
registry.register('csv', CsvHandler);
app.post('/process', (req, res) => {
try {
const handler = registry.create(req.body.type);
res.json(handler.process(req.body.data));
} catch (e) {
res.status(400).json({ error: e.message });
}
});
Ausgenutzt in der Praxis
Apache Struts (Equifax-Datenleck - 2017)
CVE-2017-5638 ermöglichte Remote Code Execution durch OGNL-Expression-Injection und betraf 147 Millionen Menschen.
Apache Commons Collections
Gadget-Ketten in Commons Collections ermöglichten RCE durch Java-Deserialisierung auf tausenden von Systemen.
Jenkins, WebLogic, JBoss
Mehrere kritische RCE-Schwachstellen durch Java-Deserialisierung haben große Enterprise-Plattformen betroffen.
Werkzeuge zum Testen/Ausnutzen
-
ysoserial — Java-Deserialisierungs-Payload-Generator.
-
marshalsec — Java-Unmarshaller-Exploits.
-
PHPGGC — PHP-Gadget-Chain-Generator.
-
Burp Suite — Deserialisierungs-Endpunkte testen.
CVE-Beispiele
-
CVE-2017-5638 — Apache Struts RCE.
-
CVE-2015-7501 — Apache Commons Collections.
-
CVE-2020-9484 — Apache Tomcat Deserialisierung.
Referenzen
-
MITRE. "CWE-470: Use of Externally-Controlled Input to Select Classes or Code." https://cwe.mitre.org/data/definitions/470.html
-
OWASP. "Insecure Deserialization." https://owasp.org/Top10/A08_2021-Software_and_Data_Integrity_Failures/