Description
A git hosting service. Log in and find the flag hidden in a private repository you have no direct access to.
Setup
Open the challenge URL and create an account.
# Open http://mercury.picoctf.net:<PORT_FROM_INSTANCE>/ in your browserSolution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1
Understand the authorization modelObservationI noticed the challenge description said the flag was in a private repository with no direct access, which suggested the key was understanding how the server decides who is authorized, and whether any privileged code path could be reached from outside.After registering and exploring the Bithug UI, check the source or experiment with the API. The server grants admin privileges to any HTTP request that arrives from a loopback address (127.0.0.1 or ::1). That admin status bypasses every repository authorization check, so an admin can push to any repo, including the private /_/<username>.git that was created for you at signup and holds the flag. The challenge is therefore to make the server issue an HTTP request to itself from 127.0.0.1 - a classic Server-Side Request Forgery (SSRF) setup.Learn more
Why localhost == admin? The app sits behind a reverse proxy configured to pass the real client IP. Spoofing the X-Forwarded-For header is blocked by the proxy. The only way to produce a request genuinely originating from 127.0.0.1 is to make the server itself send that request - which is exactly what a webhook does.
Access control via refs/meta/config. Access to each repository is gated by an
access.conffile stored in the git refrefs/meta/config. Only users listed there (or admins) can clone the repo. The exploit goal is to push a newaccess.confthat adds your username, and that push must arrive from localhost so the server accepts it as admin.For the broader SSRF technique, confirming the sink and sweeping internal ranges, see the SSRF for CTF post.
Step 2
Identify the SSRF vector: webhooks with template-injected URLsObservationI noticed Bithug offered a webhook feature that POSTs to a user-supplied URL after each push, which suggested the server itself would originate that outbound request and thus appear to any destination as coming from 127.0.0.1, creating a classic SSRF entry point.Bithug supports repository webhooks: after a push, the server sends a POST request to the configured URL. Because the POST originates from the server process, the destination server sees the source IP as 127.0.0.1. This is the SSRF primitive. However, the webhook validation rejects URLs pointing to localhost or the loopback range, so a direct http://localhost:1823/... webhook is refused. The bypass exploits how the server processes the webhook URL through a Go-style formatString template before making the request: template variables like {{property}} are replaced with values from a data object, and any unknown variable is replaced with an empty string. By crafting a URL where the host component is a template variable, that component is stripped at execution time even though the URL looked valid during the denylist check.Learn more
Template injection trick. The webhook URL is run through
formatStringafter it passes validation. The function replaces{{key}}with the corresponding value from a data object, and replaces unknown keys with an empty string. The URLhttp://{{/}}localhost:1823/_/user.git/git-receive-packpasses validation because the host field after URL parsing is{{/(not a recognized loopback address). WhenformatStringruns,{{/}}is unknown and becomes an empty string, yieldinghttp://localhost:1823/_/user.git/git-receive-pack.Alternative: 307 redirect server. Some solvers instead pointed the webhook at an external server they controlled that immediately returned a 307 Temporary Redirect to the localhost target. The HTTP client follows the redirect, and the resulting request originates from the server (127.0.0.1). Both approaches achieve the same SSRF; the template injection path requires no external infrastructure.
Step 3
Craft a git packfile that adds you to access.confObservationI noticed the access control for each repository was stored in access.conf under the special ref refs/meta/config, which suggested that forging a git-receive-pack payload pushing a new access.conf to that ref, delivered via the SSRF webhook as admin, would grant me clone rights.A git push over HTTP is a POST to /repo.git/git-receive-pack with a specific binary body (a git packfile). You need to craft one that pushes a new commit to refs/meta/config, where that commit contains a single file called access.conf listing your username. The easiest approach is to perform a real git push against a repo you own while capturing the network traffic (e.g. with Wireshark or a local netcat listener), then replay the captured packfile bytes as the webhook body. Bithug accepts the body base64-encoded, so base64-encode the raw bytes before setting the webhook body field.bash# Set up a bare local repo to generate the packfilebashmkdir flag-access && cd flag-access && git initbashgit checkout --orphan meta-configbashecho 'your_username' > access.confbashgit add access.conf && git commit -m 'grant access'bash# Push to a local netcat listener to capture the packfilebashnc -lvp 9999 > capture.bin &bashgit push http://localhost:9999/fake.git HEAD:refs/meta/configbash# base64-encode the captured POST body (skip the HTTP headers, keep the binary body)bashbase64 -w0 packfile.bin > packfile.b64Expected output
picoCTF{good_job_at_gitting_good}What didn't work first
Tried: Use git fast-export or git bundle instead of capturing the raw HTTP packfile body.
git fast-export produces a stream-format dump meant for git fast-import, and git bundle wraps objects in a bundle header - neither is the raw pkt-line + PACK binary that git-receive-pack expects as an HTTP POST body. Replaying either will result in a malformed request that Bithug rejects with a 400 or silently ignores. You need the exact bytes that git's HTTP transport sends, captured from a real push.
Tried: Push to refs/heads/main instead of refs/meta/config when generating the capture.
The access control file Bithug reads is stored in the special ref refs/meta/config, not in any normal branch. Pushing to refs/heads/main updates the working history of the repo but leaves access.conf untouched. The server will still deny your clone attempt because the access.conf under refs/meta/config has not changed. The captured packfile must reference refs/meta/config as the destination ref.
Learn more
What is a git packfile? When you run
git push, the client sends a binary stream: first a pkt-line listing the ref updates (old-sha new-sha refname), then a PACK file containing the objects needed to reconstruct those commits. This exact byte stream is what you need to replay as the webhook body so that the Bithug server processes it as a legitimate git push from an admin.refs/meta/config is a git ref that exists outside the normal branch namespace. Bithug uses it to store access configuration separate from the working history of the repository, similar to how Gerrit Code Review manages project config.
Step 4
Register the webhook and trigger itObservationI noticed the template variable trick allowed a localhost URL to pass the webhook denylist check, and the packfile was ready, so the next step was to wire them together by registering the webhook on a repo I controlled and triggering it with a push.Go to the settings of a repository you own and add a webhook. Set the URL to the template-injected localhost address that targets your private flag repository. Set the body to the base64-encoded packfile from the previous step and the content type to application/x-git-receive-pack-request. Once the webhook is saved, push any commit to that repository. The push triggers the webhook, the server's HTTP client strips the template variable from the URL and sends the POST to localhost:1823/_/<username>.git/git-receive-pack, the request arrives as 127.0.0.1, the server grants it admin status, and the packfile is accepted - updating refs/meta/config with your access.conf.bash# Template-injected webhook URL (replace <PORT> and <USERNAME>)bashhttp://{{/}}localhost:<PORT_FROM_INSTANCE>/_/<USERNAME>.git/git-receive-packbashbash# Trigger by pushing anything to the repo that owns the webhookbashecho trigger >> README.mdbashgit add README.md && git commit -m 'trigger webhook'bashgit pushWhat didn't work first
Tried: Set the webhook URL directly to http://localhost:PORT/_/username.git/git-receive-pack without the template variable trick.
Bithug validates the webhook URL before saving it and rejects any host that resolves to the loopback range (127.0.0.1, ::1, localhost). The server returns an error such as 'invalid webhook target' and the webhook is never registered. The template variable trick is required because the URL parser sees the host field as the literal string '{{/' during validation, which does not match the denylist - only after saving does formatString collapse it to an empty string, producing the localhost URL at execution time.
Tried: Add the X-Forwarded-For: 127.0.0.1 header to a direct clone request instead of using the webhook SSRF.
Bithug sits behind a reverse proxy that strips or overwrites X-Forwarded-For before the request reaches the application. An externally supplied X-Forwarded-For header is ignored, so the app sees your real public IP and denies admin privileges. Admin status can only be obtained by making the server itself originate the connection over the loopback interface, which is what the webhook POST achieves.
Learn more
Why does this work at the wire level? The webhook HTTP client on the server makes a POST to
http://localhost:1823/_/username.git/git-receive-pack. The TCP connection originates from the server process itself, so the loopback stack sets the source IP to127.0.0.1. The Bithug request handler checks the source IP, sees a loopback address, and promotes the request to admin. The packfile body is then processed with full admin privileges, bypassing the normal access.conf check that would reject a regular user.Step 5
Clone the flag repository and read the flagObservationI noticed the webhook push had updated refs/meta/config with my username in access.conf, which meant my credentials should now satisfy the authorization check and allow a normal git clone of the private /_/<username>.git repository.With your username now listed in the access.conf of /_/<username>.git, you can clone that repository using your normal credentials. The README.md inside contains the flag.bashgit clone http://<YOUR_USER>:<YOUR_PASS>@mercury.picoctf.net:<PORT_FROM_INSTANCE>/_/<YOUR_USER>.gitbashcat <YOUR_USER>/README.mdThe README.md contains the flag:
picoCTF{good_job_at_gitting_good}.
Interactive tools
- Regex TesterTest regular expressions against a string with live match highlighting, flag toggles, and common CTF pattern shortcuts.
Flag
Reveal flag
picoCTF{good_job_at_gitting_good}
Bithug chains SSRF with git packfile injection: a webhook URL crafted with a Go template variable that is stripped at execution time bypasses the localhost denylist, allowing the server to POST a git-receive-pack payload to itself as admin, which updates refs/meta/config to grant access to the private flag repository.