Unsachgemäße Ausgabeneutralisierung für Logs
Beschreibung
Unsachgemäße Ausgabeneutralisierung für Logs tritt auf, wenn Software Log-Einträge aus benutzergesteuerten Eingaben erstellt, ohne Sonderzeichen ordnungsgemäß zu neutralisieren, die von Log-Anzeige-Tools oder Log-Verarbeitungssystemen interpretiert werden könnten. Angreifer können gefälschte Log-Einträge einschleusen, Audit-Trails fälschen, schädliche Inhalte in SIEM-Systeme injizieren oder Log-Anzeigeanwendungen ausnutzen. Dies wird häufig als "Log Injection" oder "Log Forging" bezeichnet.
Risiko
Log Injection untergräbt die Integrität von Audit-Trails und Sicherheitsüberwachung. Angreifer können gefälschte Einträge einschleusen, um ihre Spuren zu verwischen, falsche Beweise zu erstellen oder Alarme auszulösen, um Sicherheitsteams abzulenken. In SIEM-Systemen können eingeschleuste Einträge automatisierte Reaktionen auslösen oder Erkennungsregeln umgehen. Log4Shell (CVE-2021-44228) zeigte, dass Log-Frameworks eingeschleuste Inhalte ausführen können. Selbst ohne Code-Ausführung können gefälschte Logs rechtliche Auswirkungen haben, indem sie Beweise und für die Compliance erforderliche Audit-Aufzeichnungen beschädigen.
Lösung
Bereinigen Sie alle Benutzereingaben, bevor sie in Log-Einträge aufgenommen werden. Kodieren oder escapen Sie Sonderzeichen (Zeilenumbrüche, CRLF, Steuerzeichen). Verwenden Sie strukturierte Log-Formate (JSON) anstelle von Klartext. Validieren Sie das Log-Eintrags-Format auf Ebene des Logging-Frameworks. Implementieren Sie Log-Integritätsprüfung (Signierung, Hashing). Konfigurieren Sie SIEM-Systeme zur Erkennung von Injection-Versuchen. Verwenden Sie separates Logging für benutzergesteuerte Daten und Systemereignisse.
Häufige Auswirkungen
| Auswirkung | Details |
|---|---|
| Integrität | Bereich: Log-Fälschung Angreifer können gefälschte Log-Einträge einschleusen und damit Audit-Trails und forensische Beweise beschädigen. |
| Nichtabstreitbarkeit | Bereich: Beweismanipulation Eingeschleuste Logs können falsche Beweise erzeugen oder die Zuordnung bösartiger Aktionen aufheben. |
| Verfügbarkeit | Bereich: Systemmanipulation SIEM-Systeme können automatisierte Reaktionen auf Basis gefälschter Log-Einträge ausführen. |
Beispielcode und Lösung
Verwundbarer Code
# VERWUNDBAR: Direkte Protokollierung von Benutzereingaben
import logging
logger = logging.getLogger(__name__)
@app.route('/login', methods=['POST'])
def login_vulnerable():
username = request.form['username']
password = request.form['password']
# Angreifer-Benutzername: "admin\n[CRITICAL] System compromised!"
logger.info(f"Login attempt for user: {username}")
# Resultierendes Log:
# INFO Login attempt for user: admin
# [CRITICAL] System compromised!
if authenticate(username, password):
logger.info(f"User {username} logged in successfully")
return redirect('/dashboard')
else:
logger.warning(f"Failed login for user: {username}")
return 'Login failed', 401
# VERWUNDBAR: Log Injection in der Fehlerbehandlung
@app.route('/api/data')
def get_data_vulnerable():
user_id = request.args.get('id')
try:
data = fetch_data(user_id)
return jsonify(data)
except Exception as e:
# Benutzer kontrolliert den Inhalt der Fehlermeldung!
logger.error(f"Error fetching data for {user_id}: {str(e)}")
return 'Error', 500
// VERWUNDBAR: Java-Logging ohne Bereinigung
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@RestController
public class VulnerableLogging {
private static final Logger logger = LoggerFactory.getLogger(VulnerableLogging.class);
@PostMapping("/login")
public ResponseEntity<?> login(@RequestParam String username, @RequestParam String password) {
// Angreifer: username = "admin\n2024-01-01 [ERROR] Payment failed for card 4111-1111-1111-1111"
logger.info("Login attempt for user: {}", username);
if (authenticate(username, password)) {
logger.info("User {} logged in successfully", username);
return ResponseEntity.ok().build();
} else {
logger.warn("Failed login for user: {}", username);
return ResponseEntity.status(401).build();
}
}
// VERWUNDBAR: Ausnahmenachricht in Logs
@GetMapping("/data/{id}")
public ResponseEntity<?> getData(@PathVariable String id) {
try {
return ResponseEntity.ok(dataService.find(id));
} catch (Exception e) {
// Ausnahmenachricht kann Benutzereingaben enthalten
logger.error("Error getting data: {}", e.getMessage());
return ResponseEntity.status(500).build();
}
}
}
// VERWUNDBAR: Node.js-Logging
const logger = require('winston').createLogger({ /* config */ });
app.post('/login', (req, res) => {
const { username, password } = req.body;
// Angreifer: username = "admin\n[ALERT] Security breach detected from IP 192.168.1.1"
logger.info(`Login attempt for user: ${username}`);
if (authenticate(username, password)) {
logger.info(`User ${username} logged in successfully`);
res.redirect('/dashboard');
} else {
logger.warn(`Failed login for user: ${username}`);
res.status(401).send('Login failed');
}
});
// VERWUNDBAR: User-Agent-Protokollierung
app.use((req, res, next) => {
// Der User-Agent-Header wird vom Benutzer kontrolliert!
logger.info(`Request from: ${req.headers['user-agent']} to ${req.path}`);
next();
});
Sichere Lösung
# SICHER: Bereinigte Protokollierung
import logging
import re
logger = logging.getLogger(__name__)
def sanitize_log_input(value):
"""Entfernt Zeichen, die Log Injection verursachen könnten."""
if value is None:
return ''
# In String konvertieren
value = str(value)
# Zeilenumbrüche, Wagenrückläufe und andere Steuerzeichen entfernen
value = re.sub(r'[\r\n\t]', ' ', value)
# ANSI-Escape-Sequenzen entfernen
value = re.sub(r'\x1b\[[0-9;]*m', '', value)
# Länge begrenzen
return value[:200]
@app.route('/login', methods=['POST'])
def login_safe():
username = request.form.get('username', '')
password = request.form.get('password', '')
# Vor der Protokollierung bereinigen
safe_username = sanitize_log_input(username)
logger.info("Login attempt for user: %s", safe_username)
if authenticate(username, password):
logger.info("User %s logged in successfully", safe_username)
return redirect('/dashboard')
else:
logger.warning("Failed login for user: %s", safe_username)
return 'Login failed', 401
# SICHER: Strukturiertes Logging (JSON-Format)
import json
import structlog
structlog.configure(
processors=[
structlog.processors.JSONRenderer()
]
)
log = structlog.get_logger()
@app.route('/login', methods=['POST'])
def login_structured():
username = request.form.get('username', '')
# Strukturiertes Logging - Daten werden korrekt escaped
log.info(
"login_attempt",
username=username, # Wird JSON-escaped
ip_address=request.remote_addr,
user_agent=request.headers.get('User-Agent', '')
)
if authenticate(username, request.form.get('password', '')):
log.info("login_success", username=username)
return redirect('/dashboard')
else:
log.warning("login_failed", username=username)
return 'Login failed', 401
# SICHER: Benutzerdefinierter Log-Formatter mit Escaping
class SafeFormatter(logging.Formatter):
def format(self, record):
# Alle String-Argumente bereinigen
if record.args:
record.args = tuple(
sanitize_log_input(arg) if isinstance(arg, str) else arg
for arg in record.args
)
return super().format(record)
handler = logging.StreamHandler()
handler.setFormatter(SafeFormatter('%(asctime)s - %(levelname)s - %(message)s'))
logger.addHandler(handler)
// SICHER: Java mit bereinigter Protokollierung
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.owasp.encoder.Encode;
@RestController
public class SecureLogging {
private static final Logger logger = LoggerFactory.getLogger(SecureLogging.class);
@PostMapping("/login")
public ResponseEntity<?> login(@RequestParam String username, @RequestParam String password) {
// Vor der Protokollierung bereinigen
String safeUsername = sanitizeForLog(username);
logger.info("Login attempt for user: {}", safeUsername);
if (authenticate(username, password)) {
logger.info("User {} logged in successfully", safeUsername);
return ResponseEntity.ok().build();
} else {
logger.warn("Failed login for user: {}", safeUsername);
return ResponseEntity.status(401).build();
}
}
private String sanitizeForLog(String input) {
if (input == null) {
return "";
}
// Zeilenumbrüche und Steuerzeichen entfernen
String sanitized = input
.replaceAll("[\r\n\t]", " ")
.replaceAll("[\\x00-\\x1F\\x7F]", "");
// Länge begrenzen
if (sanitized.length() > 200) {
sanitized = sanitized.substring(0, 200) + "...";
}
return sanitized;
}
}
// SICHER: Verwendung von strukturiertem Logging mit Logback + JSON
// logback.xml Konfiguration:
/*
<appender name="JSON" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<fieldNames>
<message>msg</message>
</fieldNames>
</encoder>
</appender>
*/
@Service
public class AuditService {
private static final Logger auditLogger = LoggerFactory.getLogger("AUDIT");
public void logAuditEvent(String action, String userId, Map<String, Object> details) {
// Verwendung von MDC für strukturiertes Logging
MDC.put("action", action);
MDC.put("userId", sanitizeForLog(userId));
MDC.put("timestamp", Instant.now().toString());
try {
auditLogger.info("Audit event: {}", sanitizeForLog(action));
} finally {
MDC.clear();
}
}
}
// SICHER: Benutzerdefinierter Logback-Filter
public class LogSanitizingFilter extends Filter<ILoggingEvent> {
@Override
public FilterReply decide(ILoggingEvent event) {
// Log-Nachrichten hier untersuchen und bereinigen
return FilterReply.NEUTRAL;
}
}
// SICHER: Node.js mit bereinigter Protokollierung
const winston = require('winston');
function sanitizeLogInput(value) {
if (value === null || value === undefined) {
return '';
}
return String(value)
// Zeilenumbrüche und Wagenrückläufe entfernen
.replace(/[\r\n\t]/g, ' ')
// ANSI-Escape-Codes entfernen
.replace(/\x1b\[[0-9;]*m/g, '')
// Andere Steuerzeichen entfernen
.replace(/[\x00-\x1F\x7F]/g, '')
// Länge begrenzen
.substring(0, 200);
}
// Benutzerdefiniertes Format mit Bereinigung
const sanitizeFormat = winston.format((info) => {
// Nachricht bereinigen
if (typeof info.message === 'string') {
info.message = sanitizeLogInput(info.message);
}
// Alle Metadaten bereinigen
for (const [key, value] of Object.entries(info)) {
if (key !== 'level' && key !== 'timestamp' && typeof value === 'string') {
info[key] = sanitizeLogInput(value);
}
}
return info;
});
const logger = winston.createLogger({
format: winston.format.combine(
sanitizeFormat(),
winston.format.timestamp(),
winston.format.json() // JSON-Ausgabe für strukturiertes Logging
),
transports: [new winston.transports.Console()]
});
app.post('/login', (req, res) => {
const { username, password } = req.body;
// Sicher zu protokollieren - wird bereinigt
logger.info('Login attempt', {
username: username,
ipAddress: req.ip,
userAgent: req.headers['user-agent']
});
if (authenticate(username, password)) {
logger.info('Login success', { username });
res.redirect('/dashboard');
} else {
logger.warn('Login failed', { username });
res.status(401).send('Login failed');
}
});
// SICHER: Verwendung von Pino für hochperformantes strukturiertes Logging
const pino = require('pino');
const pinoLogger = pino({
formatters: {
log: (obj) => {
// Alle String-Werte bereinigen
const sanitized = {};
for (const [key, value] of Object.entries(obj)) {
sanitized[key] = typeof value === 'string' ?
sanitizeLogInput(value) : value;
}
return sanitized;
}
}
});
// Verwendung
pinoLogger.info({ username: req.body.username }, 'Login attempt');
Ausgenutzt in der Praxis
Log4Shell (CVE-2021-44228)
Obwohl es sich hauptsächlich um eine JNDI-Injection-Schwachstelle handelt, zeigte Log4Shell, wie die Protokollierung benutzergesteuerter Daten zu Remote Code Execution führen kann, wenn das Logging-Framework spezielle Syntax verarbeitet.
SIEM-Umgehungsangriffe
Angreifer haben Log Injection genutzt, um gefälschte "Entwarnung"-Nachrichten zu erstellen oder Fehlalarme auszulösen, um Sicherheitsteams während tatsächlicher Angriffe abzulenken.
Audit-Trail-Manipulation
Finanzsysteme wurden durch Log Injection angegriffen, um Spuren betrügerischer Transaktionen durch Fälschung von Audit-Einträgen zu verwischen.
Tools zum Testen und Ausnutzen
-
Burp Suite -- Log-Payloads über Request-Parameter einschleusen.
-
Manuelles Testen mit CRLF-Sequenzen (%0d%0a).
-
Benutzerdefinierte Skripte zum Testen von Log-Injection-Mustern.
CVE-Beispiele
-
CVE-2021-44228 -- Log4Shell.
-
CVE-2019-17571 -- Apache Log4j Deserialisierung.
-
CVE-2017-5929 -- QOS.ch Logback JNDI.
Referenzen
-
MITRE. "CWE-117: Improper Output Neutralization for Logs." https://cwe.mitre.org/data/definitions/117.html
-
OWASP. "Log Injection." https://owasp.org/www-community/attacks/Log_Injection