Ghost CMS CVE-2026-26980: Critical SQL Injection Leads to Admin Takeover and ClickFix Campaigns
Attackers are actively exploiting CVE-2026-26980, a critical SQL injection in the Ghost CMS Content API, to extract Admin API Keys. Stolen keys are used to inject malicious JavaScript into published articles, serving ClickFix social engineering payloads to website visitors.
On this page 0% read
Executive Summary
A critical SQL injection vulnerability, CVE-2026-26980, in the Content API of Ghost CMS allows unauthenticated remote attackers to read arbitrary data from the database, most notably the site’s Admin API Keys [Source 1] [Source 2]. Possession of these keys allows full administrative control over the Ghost instance [Source 2] [Source 3].
Starting around May 2026, threat actors launched a widespread campaign exploiting this vulnerability, compromising over 700 websites (including academic institutions and enterprise portals) [Source 2] [Source 4]. Attackers used stolen Admin API keys to inject malicious JavaScript into published articles, executing ClickFix social engineering attacks [Source 3] [Source 4]. Users visiting compromised pages are redirected to a fake Cloudflare “Verify you are human” prompt that tricks them into running malicious PowerShell commands on their local systems, leading to info-stealer infections [Source 2] [Source 4].
The vulnerability affects Ghost CMS versions 3.24.0 through 6.19.0 and was patched in version 6.19.1 [Source 1] [Source 4]. Operators of Ghost instances should immediately upgrade to version 6.19.1 or newer, audit published content for injected scripts, and rotate all Admin API keys [Source 2] [Source 5].
Key Facts
cve: "CVE-2026-26980"
vendor: "Ghost Foundation"
product: "Ghost CMS"
cvss_v3_1: "9.4 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N)"
cwe: "CWE-89"
disclosed_date: "2026-02-19"
earliest_observed_exploitation: "2026-05-15"
affected_versions: "3.24.0 to 6.19.0"
fixed_version: "6.19.1"
affected_configuration:
- "Ghost CMS instance running vulnerable version"
- "Content API is exposed to untrusted requests"
high_value_evidence:
- "package.json"
- "Admin API log showing unauthorized POST requests"
- "Injected script tags in published posts database"
credentials_at_risk:
- "Ghost Admin API keys"
Evidence Assessment
- confirmed: SentinelOne and other security vendors confirm active exploitation of CVE-2026-26980 to extract Admin API keys and modify blog contents [Source 2] [Source 3].
- confirmed: Attackers use stolen keys to execute ClickFix attacks, prompting visitors to execute PowerShell code to deploy infostealer payloads [Source 2] [Source 4].
- confirmed: Ghost Foundation released version 6.19.1 to patch the SQL injection in the Content API slug filter [Source 1] [Source 4].
- unclear: Public reports do not identify all affected domains or the total quantity of compromised Admin API keys currently in circulation.
Impact Determination
| Classification | Criteria | Required evidence | Handling decision |
|---|---|---|---|
| Confirmed compromise | Ghost instance is below 6.19.1, and web server logs, database audits, or system logs show unauthorized Content API SQL injection attempts or injected JavaScript blocks in post content. | Log events showing SQL injection syntax in request paths, database records containing injected <script> tags, or unauthorized Admin API key activity. | Isolate the application instance, rotate all Admin API keys, inspect and restore all published content to a clean state, and perform host-level forensics. |
| Presumed exposed | Ghost CMS instance is verified to be within versions 3.24.0 to 6.19.0 and exposed to the public internet during the May 2026 onward campaign window. | Installed package metadata (package.json, lockfiles) or web server configuration showing public access to the Content API. | Upgrade immediately to 6.19.1 or newer, check all post contents for external scripts, and proactively rotate all Admin API keys. |
| Potentially exposed | Ghost CMS is used in the infrastructure but version, patching state, or key rotation status is unverified. | Software asset list, dependency logs, or incomplete patch reports. | Run immediate vulnerability scans and verify the installed Ghost CMS build version. |
| Not exposed | Ghost CMS is verified to be running version >= 6.19.1, or the platform is not present in the environment. | Verified build files, deployment manifests, or negative asset inventory results. | No immediate remediation action required. |
Timeline
- 2026-02-19: Ghost Foundation patches CVE-2026-26980 in version 6.19.1 [Source 1].
- 2026-05-15: Earliest observed active exploitation chaining the SQL injection with ClickFix malicious scripts [Source 2].
- 2026-06-01: Security vendors report over 700 sites compromised in the campaign, targeting university and corporate blogs [Source 2] [Source 4].
- 2026-06-11: This threat post analysis is published.
Technical Analysis
The vulnerability, CVE-2026-26980, is an unauthenticated SQL injection vulnerability within the Content API of Ghost CMS [Source 1] [Source 2]. The issue occurs due to improper input sanitization in the API’s slug filtering logic. An attacker can supply a crafted database query inside a slug parameter request (e.g. via the /ghost/api/v3/content/posts/ endpoint), allowing them to bypass normal query logic and extract arbitrary database records [Source 2] [Source 3].
Because Ghost stores its administrative credentials and Admin API keys in the database, unauthenticated attackers can extract these keys [Source 2] [Source 5]. With a valid Admin API key, the attacker can make authenticated requests to the Admin API, bypass normal dashboard authentication, and add custom scripts to the site’s head/foot templates or post bodies [Source 2] [Source 3].
In the observed campaign, attackers injected malicious script tags that dynamically load fake verification pages (ClickFix) designed to trick users into running local PowerShell scripts, causing the execution of infostealers [Source 4] [Source 3].
Affected Assets and Blast Radius
affected_assets:
- "Internet-exposed Ghost CMS instances below version 6.19.1"
highest_priority:
- "Production blog instances with public-facing Content APIs"
- "Workstations of administrators who accessed compromised Ghost dashboards"
credentials_and_data_at_risk:
- "Ghost Admin API keys"
- "Ghost database contents (user lists, hashed passwords, drafts)"
- "Downstream reader systems (subject to ClickFix drive-by injection)"
Indicators of Compromise
These indicators are associated with the active ClickFix campaigns exploiting Ghost CMS [Source 2] [Source 3].
domains:
- "clo4shara.xyz"
- "com-apps.cc"
- "cloud-verification.com"
files:
- "UtilifySetup.exe"
- "update.zip"
- "NotepadPlusPlus.zip"
vulnerabilities:
- "CVE-2026-26980"
packages:
- "ghost"
Detection and Hunting
Script: local repository and exported telemetry scope
#!/usr/bin/env python3
import os
import sys
import json
import subprocess
from pathlib import Path
ROOT = sys.argv[1] if len(sys.argv) > 1 else "."
LOG_ROOT = os.environ.get("LOG_ROOT", "")
OUT = Path(os.environ.get("OUT", "hp-ghost-cms-cve-2026-26980-takeover-scope"))
SINCE = "2026-06-11T00:00:00Z"
UNTIL = "2026-06-11T23:59:59Z"
PACKAGES = [
]
VERSIONS = [
]
FILES = [
"UtilifySetup.exe",
"update.zip",
"NotepadPlusPlus.zip",
]
DOMAINS = [
"clo4shara.xyz",
"com-apps.cc",
"cloud-verification.com",
]
URLS = [
]
IPS = [
]
HASHES = [
]
PROCESS_PATTERNS = [
]
NETWORK_PATTERNS = [
]
# Positive signal: repository, lockfile, artifact, process, or network telemetry contains one of the exact incident selectors above.
# Escalation: any match tied to a production build, CI run, deployed asset, or secret-bearing host moves the asset to presumed exposed.
OUT.mkdir(parents=True, exist_ok=True)
indicators_file = OUT / "indicators.txt"
# Collect unique indicators
indicators = set()
for group in [PACKAGES, VERSIONS, FILES, DOMAINS, URLS, IPS, HASHES, PROCESS_PATTERNS, NETWORK_PATTERNS]:
for val in group:
if val:
indicators.add(val)
with open(indicators_file, "w") as f:
for ind in sorted(indicators):
f.write(ind + "\n")
print(f"[+] Written unique selectors to {indicators_file}")
# Walk local directory
print(f"[+] Scanning directory: {ROOT} for selectors...")
matches = []
exclude_dirs = {"node_modules", "vendor", "dist", ".git"}
for root, dirs, filenames in os.walk(ROOT):
dirs[:] = [d for d in dirs if d not in exclude_dirs]
for filename in filenames:
filepath = Path(root) / filename
try:
content = filepath.read_text(errors="ignore")
for ind in indicators:
if ind in content:
matches.append(f"{filepath}: found '{ind}'")
except Exception:
pass
if matches:
(OUT / "repository-indicator-matches.txt").write_text("\n".join(matches) + "\n")
print(f"[!] Found {len(matches)} matches in codebase!")
# Optional Log Scanning
if LOG_ROOT and os.path.exists(LOG_ROOT):
print(f"[+] Scanning telemetry log directory: {LOG_ROOT}...")
log_matches = []
for root, _, filenames in os.walk(LOG_ROOT):
for filename in filenames:
filepath = Path(root) / filename
try:
content = filepath.read_text(errors="ignore")
for ind in indicators:
if ind in content:
log_matches.append(f"{filepath}: found '{ind}'")
except Exception:
pass
if log_matches:
(OUT / "exported-telemetry-indicator-matches.txt").write_text("\n".join(log_matches) + "\n")
print(f"[!] Found {len(log_matches)} matches in logs!")
if PACKAGES:
registry_dir = OUT / "registry"
registry_dir.mkdir(exist_ok=True)
print(f"[+] Wrote scope artifacts under {OUT}")
Downstream Abuse Audits
Script: GitHub organization run, release, secret, and workflow audit
#!/usr/bin/env python3
import os
import sys
import json
import subprocess
from pathlib import Path
if "ORG" not in os.environ:
print("ERROR: Set ORG environment variable to the GitHub organization to audit", file=sys.stderr)
sys.exit(1)
ORG = os.environ["ORG"]
SINCE = "2026-06-11T00:00:00Z"
UNTIL = "2026-06-11T23:59:59Z"
OUT = Path(os.environ.get("OUT", "hp-ghost-cms-cve-2026-26980-takeover-github-audit"))
SELECTORS = [
"UtilifySetup.exe",
"update.zip",
"NotepadPlusPlus.zip",
"clo4shara.xyz",
"com-apps.cc",
"cloud-verification.com",
]
# Positive signal: a workflow run, release, secret, key, package, or workflow change overlaps the exposure window and references an incident selector.
# Remediation trigger: unauthorized post-exposure write activity or a secret-bearing run matching an incident selector requires token revocation and downstream cloud/registry review.
OUT.mkdir(parents=True, exist_ok=True)
(OUT / "runs").mkdir(exist_ok=True)
(OUT / "logs").mkdir(exist_ok=True)
(OUT / "repos").mkdir(exist_ok=True)
# 1. Write incident-selectors file
selectors_file = OUT / "incident-selectors.txt"
with open(selectors_file, "w") as sf:
for s in SELECTORS:
if s:
sf.write(s + "\n")
# 2. Get list of repos
print(f"[+] Fetching repositories for organization: {ORG}")
repo_res = subprocess.run(["gh", "repo", "list", ORG, "--limit", "1000", "--json", "nameWithOwner"], capture_output=True, text=True)
if repo_res.returncode != 0:
print(f"[-] Failed to fetch repos: {repo_res.stderr}", file=sys.stderr)
sys.exit(1)
repos = [r["nameWithOwner"] for r in json.loads(repo_res.stdout)]
for repo in repos:
safe_repo = repo.replace("/", "__")
print(f"[+] Auditing repository: {repo}")
# Check runs in the window
runs_res = subprocess.run([
"gh", "api", f"/repos/{repo}/actions/runs",
"-f", "per_page=100",
"-f", f"created=>={SINCE}",
"--paginate"
], capture_output=True, text=True)
if runs_res.returncode == 0:
try:
all_runs = json.loads(runs_res.stdout).get("workflow_runs", [])
filtered_runs = [r for r in all_runs if r["created_at"] <= UNTIL]
if filtered_runs:
with open(OUT / "runs" / f"{safe_repo}-runs.jsonl", "w") as rf:
for run in filtered_runs:
rf.write(json.dumps(run) + "\n")
# Fetch log dynamically
run_id = str(run["id"])
log_res = subprocess.run(["gh", "run", "view", run_id, "--repo", repo, "--log"], capture_output=True, text=True)
if log_res.returncode == 0:
(OUT / "logs" / f"{safe_repo}-{run_id}.log").write_text(log_res.stdout)
# Fetch details
view_res = subprocess.run(["gh", "run", "view", run_id, "--repo", repo, "--json", "databaseId,workflowName,headSha,event,createdAt,jobs"], capture_output=True, text=True)
if view_res.returncode == 0:
(OUT / "runs" / f"{safe_repo}-{run_id}.json").write_text(view_res.stdout)
except Exception as e:
print(f"[-] Error parsing runs for {repo}: {e}")
# Check releases in window
subprocess.run(["gh", "api", f"/repos/{repo}/releases", "-f", "per_page=100", "--paginate"], capture_output=True)
# Check repo secrets updated in window
subprocess.run(["gh", "api", f"/repos/{repo}/actions/secrets", "-f", "per_page=100", "--paginate"], capture_output=True)
# Check deploy keys
subprocess.run(["gh", "api", f"/repos/{repo}/keys", "-f", "per_page=100", "--paginate"], capture_output=True)
# Scan output directory for any indicator selector matches
print("[+] Scanning gathered telemetry for indicator matches...")
subprocess.run(["rg", "-n", "--hidden", "--fixed-strings", "-f", str(selectors_file), str(OUT)], capture_output=False)
print(f"[+] Wrote GitHub audit artifacts under {OUT}")
Script: cloud OIDC and deployment credential follow-on audit
#!/usr/bin/env python3
import os
import json
import subprocess
from pathlib import Path
SINCE = "2026-06-11T00:00:00Z"
UNTIL = "2026-06-11T23:59:59Z"
OUT = Path(os.environ.get("OUT", "hp-ghost-cms-cve-2026-26980-takeover-cloud-audit"))
AWS_REGIONS = os.environ.get("AWS_REGIONS", "us-east-1").split(",")
# Positive signal: token exchange or privileged write activity occurs in the exposure window from GitHub, CI/CD, package registry, or deployment automation identity.
# Remediation trigger: unexpected write, deploy, IAM, secret, or registry activity tied to an exposed CI/CD path requires trust-policy disablement and credential rotation.
OUT.mkdir(parents=True, exist_ok=True)
# 1. AWS CloudTrail Audit
print("[+] Querying AWS CloudTrail for Web Identity token exchanges...")
aws_events = []
for region in AWS_REGIONS:
res = subprocess.run([
"aws", "cloudtrail", "lookup-events",
"--region", region,
"--start-time", SINCE,
"--end-time", UNTIL,
"--lookup-attributes", "AttributeKey=EventName,AttributeValue=AssumeRoleWithWebIdentity",
"--output", "json"
], capture_output=True, text=True)
if res.returncode == 0:
try:
events = json.loads(res.stdout).get("Events", [])
for event in events:
ct = json.loads(event.get("CloudTrailEvent", "{}"))
ct["region"] = region
aws_events.append(ct)
except Exception as e:
print(f"[-] Error parsing AWS CloudTrail events: {e}")
if aws_events:
with open(OUT / "aws-assume-role-with-web-identity.jsonl", "w") as f:
for ev in aws_events:
f.write(json.dumps(ev) + "\n")
# Audit follow-on events for returned access keys
for ev in aws_events:
access_key = ev.get("responseElements", {}).get("credentials", {}).get("accessKeyId")
region = ev.get("region", "us-east-1")
if access_key:
print(f"[+] Enumerating AWS events for AccessKey: {access_key}")
f_res = subprocess.run([
"aws", "cloudtrail", "lookup-events",
"--region", region,
"--start-time", SINCE,
"--end-time", UNTIL,
"--lookup-attributes", f"AttributeKey=AccessKeyId,AttributeValue={access_key}",
"--output", "json"
], capture_output=True, text=True)
if f_res.returncode == 0:
try:
f_events = json.loads(f_res.stdout).get("Events", [])
with open(OUT / "aws-follow-on-api-calls.jsonl", "a") as ff:
for fe in f_events:
ff.write(fe.get("CloudTrailEvent", "{}") + "\n")
except Exception as e:
print(f"[-] Error writing follow-on events: {e}")
# 2. Azure Activity Log Audit
print("[+] Querying Azure activity logs...")
az_res = subprocess.run([
"az", "monitor", "activity-log", "list",
"--start-time", SINCE,
"--end-time", UNTIL,
"--query", "[?contains(operationName.value, 'write') || contains(operationName.value, 'delete') || contains(operationName.value, 'Microsoft.ManagedIdentity')]",
"-o", "json"
], capture_output=True, text=True)
if az_res.returncode == 0:
(OUT / "azure-write-delete-activity.json").write_text(az_res.stdout)
# 3. GCP Logging Audit
print("[+] Querying GCP Cloud Logging...")
gcp_filter = f'timestamp>="{SINCE}" AND timestamp<="{UNTIL}" AND (protoPayload.methodName="google.sts.v1.SecurityTokenService.ExchangeToken" OR protoPayload.methodName:"GenerateAccessToken" OR protoPayload.methodName:"CreateServiceAccountKey" OR protoPayload.methodName:"SetIamPolicy")'
gcp_res = subprocess.run([
"gcloud", "logging", "read", gcp_filter,
"--format", "json"
], capture_output=True, text=True)
if gcp_res.returncode == 0:
(OUT / "gcp-token-and-iam-activity.json").write_text(gcp_res.stdout)
print(f"[+] Wrote cloud audit artifacts under {OUT}")
Script: registry metadata and artifact audit
#!/usr/bin/env python3
import os
import json
import subprocess
from pathlib import Path
SINCE = "2026-06-11T00:00:00Z"
OUT = Path(os.environ.get("OUT", "hp-ghost-cms-cve-2026-26980-takeover-registry-audit"))
PACKAGES = [
]
VERSIONS = [
]
# Positive signal: workflow files or extensions reference the affected action/extension names or versions.
# Remediation trigger: exposed secrets or OIDC federation policies must be immediately rotated.
OUT.mkdir(parents=True, exist_ok=True)
with open(OUT / "affected-versions.txt", "w") as av:
for version in VERSIONS:
if version:
av.write(version + "\n")
# 1. Search local workspace files for the affected actions/extensions
print("[+] Scanning workspace workflows for selectors...")
for file in Path(".").glob(".github/workflows/**/*.yml"):
subprocess.run(["rg", "-n", "--hidden", "--fixed-strings", "-f", str(OUT / "affected-versions.txt"), str(file)])
# 2. HOW TO ROTATE EXPOSED GITHUB ACTIONS SECRETS:
# Overwrite compromised secrets with newly generated credentials:
# subprocess.run(["gh", "secret", "set", "COMPROMISED_SECRET_NAME", "--body", "my-new-secret-value", "--repo", "my-org/my-repo"])
# For organization-level secrets:
# subprocess.run(["gh", "secret", "set", "COMPROMISED_SECRET_NAME", "--org", "my-org", "--visibility", "private"])
# Revoke compromised OIDC federated trust credentials in AWS/GCP and redeploy the IAM trust policy:
# subprocess.run(["aws", "iam", "update-assume-role-policy", "--role-name", "my-role-name", "--policy-document", "file://new-clean-trust-policy.json"])
print(f"[+] Wrote registry audit artifacts under {OUT}")
Remediation and Closure
- Upgrade: Upgrade all Ghost CMS instances immediately to version 6.19.1 or newer.
- Rotate Keys: Generate new Admin API keys and revoke any existing keys that were active prior to the upgrade.
- Audit Database: Query the database for any injected script tags or raw HTML tags containing untrusted domains (e.g.,
clo4shara.xyzorcom-apps.cc). - Endpoint Triage: If an administrator or user is confirmed to have clicked on a fake Cloudflare captcha page, isolate the workstation immediately and inspect for infostealer execution.
Sources
- GitHub: Ghost CMS CVE-2026-26980 Patch release - Role: DIRECT_SOURCE - Impact: Vulnerability patches and fix version information.
- SentinelOne: Widespread Exploitation of Ghost CMS Vulnerability Leads to ClickFix Attacks - Role: PRIMARY_RESEARCH - Impact: Campaign scope, Admin API key extraction details, and threat behavior.
- NVD: CVE-2026-26980 - Role: ENRICHMENT_DATA - Impact: CWE mapping, CVSS score, and vulnerability descriptions.
- Mallory Security: Massive ClickFix Campaign Targeting Ghost CMS Users - Role: PRIMARY_RESEARCH - Impact: Indicators, ClickFix domain list, and payload file names.
IOC Clipboard
6 IOCsclo4shara.xyz clo4shara[.]xyz com-apps.cc com-apps[.]cc cloud-verification.com cloud-verification[.]com UtilifySetup.exe UtilifySetup.exe update.zip update.zip NotepadPlusPlus.zip NotepadPlusPlus.zip