Schwacher Passwort-Wiederherstellungsmechanismus für vergessenes Passwort
Beschreibung
Schwacher Passwort-Wiederherstellungsmechanismus für vergessenes Passwort tritt auf, wenn eine Anwendung einen Passwort-Wiederherstellungsprozess implementiert, der anfällig für Missbrauch oder Ausnutzung ist. Häufige Schwächen umfassen: Verwendung leicht erratbarer Sicherheitsfragen, Senden von Passwörtern im Klartext per E-Mail, Verwendung vorhersagbarer Passwort-Reset-Tokens, keine Ablaufzeit für Reset-Links, Erlauben unbegrenzter Reset-Versuche, Offenlegung ob Konten existieren, und Verwendung unsicherer sekundärer Authentifizierungsmethoden. Diese Schwächen ermöglichen Angreifern, Benutzerkonten zu übernehmen, ohne das ursprüngliche Passwort zu kennen.
Risiko
Passwort-Wiederherstellung ist ein kritischer Authentifizierungs-Bypass-Mechanismus und ein primäres Ziel für Angreifer. CVE-2023-7028 in GitLab CE/EE ermöglichte die Zustellung von Passwort-Reset-E-Mails an unverifizierte E-Mail-Adressen, was vollständige Kontoübernahme ohne Benutzerinteraktion ermöglichte und Versionen 16.1 bis 16.7 betraf. Schwerwiegende Schwachstellen wurden in HPE Cloudline, Ruijie Reyee OS, GLPI, IBM Security SOAR und Dell PowerProtect gefunden. Schwache Wiederherstellungsmechanismen ermöglichen Massenkontokompromittierung, besonders in Kombination mit E-Mail-Listen-Leaks oder Benutzernamen-Enumeration. Angreifer können administrative Konten übernehmen, auf sensible Daten zugreifen und persistenten Zugang aufrechterhalten.
Lösung
Generieren Sie kryptographisch zufällige, einmalig verwendbare Passwort-Reset-Tokens mit kurzen Ablaufzeiten (15-60 Minuten). Offenbaren Sie niemals durch unterschiedliche Antwortnachrichten, ob ein Konto existiert. Verwenden Sie seitenkanalresistenten Vergleich für Tokens. Fordern Sie zusätzliche Verifizierung (SMS-Code, Sicherheitsschlüssel) für Passwort-Resets. Implementieren Sie Rate-Limiting bei Reset-Anfragen. Invalidieren Sie alle bestehenden Sessions nach Passwortänderung. Senden Sie Passwort-Reset-Links, niemals tatsächliche Passwörter. Erwägen Sie passwortlose Authentifizierung oder Passkeys. Protokollieren Sie alle Passwort-Reset-Versuche für Sicherheitsüberwachung.
Häufige Auswirkungen
| Auswirkung | Details |
|---|---|
| Authentifizierung | Bereich: Kontoübernahme Angreifer erhalten vollen Zugriff auf Opferkonten durch Ausnutzung schwacher Wiederherstellungsmechanismen. |
| Vertraulichkeit | Bereich: Datenverletzung Kompromittierte Konten bieten Zugriff auf alle Benutzerdaten und potenziell administrative Funktionen. |
| Integrität | Bereich: Unbefugte Aktionen Angreifer können Kontoeinstellungen, Daten ändern und Aktionen als das Opfer durchführen. |
Beispielcode + Lösungscode
Verwundbarer Code
# VERWUNDBAR: Vorhersagbare Reset-Tokens
import time
import hashlib
def generate_reset_token(email):
# Token basiert auf vorhersagbaren Werten - kann erraten werden!
token = hashlib.md5(f"{email}{time.time()}".encode()).hexdigest()
return token
# VERWUNDBAR: Kontoexistenz offenlegen
@app.route('/forgot-password', methods=['POST'])
def forgot_password():
email = request.form['email']
user = User.query.filter_by(email=email).first()
if user:
send_reset_email(user)
return "Reset-E-Mail gesendet" # Unterschiedliche Antwort offenbart Kontoexistenz
else:
return "E-Mail nicht gefunden" # Angreifer weiß, E-Mail existiert nicht
# VERWUNDBAR: Keine Token-Ablaufzeit
def verify_reset_token(token):
reset = PasswordReset.query.filter_by(token=token).first()
if reset:
return reset.user # Token läuft nie ab!
return None
// VERWUNDBAR: Sicherheitsfragen mit erratbaren Antworten
public class PasswordRecovery {
private static final String[] SECURITY_QUESTIONS = {
"Wie lautet der Mädchenname Ihrer Mutter?", // Oft öffentliche Info
"Wie heißt Ihr Haustier?", // Leicht in Social Media zu finden
"In welcher Stadt wurden Sie geboren?" // Öffentliche Aufzeichnungen
};
public boolean verifySecurityAnswer(User user, String answer) {
// Groß-/Kleinschreibung ignorierender Vergleich offenbart Teilinformationen
return user.getSecurityAnswer().equalsIgnoreCase(answer);
}
// VERWUNDBAR: Passwort im Klartext per E-Mail senden
public void resetPassword(User user) {
String newPassword = generateSimplePassword(); // Schwaches Passwort
user.setPassword(newPassword); // Ohne Hashing gespeichert!
// Tatsächliches Passwort in E-Mail senden - abfangbar!
emailService.send(user.getEmail(),
"Ihr neues Passwort ist: " + newPassword);
}
}
// VERWUNDBAR: Reset-Token in URL ohne Ablaufzeit
app.get('/reset-password/:token', (req, res) => {
const token = req.params.token;
// Token dauerhaft gespeichert, läuft nie ab
const reset = db.findResetToken(token);
if (reset) {
// Token wiederverwendbar - Angreifer kann mehrfach zurücksetzen
res.render('reset-form', { userId: reset.userId });
}
});
// VERWUNDBAR: Kein Rate-Limiting
app.post('/forgot-password', (req, res) => {
// Angreifer kann unbegrenzte Resets anfordern
// um Opfer zu spammen oder Konten zu enumerieren
const user = db.findByEmail(req.body.email);
if (user) {
sendResetEmail(user);
}
res.json({ message: 'Wenn Konto existiert, wurde E-Mail gesendet' });
});
Lösungscode
# SICHER: Kryptographisch sichere Reset-Tokens
import secrets
from datetime import datetime, timedelta
def generate_reset_token_safe():
# 256 Bits kryptographische Zufälligkeit
return secrets.token_urlsafe(32)
# SICHER: Konsistente Antwort unabhängig von Kontoexistenz
@app.route('/forgot-password', methods=['POST'])
@rate_limit(max_requests=3, per_minutes=15)
def forgot_password_safe():
email = request.form['email']
user = User.query.filter_by(email=email).first()
if user:
# Sicheres Token mit Ablaufzeit generieren
token = generate_reset_token_safe()
expiration = datetime.utcnow() + timedelta(minutes=30)
# Alle bestehenden Tokens invalidieren
PasswordReset.query.filter_by(user_id=user.id).delete()
# Gehashtes Token speichern
token_hash = hash_token(token)
reset = PasswordReset(
user_id=user.id,
token_hash=token_hash,
expires_at=expiration
)
db.session.add(reset)
db.session.commit()
send_reset_email(user, token)
# Gleiche Antwort unabhängig davon, ob Benutzer existiert
return jsonify({
'message': 'Wenn ein Konto mit dieser E-Mail existiert, wurde ein Reset-Link gesendet'
})
# SICHER: Token-Verifizierung mit Ablaufzeit und Einmalverwendung
def verify_reset_token_safe(token):
token_hash = hash_token(token)
reset = PasswordReset.query.filter_by(token_hash=token_hash).first()
if not reset:
return None
# Ablaufzeit prüfen
if datetime.utcnow() > reset.expires_at:
db.session.delete(reset)
db.session.commit()
return None
return reset.user
def complete_reset(token, new_password):
user = verify_reset_token_safe(token)
if not user:
raise InvalidTokenError()
# Passwort mit ordnungsgemäßem Hashing aktualisieren
user.password_hash = hash_password(new_password)
# Token invalidieren (Einmalverwendung)
PasswordReset.query.filter_by(user_id=user.id).delete()
# Alle bestehenden Sessions invalidieren
Session.query.filter_by(user_id=user.id).delete()
db.session.commit()
# Passwortänderung protokollieren
audit_log.info(f"Passwort-Reset abgeschlossen für Benutzer {user.id}")
// SICHER: Sichere Passwort-Wiederherstellungsimplementierung
import java.security.SecureRandom;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
public class SecurePasswordRecovery {
private static final SecureRandom secureRandom = new SecureRandom();
private static final int TOKEN_LENGTH = 32;
private static final int TOKEN_EXPIRY_MINUTES = 30;
public String initiatePasswordReset(String email) {
User user = userRepository.findByEmail(email);
if (user != null) {
// Kryptographisch sicheres Token generieren
byte[] tokenBytes = new byte[TOKEN_LENGTH];
secureRandom.nextBytes(tokenBytes);
String token = Base64.getUrlEncoder().withoutPadding()
.encodeToString(tokenBytes);
// Gehashtes Token speichern
String tokenHash = hashToken(token);
Instant expiry = Instant.now().plus(TOKEN_EXPIRY_MINUTES, ChronoUnit.MINUTES);
// Bestehende Tokens invalidieren
passwordResetRepository.deleteByUserId(user.getId());
PasswordResetToken resetToken = new PasswordResetToken(
user.getId(),
tokenHash,
expiry
);
passwordResetRepository.save(resetToken);
// Reset-Link senden (nicht das Passwort!)
emailService.sendPasswordResetLink(user.getEmail(), token);
}
// Gleiche Antwort unabhängig von Benutzerexistenz
return "Wenn ein Konto existiert, wurde ein Reset-Link gesendet";
}
public boolean resetPassword(String token, String newPassword) {
String tokenHash = hashToken(token);
PasswordResetToken resetToken = passwordResetRepository
.findByTokenHash(tokenHash);
if (resetToken == null) {
auditLog.warn("Ungültiger Reset-Token-Versuch");
return false;
}
// Ablaufzeit prüfen
if (Instant.now().isAfter(resetToken.getExpiry())) {
passwordResetRepository.delete(resetToken);
return false;
}
// Passwort mit starkem Hashing aktualisieren
User user = userRepository.findById(resetToken.getUserId());
user.setPasswordHash(BCrypt.hashpw(newPassword, BCrypt.gensalt(12)));
userRepository.save(user);
// Token invalidieren (Einmalverwendung)
passwordResetRepository.delete(resetToken);
// Alle Sessions invalidieren
sessionRepository.deleteByUserId(user.getId());
// Bestätigung senden (nicht das Passwort!)
emailService.sendPasswordChangeConfirmation(user.getEmail());
auditLog.info("Passwort-Reset abgeschlossen für Benutzer: " + user.getId());
return true;
}
private String hashToken(String token) {
return Hashing.sha256().hashString(token, StandardCharsets.UTF_8).toString();
}
}
// SICHER: Vollständiger sicherer Passwort-Reset-Ablauf
const crypto = require('crypto');
const argon2 = require('argon2');
class SecurePasswordResetService {
constructor(db, emailService, rateLimiter) {
this.db = db;
this.emailService = emailService;
this.rateLimiter = rateLimiter;
}
async initiateReset(email, ip) {
// Rate-Limit nach IP
if (!await this.rateLimiter.check(ip, 'reset', 3, 900)) {
throw new RateLimitError('Zu viele Reset-Anfragen');
}
const user = await this.db.findUserByEmail(email);
if (user) {
// Sicheres Token generieren
const token = crypto.randomBytes(32).toString('base64url');
const tokenHash = crypto.createHash('sha256')
.update(token).digest('hex');
const expiresAt = new Date(Date.now() + 30 * 60 * 1000); // 30 Minuten
// Bestehende Tokens invalidieren
await this.db.deleteResetTokens(user.id);
// Gehashtes Token speichern
await this.db.createResetToken({
userId: user.id,
tokenHash,
expiresAt,
ipAddress: ip
});
// Reset-Link senden
await this.emailService.sendResetLink(user.email, token);
}
// Immer gleiche Antwort zurückgeben
return { message: 'Wenn Konto existiert, wurde Reset-Link gesendet' };
}
async completeReset(token, newPassword, ip) {
const tokenHash = crypto.createHash('sha256')
.update(token).digest('hex');
const resetToken = await this.db.findResetToken(tokenHash);
if (!resetToken) {
await this.logFailedAttempt(ip);
throw new InvalidTokenError();
}
if (new Date() > resetToken.expiresAt) {
await this.db.deleteResetToken(resetToken.id);
throw new ExpiredTokenError();
}
// Neues Passwort mit Argon2 hashen
const passwordHash = await argon2.hash(newPassword, {
type: argon2.argon2id,
memoryCost: 65536,
timeCost: 3,
parallelism: 4
});
// Passwort aktualisieren
await this.db.updateUserPassword(resetToken.userId, passwordHash);
// Token invalidieren (Einmalverwendung)
await this.db.deleteResetToken(resetToken.id);
// Alle Sessions invalidieren
await this.db.deleteUserSessions(resetToken.userId);
// Bestätigung senden
const user = await this.db.findUserById(resetToken.userId);
await this.emailService.sendPasswordChangedNotification(user.email);
return { message: 'Passwort erfolgreich zurückgesetzt' };
}
}
Ausgenutzt in der Praxis
GitLab-Kontoübernahme (GitLab, 2024)
CVE-2023-7028 in GitLab CE/EE 16.1-16.7 ermöglichte das Senden von Passwort-Reset-E-Mails an unverifizierte E-Mail-Adressen, was vollständige Kontoübernahme ohne Benutzerinteraktion oder Authentifizierung ermöglichte—remote ausnutzbar mit wiederholbarem Erfolg.
HPE Cloudline Server (HPE, 2025)
Schwerwiegende schwache Passwort-Wiederherstellungs-Schwachstelle in HPE Cloudline CL4150 Gen10 Server ermöglichte Angreifern, Authentifizierung durch fehlerhafte Wiederherstellungsmechanismen zu umgehen.
Ruijie Reyee OS (Ruijie, 2024)
Schwerwiegende Passwort-Wiederherstellungs-Schwachstelle in Ruijie Reyee OS Netzwerkequipment ermöglichte Angreifern, Gerätemanagement-Konten zu übernehmen.
Tools zum Testen/Ausnutzen
- Burp Suite — Passwort-Reset-Abläufe und Token-Vorhersagbarkeit testen.
- OWASP ZAP — automatisiertes Testen von Authentifizierungsmechanismen.
- Hydra — Rate-Limiting auf Reset-Endpunkten testen.
CVE-Beispiele
- CVE-2023-7028 — GitLab Passwort-Reset an unverifizierte E-Mail.
- CVE-2017-12426 — GitLab Passwort-Reset-Token-Offenlegung.
- CVE-2012-3427 — Red Hat JBoss Passwort-Reset-Schwachstelle.
Referenzen
- MITRE. "CWE-640: Weak Password Recovery Mechanism for Forgotten Password." https://cwe.mitre.org/data/definitions/640.html
- OWASP. "Forgot Password Cheat Sheet." https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html