modules.machinery.physical

Package modules :: Package machinery :: Module physical |
[hide private]
[frames] | no frames]

# Source Code for Module modules.machinery.physical

      1  # Copyright (C) 2012-2014 The MITRE Corporation.
      2  # This file is part of Cuckoo Sandbox - http://www.cuckoosandbox.org
      3  # See the file 'docs/LICENSE' for copying permission.
      4
      5  import socket
      6  import logging
      7  import xmlrpclib
      8  import subprocess
      9
     10  log = logging.getLogger(__name__)
     11
     12  try:
     13      import bs4
     14      import requests
     15      import wakeonlan.wol
     16      HAVE_FOG = True
     17  except ImportError:
     18      HAVE_FOG = False
     19      log.critical(
     20          "The bs4, requests, and wakeonlan Python libraries are required for "
     21          "proper FOG integration with Cuckoo (please install them through "
     22          "`pip install bs4 requests wakeonlan`)."
     23      )
     24
     25  from lib.cuckoo.common.abstracts import Machinery
     26  from lib.cuckoo.common.constants import CUCKOO_GUEST_PORT
     27  from lib.cuckoo.common.exceptions import CuckooCriticalError
     28  from lib.cuckoo.common.exceptions import CuckooMachineError
     29  from lib.cuckoo.common.utils import TimeoutServer
     30
     31 -class Physical(Machinery):
     32      """Manage physical sandboxes."""
     33
     34      # Physical machine states.
     35      RUNNING = "running"
     36      STOPPED = "stopped"
     37      ERROR = "error"
     38
     39 -    def _initialize_check(self):
     40          """Ensures that credentials have been entered into the config file.
     41          @raise CuckooCriticalError: if no credentials were provided or if
     42              one or more physical machines are offline.
     43          """
     44          # TODO This should be moved to a per-machine thing.
     45          if not self.options.physical.user or not self.options.physical.password:
     46              raise CuckooCriticalError(
     47                  "Physical machine credentials are missing, please add it to "
     48                  "the Physical machinery configuration file."
     49              )
     50
     51          self.fog_init()
     52
     53          for machine in self.machines():
     54              status = self._status(machine.label)
     55              if status == self.STOPPED:
     56                  # Send a Wake On Lan message (if we're using FOG).
     57                  self.wake_on_lan(machine.label)
     58              elif status == self.ERROR:
     59                  raise CuckooMachineError(
     60                      "Unknown error occurred trying to obtain the status of "
     61                      "physical machine %s. Please turn it on and check the "
     62                      "Cuckoo Agent." % machine.label
     63                  )
     64
     65 -    def _get_machine(self, label):
     66          """Retrieve all machine info given a machine's name.
     67          @param label: machine name.
     68          @return: machine dictionary (id, ip, platform, ...).
     69          @raises CuckooMachineError: if no machine is available with the given label.
     70          """
     71          for m in self.machines():
     72              if label == m.label:
     73                  return m
     74
     75          raise CuckooMachineError("No machine with label: %s." % label)
     76
     77 -    def start(self, label, task):
     78          """Start a physical machine.
     79          @param label: physical machine name.
     80          @param task: task object.
     81          @raise CuckooMachineError: if unable to start.
     82          """
     83          # Check to ensure a given machine is running
     84          log.debug("Checking if machine %r is running.", label)
     85          status = self._status(label)
     86          if status == self.RUNNING:
     87              log.debug("Machine already running: %s.", label)
     88          elif status == self.STOPPED:
     89              self._wait_status(label, self.RUNNING)
     90          else:
     91              raise CuckooMachineError("Error occurred while starting: "
     92                                       "%s (STATUS=%s)" % (label, status))
     93
     94 -    def stop(self, label):
     95          """Stops a physical machine.
     96          @param label: physical machine name.
     97          @raise CuckooMachineError: if unable to stop.
     98          """
     99          # Since we are 'stopping' a physical machine, it must
    100          # actually be rebooted to kick off the re-imaging process.
    101          creds = "%s%%%s" % (
    102              self.options.physical.user, self.options.physical.password
    103          )
    104
    105          if self._status(label) == self.RUNNING:
    106              log.debug("Rebooting machine: %s.", label)
    107              machine = self._get_machine(label)
    108
    109              args = [
    110                  "net", "rpc", "shutdown", "-I", machine.ip,
    111                  "-U", creds, "-r", "-f", "--timeout=5"
    112              ]
    113              output = subprocess.check_output(args)
    114
    115              if "Shutdown of remote machine succeeded" not in output:
    116                  raise CuckooMachineError("Unable to initiate RPC request")
    117              else:
    118                  log.debug("Reboot success: %s." % label)
    119
    120              # Deploy a clean image through FOG, assuming we're using FOG.
    121              self.fog_queue_task(label)
    122
    123 -    def _list(self):
    124          """Lists physical machines installed.
    125          @return: physical machine names list.
    126          """
    127          active_machines = []
    128          for machine in self.machines():
    129              if self._status(machine.label) == self.RUNNING:
    130                  active_machines.append(machine.label)
    131
    132          return active_machines
    133
    134 -    def _status(self, label):
    135          """Gets current status of a physical machine.
    136          @param label: physical machine name.
    137          @return: status string.
    138          """
    139          # For physical machines, the agent can either be contacted or not.
    140          # However, there is some information to be garnered from potential
    141          # exceptions.
    142          log.debug("Getting status for machine: %s.", label)
    143          machine = self._get_machine(label)
    144
    145          # The status is only used to determine whether the Guest is running
    146          # or whether it is in a stopped status, therefore the timeout can most
    147          # likely be fairly arbitrary. TODO This is a temporary fix as it is
    148          # not compatible with the new Cuckoo Agent, but it will have to do.
    149          url = "http://{0}:{1}".format(machine.ip, CUCKOO_GUEST_PORT)
    150          server = TimeoutServer(url, allow_none=True, timeout=60)
    151
    152          try:
    153              status = server.get_status()
    154          except xmlrpclib.Fault as e:
    155              # Contacted Agent, but it threw an error.
    156              log.debug("Agent error: %s (%s) (Error: %s).",
    157                        machine.id, machine.ip, e)
    158              return self.ERROR
    159          except socket.error as e:
    160              # Could not contact agent.
    161              log.debug("Agent unresponsive: %s (%s) (Error: %s).",
    162                        machine.id, machine.ip, e)
    163              return self.STOPPED
    164          except Exception as e:
    165              # TODO Handle this better.
    166              log.debug("Received unknown exception: %s.", e)
    167              return self.ERROR
    168
    169          # If the agent responded successfully, then the physical machine
    170          # is running
    171          if status:
    172              return self.RUNNING
    173
    174          return self.ERROR
    175
    176 -    def fog_query(self, uri, data={}):
    177          """Wrapper around requests for simplifying FOG API access. Assuming
    178          you can call what FOG is providing an API."""
    179          url = "http://%s/fog/management/index.php?%s" % (
    180              self.options.fog.hostname, uri,
    181          )
    182
    183          data.update({
    184              "uname": self.options.fog.username,
    185              "upass": self.options.fog.password,
    186          })
    187
    188          return requests.post(url, data=data)
    189
    190 -    def fog_init(self):
    191          """Initiate by indexing FOG regarding all available machines."""
    192          self.fog_machines = {}
    193          if not HAVE_FOG or self.options.fog.hostname == "none":
    194              return
    195
    196          # TODO Handle exceptions such as not being able to connect.
    197          r = self.fog_query("node=tasks&sub=listhosts")
    198
    199          # Parse the HTML.
    200          b = bs4.BeautifulSoup(r.content, "html.parser")
    201          if not b.find_all("table"):
    202              raise CuckooCriticalError(
    203                  "The supplied FOG username and/or password do not allow us "
    204                  "to login into FOG, please configure the correct credentials."
    205              )
    206
    207          # Mapping for physical machine hostnames to their mac address and uri
    208          # for "downloading" a safe image onto the host. Great piece of FOG API
    209          # usage here.
    210          for row in b.find_all("table")[0].find_all("tr")[1:]:
    211              hostname, macaddr, download, upload, advanced = row.find_all("td")
    212              self.fog_machines[hostname.text] = (
    213                  macaddr.text, next(download.children).attrs["href"][1:],
    214              )
    215
    216          # Check whether all our machines are available on FOG.
    217          for machine in self.machines():
    218              if machine.label not in self.fog_machines:
    219                  raise CuckooMachineError(
    220                      "The physical machine %s has not been defined in FOG, "
    221                      "please investigate and configure the configuration "
    222                      "correctly." % machine.label
    223                  )
    224
    225 -    def fog_queue_task(self, hostname):
    226          """Queues a task with FOG to deploy the given machine after reboot."""
    227          if hostname in self.fog_machines:
    228              macaddr, download = self.fog_machines[hostname]
    229              self.fog_query(download)
    230
    231 -    def wake_on_lan(self, hostname):
    232          """Start a machine that's currently shutdown."""
    233          if hostname in self.fog_machines:
    234              macaddr, download = self.fog_machines[hostname]
    235              wakeonlan.wol.send_magic_packet(macaddr)
    236