Source code for pysap.SAPNI

# encoding: utf-8
# pysap - Python library for crafting SAP's network protocols packets
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# Author:
#   Martin Gallo (@martingalloar)
#   Code contributed by SecureAuth to the OWASP CBAS project
#

# Standard imports
import sys
import logging
from select import select
from struct import unpack
from threading import Event
from SocketServer import BaseRequestHandler, ThreadingMixIn, TCPServer
# External imports
from scapy.fields import LenField
from scapy.packet import Packet, Raw
from scapy.supersocket import socket, StreamSocket
# Custom imports
from pysap.utils import Worker


# Create a logger for the SAPNI layer
log_sapni = logging.getLogger("pysap.sapni")


[docs]class SAPNI(Packet): """SAP NI (Network Interface) packet This packet is used for craft Network Interface packets. It serves only as a container for packets in the different protocols. As this protocol is used by different protocols and the only way to differentiate each one is by the TCP port used, each script using the NI protocol must bind the respective layer with the respective protocol. For example, a script using the SAP Diag protocol must include the following binds:: bind_layers(SAPNI, SAPDiag, ) bind_layers(SAPNI, SAPDiagDP, ) bind_layers(SAPDiagDP, SAPDiag, ) bind_layers(SAPDiag, SAPDiagItem, ) bind_layers(SAPDiagItem,SAPDiagItem, ) """ name = "SAP NI (Network Interface) protocol" fields_desc = [LenField("length", None, fmt="!I")] # Constants for keep-alive messages SAPNI_PING = "NI_PING\x00" """ :cvar: Constant for keep-alive request messages (NI_PING) :type: C{string} """ SAPNI_PONG = "NI_PONG\x00" """ :cvar: Constant for keep-alive response messages (NI_PONG) :type: C{string} """
[docs]class SAPNIStreamSocket(StreamSocket): """Stream socket implementation of the SAP Network Interface (NI) layer. """ desc = "NI Stream socket" def __init__(self, sock, keep_alive=True, base_cls=None): """Initializes the NI stream socket. :param sock: socket to wrap :type sock: C{socket} :param keep_alive: if true, the socket will automatically respond to keep-alive request messages. Otherwise, the keep-alive messages are passed to the caller in :class:`recv` and :class:`sr` calls. :type keep_alive: ``bool`` :param base_cls: the base class to use when receiving packets, it uses :class:`SAPNI` as default if no class specified :type base_cls: :class:`Packet` class """ StreamSocket.__init__(self, sock, Raw) self.keep_alive = keep_alive self.basecls = base_cls
[docs] def send(self, packet): """Send a packet at the NI layer, prepending the length field. :param packet: packet to send :type packet: Packet """ # Add the NI layer and send log_sapni.debug("To send %d bytes data + 4 bytes NI header", len(packet)) return StreamSocket.send(self, SAPNI() / packet)
[docs] def recv(self): """Receive a packet at the NI layer, first reading the length field and the reading the data. If the stream is waiting for a new packet and the remote peer sends a keep-alive request (:class:`NI_PING<SAPNI.SAPNI_PING>`), the receive method will respond with a keep-alive response (:class:`NI_PONG<SAPNI.SAPNI_PONG>`) to keep the communication stable. :return: received :class:`SAPNI` packet :rtype: :class:`SAPNI` :raise socket.error: if the connection was close """ # Receive the NI length field nidata = self.ins.recv(4, socket.MSG_PEEK) if len(nidata) == 0: raise socket.error((100, "Underlying stream socket tore down")) (nilength, ) = unpack("!I", nidata) log_sapni.debug("Received 4 bytes NI header, to receive %d bytes data", nilength) # Receive the whole NI packet (length+payload) nidata = '' while len(nidata) < nilength + 4: nidata += self.ins.recv(nilength - len(nidata) + 4) if len(nidata) == 0: raise socket.error((100, "Underlying stream socket tore down")) # If the packet received is a keep-alive request (NI_PING), send a # response (NI_PONG) and make a new receive call if nilength == len(SAPNI.SAPNI_PING) and nidata[4:] == SAPNI.SAPNI_PING: log_sapni.debug("Received NI_PING") if self.keep_alive: log_sapni.debug("Keep alive set, sending NI_PONG") self.send(Raw(SAPNI.SAPNI_PONG)) return self.recv() # Build the SAPNI packet with the received data log_sapni.debug("Received %d bytes data", nilength) # Decode the packet payload according to the base class defined packet = SAPNI(nidata) if self.basecls: packet.decode_payload_as(self.basecls) return packet
[docs] def sr(self, packet): """Send a given packet and receive the response. Wrapper around the send and receive methods. The response packet is build in the :class:`SAPNI` layer. :param packet: packet to send :type packet: Packet :return: packet received :rtype: Packet """ self.send(packet) return self.recv()
[docs] @classmethod def get_nisocket(cls, host, port, **kwargs): """Helper function to obtain a :class:`SAPNIStreamSocket`. :param host: host to connect to :type host: C{string} :param port: port to connect to :type port: ``int`` :keyword kwargs: arguments to pass to :class:`SAPNIStreamSocket` constructor :return: connected socket :rtype: :class:`SAPNIStreamSocket` :raise socket.error: if the connection to the target host/port failed """ sock = socket.create_connection((host, port)) return cls(sock, **kwargs)
[docs]class SAPNIProxy(object): """SAP NI Proxy It works by setting a listener :class:`SAPNIStreamSocket` and dispatching client's requests to a given handler class. Example usage:: proxy = SAPNIProxy(local_host, local_port, remote_host, remote_port, handler_class) proxy.handle_connection() """ def __init__(self, bind_address, bind_port, remote_address, remote_port, handler, backlog=5, keep_alive=True, options=None): """Create the proxy binding a socket in the giving port and setting the handler for the incoming connections. :param bind_address: address to bind the listener socket :type bind_address: C{string} :param bind_port: port to bind the listener socket :type bind_port: ``int`` :param remote_address: remote address to connect to :param remote_address: C{string} :param remote_port: remote port to connect to :type remote_port: ``int`` :param handler: handler class :type handler: :class:`SAPNIProxyHandler` class :param backlog: backlog parameter to set in the listener socket :type backlog: ``int`` :param keep_alive: if true, the proxy will handle the keep-alive requests and responses. Otherwise, keep-alive messages are passed to the handler as regular packets. :type keep_alive: ``bool`` :param options: options to pass to the handler instance :type options: ``dict`` """ self.remote_host = (remote_address, remote_port) self.handler = handler self.keep_alive = keep_alive self.options = options # Create and bind the listener socket sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind((bind_address, bind_port)) sock.listen(backlog) # Create the NI Stream Socket self.listener = SAPNIStreamSocket(sock, keep_alive) log_sapni.debug("SAPNIProxy: Binded to address %s:%d, proxying to %s:%d", bind_address, bind_port, remote_address, remote_port)
[docs] def handle_connection(self): """Block until a connection is received from the listener and handle that client using the provided handler class. :return: the handler instance handling the request :rtype: :class:`SAPNIProxyHandler` """ # Accept a client connection (client, address) = self.listener.ins.accept() # Creates a remote socket remote = socket.socket(socket.AF_INET, socket.SOCK_STREAM) remote.connect(self.remote_host) # Create the NI Stream Socket and handle it proxy = self.handler(SAPNIStreamSocket(client, self.keep_alive), SAPNIStreamSocket(remote, self.keep_alive), self.options) log_sapni.debug("SAPNIProxy: Handled a connection from %s", address) return proxy
[docs] def stop(self): """Stop the proxy by closing the listener socket.""" self.listener.close()
[docs]class SAPNIProxyHandler(object): """SAP NI Proxy Handler Handles NI packets. Works spawning one thread for processing data coming from each pair of client/server. """ def __init__(self, client, server, options=None): """It receives two :class:`SAPNIStreamSocket`s objects and creates the worker for processing data. Thread is started as daemon. :param client: client Stream Socket :type client: :class:`SAPNIStreamSocket` :param server: server Stream Socket :type server: :class:`SAPNIStreamSocket` :param options: options received from the proxy :type options: ``dict`` """ self.client = client self.server = server self.poll_interval = 0.5 self.processor = Worker(self, self._handle) self.processor.daemon = True self.processor.start()
[docs] def recv_send(self, local, remote, process): """Receives data from one socket connection, process it and send to the remote connection. :param local: the local socket :type local: :class:`SAPNIStreamSocket` :param remote: the remote socket :type remote: :class:`SAPNIStreamSocket` :param process: the function that process the incoming data :type process: function """ # Receive a SAP NI packet packet = local.recv() log_sapni.debug("SAPNIProxyHandler: Received %d bytes", len(packet)) # Process the packet using the given function packet = process(packet) # Send the packet to the remote peer remote.send(packet.payload) log_sapni.debug("SAPNIProxyHandler: Sent %d bytes", len(packet))
def _handle(self): """Handles data coming from either the client or the server""" r, __, __ = select([self.client, self.server], [], [], self.poll_interval) if self.client in r: try: log_sapni.debug("SAPNIProxyHandler: Client --> Server connection") self.recv_send(self.client, self.server, self.process_client) except socket.error: log_sapni.error("SAPNIProxyHandler: Client connection down") self.stop_workers() if self.server in r: try: log_sapni.debug("SAPNIProxyHandler: Client <-- Server connection") self.recv_send(self.server, self.client, self.process_server) except socket.error: log_sapni.error("SAPNIProxyHandler: Server connection down") self.stop_workers()
[docs] def process_client(self, packet): """This method is called each time a packet arrives from the client. It must return a packet in the same layer (:class:`SAPNI`). Stub method to be overloaded in subclasses. :param packet: the packet to be processed :type packet: Packet """ return packet
[docs] def process_server(self, packet): """This method is called each time a packet arrives from the server. It must return a packet in the same layer (:class:`SAPNI`). Stub method to be overloaded in subclasses. :param packet: the packet to be processed :type packet: Packet """ return packet
[docs] def stop_workers(self): """Stop the processor workers""" self.client.close() self.server.close() self.processor.stop()
[docs]class SAPNIClient(object): """Stub class for a client connecting to the SAP NI server. """
[docs]class SAPNIServer(TCPServer): """Base SAP NI Server class. Subclasses must define a client class for keeping state information on the connected clients. Example usage:: server = SAPNIServer((local_host, local_port), handler_class) server.client_cls = client_class server.serve_forever() """ clients_cls = SAPNIClient """ :cvar: Client class for storing data about new clients :type: :class:`SAPNIClient` class """ options = None """ :ivar: Options to pass to the request handler :type: ``object`` """ def __init__(self, server_address, RequestHandlerClass, bind_and_activate=True, socket_cls=None, keep_alive=True, base_cls=None): """ """ self.socket_cls = socket_cls or SAPNIStreamSocket self.keep_alive = keep_alive self.base_cls = base_cls self.clients = {} TCPServer.__init__(self, server_address, RequestHandlerClass, bind_and_activate=bind_and_activate)
[docs] def handle_error(self, request, client_address): """Called to handle an error or exception occurred with the server. """ log_sapni.exception("SAPNIServer: Client connection error: %s", sys.exc_info()[1])
[docs] def get_request(self): """Wrap the socket object with a :class:`SAPNIStreamSocket` after accepting a connection. """ socket, addr = self.socket.accept() socket = self.socket_cls(socket, keep_alive=self.keep_alive, base_cls=self.base_cls) return socket, addr
[docs] def shutdown_request(self, request): """Called to shutdown and close an individual request.""" try: request.ins.shutdown(socket.SHUT_WR) except socket.error: pass self.close_request(request)
[docs]class SAPNIServerThreaded(ThreadingMixIn, SAPNIServer): """A SAP NI Server implementation using threading """
[docs]class SAPNIServerHandler(BaseRequestHandler): """SAP NI Server Handler Handles :class:`SAPNI` packets coming from a :class:`SAPNIServer`. """
[docs] def setup(self): """Setup a new client connection. Creates a new client object for keeping state information of each client on the server instance. """ if self.client_address not in list(self.server.clients.keys()): self.server.clients[self.client_address] = self.server.clients_cls() log_sapni.debug("SAPNIServerHandler: New client %s", self.client_address) self.closed = Event()
[docs] def close(self): """Close a client connection and deletes the client from the state information on the server. """ if self.client_address in self.server.clients: del(self.server.clients[self.client_address]) self.closed.set() log_sapni.debug("SAPNIServerHandler: Bye client %s", self.client_address)
[docs] def handle(self): """Handle a client connection. The handler assumes the client connection is a :class:`SAPNIStreamSocket` object. After received a :class:`SAPNI` packet, it stores it on the 'packet' instance variable and pass the control to the handle_data method. """ while not self.closed.is_set(): log_sapni.debug("SAPNIServerHandler: Handling data from %s", self.client_address) # Receive and store the packet try: self.packet = self.request.recv() log_sapni.debug("SAPNIServerHandler: Request received") # Pass the control to the handle_data function self.handle_data() except socket.error as e: log_sapni.debug("SAPNIServerHandler: Error handling data or client %s disconnected, %s (errno %d)", self.client_address, e.message, e.errno) break
[docs] def handle_data(self): """Handle the data coming from the client. The :class:`SAPNI` packet is stored on data and client information on client_address instance variables. Stub method to be overloaded in subclasses. """ pass