Project 2: Hack into a server
Overview
The objective of this project is to compromise an Ubuntu Linux account running a
vulnerable Web server process. You can access a full copy of the server program C source code in
the course web or in your git repo. You should download the source, compile it, and analyze the
program's behavior.
You will use a shellcode to compromise the server, which is described in
the links below. Once you have compromised your server, you should modify the
files hosted by the server as proof. As you start trying to compromise your
server, it will likely seg fault. However, servers will restart after they crash. In
particular, you will need to use a reverse shellcode so that you can remotely
send commands to the server you take over.
There are five stages to completing the project
1. Find the stack-smash vulnerability in the server code.
2. Run a local copy of the server and use gdb to identify where the relevant variables are stored in
memory.
3. Construct a request string that will exploit the vulnerability by
overwriting variables stored on the stack. Choose a shellcode and incorporate it into
the request string. Suggested shellcode for P2
4. Mount an attack to "pwn" your local webserver process.
Make sure that you can reliably exploit a server process running on your local
machine before trying to exploit your remote server.
5. Pwn your assigned remote webserver process and create a new file or modify
index.html. This may take many attempts since you
cannot be exactly sure where the remote server's stack is located. Most
groups write scripts to automate and vary repeated attacks until they succeed.
To submit your project, push the following to your git repo: all files
(including source/scripting) used in your exploit, and a text file called README.txt with a short explanation of your method of
attack and any experience that might be useful or interesting.
Building and running your local server
You can run a local copy of your server on any sufficiently safe Linux system. You want it to run as a 32-bit process, even if it is a 64-bit system. Just build the program with the -m32 compiler option. See the example and other notes below. That produces a 32-bit executable program file. A 64-bit Linux system runs a 32-bit program as a 32-bit process with a 32-bit virtual address space.
If you are running your own Linux system under Docker or a VM, you might need to install some packages to enable 32-bit compilation. On Ubuntu/Debian:
apt-get install libc6-dev-i386
Safe stacks: if you choose to smash, smash responsibly. This activity is fun and healthy but it can be dangerous. Practice good hygiene: please do not leave these processes running when they are not in active use. Use protection: run your processes in a contained environment (container or VM). Don't use public machines: if someone else hacks one of your processes (easy by design) they will control your user account. OIT Security and CS Staff are aware of 310test, but they should not be finding vulnerable processes lying around the network. Have fun, but stay safe!
Lower your shields
Make sure that your local web server process has its shields down before you try to attack it.
1. Disable Address Space Layout Randomization (ASLR) for your server process. You can do this by
first starting a shell with the command setarch `uname -m` -R
/bin/bash. Then run your server under that shell. The setarch command sets a process attribute that is inherited by all child processes. The -R option sets an attribute to disable ASLR.
2. Disable No-Execute (NX) and "stack canary" defenses for your server process.
To do this, compile using the proper flags: -z execstack -fno-stack-protector.
For example, to compile webserver.c to run as an
unshielded 32-bit process:
gcc -m32 -z execstack -fno-stack-protector webserver.c -o
webserver
Note: if you run your local server in a Docker Desktop container, you need to run that container with some special options to allow you to disable ASLR. See the notes on using Docker Desktop for the labs.
Connecting to the server
You can connect to your server (local or remote) using a web browser. It returns a short default web page (index.html) announcing itself. However, this web server is very simple, and it is unstable under attack, and browsers may expect too much from it. Some browsers behave erratically with this lab.
It is easier to use the "netcat" utility (i.e., the nc command) to send messages to your server. Netcat connects to the server and sends its stdin to the server over the socket, and outputs whatever it receives from the socket to its stdout.
For
example, to request the file index.html from your server using HTTP, you could
type in echo -e "GET /index.html HTTP" | nc
YOUR_SERVER YOUR_PORT, where YOUR_SERVER is replaced with your server's hostname and YOUR_PORT
is replaced with your server's port.
You can also run a bare netcat and type directly into its stdin. This might be useful when your attack is a portbind shell and you want to connect to the attack port and type shell commands. The verbose option nc -v is useful to see connection status. For example, type nc -v YOUR_SERVER YOUR_PORT, hit return, and if you get a successful connection type: GET /index.html HTTP. You should receive the HTML source for the default web page. If the webserver returns an HTTP 404 error (not found), make sure you are running the webserver with the correct current directory. See the source code for other reasons it might return a 404.
If the connection fails, it may be because your network provider disables connections to your server from outside of the local host. For example, OIT appears to block incoming network connections to unauthorized ports on its VM service. But that's OK: you can attack your local webserver locally (as localhost) and nobody will know.
localhost. You can connect to your local server from a netcat or other process running on the same machine, e.g., under a different shell. Use the pseudo-hostname localhost in place of YOUR_SERVER in the commands above.
Where's the stack?
Your remote server runs on classic 32-bit Linux, where stack addresses start at 0xc0000000. In classic 32-bit Linux, the top (highest) gigabyte of the 32-bit address space is reserved for the kernel. The process stack starts just below that (0xc0000000) and grows down (toward lower addresses).
But the stack might show up at those reserved addresses when you run your local server as a 32-bit process on a 64-bit system. In that scenario the (64-bit) kernel is not mapped into the (32-bit) process virtual address space. The process 32-bit process therefore has the whole 4GB for its own use, and stack addresses are chosen accordingly. You can force the classic 3GB layout by adding the -3 option to setarch command above. See this stackoverflow post.
Note that stack addresses might vary based on the size of the process environment variables, and running under gdb can also move the stack lower to make room for additional data at the base.
See this
stackoverflow post with more info and a way to make sure that the
execution environment is the same with and without debugging.
Note: gdb outputs assembly in the AT&T assembly syntax, which means mov %eax, %ebx
will copy the value of eax into ebx. Intel
syntax switches the operands.
Launching your attack
Your objective is to send the webserver a request string with embedded shellcode instructions that causes the webserver process to execute your shellcode. The shellcode takes over the process and lets the attacker use the process to attack the system.
"Shell storm" is a great place to
find shell codes. Suggested shellcode for P2.
The suggested shellcode will "portbind a shell in port 5074". That means that the shellcode does a bind() system call for port 5074, does an accept() syscall to accept an incoming connection, dups the connection socket to its stdin and stdout, and then execs a shell program from a standard file path on the victim system.
So: after the webserver is hacked, the attacker can connect to the bind port (e.g., with nc -v) and input the attacker's chosen shell commands. The hacker lingo for this concept is bindshell. Be sure you understand how those syscalls work to construct the attack.
But: please do not use the default port 5074 as your bind port! You must modify the shell code to use a different port. Instead, please use a standard base + your group number as your bind port. For the base, choose one of 10000, 11000, 12000, and so on. Stay below 32K.
Why change the port? Because your attack fails if you pick a port that is already bound by some other process. You should understand why. The bind() syscall in the shellcode fails with "port is already in use" (EADDRINUSE). But the shellcode does not check the error return. It keeps going and tries to accept() on the socket, which also fails (because the socket is unbound), and so on. Eventually the process either generates an unrecoverable exception or succeeds in exec'ing a shell whose stdin and stdout are duped to an unconnected socket. Either way the pwned process exits and the attack fails.
You must modify the shellcode to embed your chosen port number. Convert it to hex and put it in the right place. For example, 5074 is 0x13d2, the number that appears as a constant in the suggested shellcode. It is not hard to find.
Notes and hints
- If the webserver returns an HTTP 404 (not found), then your attack did not succeed in smashing the stack: either the string was not long enough to smash the RA, or the server rejected it before copying. A smashed server never returns: it has lost its way (smashed RA) and cannot get back to the code that sends a response.
- If you get a 404, don't worry too much about the cause: just figure out how to smash it. If you smash it, the attack still works even if the server "meant to" return a 404.
- If a connect attempt fails with "connection refused", it means the server kernel rejected the attempt because no local process is listening on the requested port.
- If you get a "connection refused" on your remote webserver port, it means your webserver crashed. It should restart in 30-60 seconds.
- If your webserver "hangs" after you attack it, that is the desired behavior. It is likely blocked in accept() on your bind port. Connect to your bind port.
- If you get a "connection refused" on your expected bind (attack) port, it means the attack failed or your bind port number was not installed correctly in the shellcode, and your hacked server is listening on a different port. You can use the lsof command to find out what ports a local process has open.
- If a connect to your bind/attack port succeeds, but the process crashes immediately, it is likely because the end of your shellcode is too close to the top of the stack, and the shellcode's push instructions overwrote itself.
- If the attack crashes the webserver, it might be because the attack string was too long and ran off the base of the stack, or your smashing RA pointed off into the darkness: you missed your NOP sled.
- Don't forget to byteswap your RA: send the least significant byte first in the attack string.
- If your attack fails with 404 and the webserver prints "unsupported command", it may be because you forgot the / before the filename or there is a null character somewhere in the shellcode.
Resources
Please post to Piazza with any difficulties you are having.
The instructor and TA can post tips that may point you and others in the right
direction.
Read the document
"Smashing
the Stack for Fun and Profit" for an introduction to call stack
vulnerabilities.
Look at
"Shellcoding for
Linux and Windows Tutorial" for shellcode samples for Linux. Past students
have found this to be extremely useful.