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

AuswirkungDetails
IntegritätBereich: Log-Fälschung

Angreifer können gefälschte Log-Einträge einschleusen und damit Audit-Trails und forensische Beweise beschädigen.
NichtabstreitbarkeitBereich: Beweismanipulation

Eingeschleuste Logs können falsche Beweise erzeugen oder die Zuordnung bösartiger Aktionen aufheben.
VerfügbarkeitBereich: 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


Referenzen

  1. MITRE. "CWE-117: Improper Output Neutralization for Logs." https://cwe.mitre.org/data/definitions/117.html

  2. OWASP. "Log Injection." https://owasp.org/www-community/attacks/Log_Injection