Google Cloud Platform Compute Engine Instance Metadata Enumeration (Unix)

2017-04-03T00:00:00
ID ENUMERATE_GOOGLE_COMPUTE_ENGINE_NIX.NASL
Type nessus
Reporter This script is Copyright (C) 2017-2020 and is owned by Tenable, Inc. or an Affiliate thereof.
Modified 2017-04-03T00:00:00

Description

The remote host appears to be a Google Compute Engine instance. Nessus was able to use the metadata API to collect information about the system.

                                        
                                            #TRUSTED b18f9035c47755e97c1e4347f953f5613f0fbad655e23cccf456ce9dae7aed3b72196db74149b010f7bf9eccf4caa93f0968dd349986d275ae7665acab58b46c2ca424ef2eb31139b3d8f17e6d58092b0e51f2c97fc9e6788a20f3b032628d7fa8bbd379167de30667df98db120912395e309b77d435370237639722565222533da332e52bbdda8eee4b2e9c8770e2f4dcf633efaedffff2d9ed13f58f8296a4209ad4fd0eedfac4955c64f715bf8f5d345463f88272c08c22dcaa6761bf229cda47bffc0741b79c8c3067ac4e144bf4610ad248eb0bf1d82c5831c8e100027bbca1e3d5708d163e3b295a73526e3bf251a78515270bdf5152614c06ffef56ecd01cfe5fb80ad4cc3a66ddaed573624cd8582a406d035287c0e4e5b941b738604fb0b20e77fa43d325edc6d80bfc97ed03e19d1bb1876b2f862663e6ee7e9293e14096b9547ac93aeda8350848a02cd2b7d59bcc40dd512994fe394a51f8f06bfede53ddfc2dfc955b8d7bc105b9ae017cecd1c8fb8dea5ab6a0388c2045103137b508ea4a842a4ac91382df66def804eb3cbc307cf89316d18b8e3d096bb4a07c9160b6a67ced1766c2b3f6f4d9860cb76c0cb0292588674c213b01d43295987cc1585153c04bd9dbd7009fe0d571c3d39c2abfe90b393df639896737029b3b57313111fec7120a484dd890e582feb20e897cc88ae7b30e69f51848aa60e256
#
# (C) Tenable Network Security, Inc.
#

include("compat.inc");

if (description)
{
  script_id(99169);
  script_version("1.11");
  script_set_attribute(attribute:"plugin_modification_date", value:"2020/06/12");

  script_name(english:"Google Cloud Platform Compute Engine Instance Metadata Enumeration (Unix)");
  script_summary(english:"Attempts to retrieve Google Compute Engine metadata from a Unix-like operating system.");

  script_set_attribute(attribute:"synopsis", value:
"The remote host is a Google Compute Engine instance for which metadata
could be retrieved.");
  script_set_attribute(attribute:"description", value:
"The remote host appears to be a Google Compute Engine instance. Nessus
was able to use the metadata API to collect information about the
system.");
  script_set_attribute(attribute:"see_also", value:"https://cloud.google.com/compute/");
  script_set_attribute(attribute:"solution", value:"n/a");
  script_set_attribute(attribute:"risk_factor", value:"None");

  script_set_attribute(attribute:"plugin_publication_date", value:"2017/04/03");

  script_set_attribute(attribute:"plugin_type", value:"local");
  script_set_attribute(attribute:"cpe", value:"x-cpe:/a:google:compute_engine");
  script_set_attribute(attribute:"agent", value:"unix");
  script_end_attributes();

  script_category(ACT_GATHER_INFO);
  script_family(english:"General");

  script_copyright(english:"This script is Copyright (C) 2017-2020 and is owned by Tenable, Inc. or an Affiliate thereof.");

  script_dependencies("ssh_get_info.nasl");
  script_require_keys("Host/local_checks_enabled");

  exit(0);
}

include("audit.inc");
include("global_settings.inc");
include("ssh_func.inc");
include("telnet_func.inc");
include("hostlevel_funcs.inc");
include("misc_func.inc");
include("http.inc");

if (sshlib::get_support_level() >= sshlib::SSH_LIB_SUPPORTS_COMMANDS)
  enable_ssh_wrappers();
else disable_ssh_wrappers();

# Include global constants for interacting with the API
include("google_compute_engine.inc");

info_t = NULL;

###
# Establish transport for command running
#
# @remark Checks a list of "supported OS" kb items, and will
#         exit / audit on any failure that would not allow
#         us to continue the check.
#
# @return Always NULL
###
function init_trans()
{
  local_var unsupported, supported, oskb;

  get_kb_item_or_exit("Host/local_checks_enabled");

  unsupported = TRUE;
  # Remote OSes this check is supported on
  supported = make_list(
    "Host/Debian/release",
    "Host/CentOS/release",
    "Host/Ubuntu/release",
    "Host/RedHat/release",
    "Host/SuSE/release",
    "Host/Container-Optimized OS/release"
  );

  foreach oskb (supported)
  {
    if (get_kb_item(oskb))
    {
      unsupported = FALSE;
      break;
    }
  }

  # Not a support OS, bail
  if (unsupported)
    exit(0, "Collection of Google Compute Engine metadata via this plugin is not supported on the host.");

  # Establish command transport
  if (islocalhost())
  {
    if (!defined_func("pread"))
      audit(AUDIT_FN_UNDEF,"pread");
    info_t = INFO_LOCAL;
  }
  else
  {
    sock_g = ssh_open_connection();
    if (!sock_g)
      audit(AUDIT_FN_FAIL,"ssh_open_connection");
    info_t = INFO_SSH;
  }
}

###
# Logging wrapper for info_send_command
#
# @param cmd string command to run with info send command
#
# @return the output of the command
###
function run_cmd(cmd)
{
  local_var ret;
  spad_log(message:'Running command :\n'+cmd);
  ret = info_send_cmd(cmd:cmd);
  spad_log(message:'Output :\n'+ret);
  return ret;
}

##
# Checks the BIOS/Hypervisor info for Google Compute Engine
#
# @remark used to prevent unnecessary requests to API Host
#
# @return TRUE if check passed FALSE otherwise
##
function google_compute_engine_bios_check()
{
  local_var pbuf;
  # HVM
  pbuf = run_cmd(cmd:'cat /sys/devices/virtual/dmi/id/product_name');
  if ("Google Compute Engine" >< pbuf) return TRUE;
  else return FALSE;
}

##
# For remote scans / agent scans on systems without curl
##
function use_wget()
{
  local_var item, cmd, cmdt;
  cmdt = 'wget --header="Metadata-Flavor: Google" -q -O - {URI}';
  item = "http://"+GOOGLE_COMPUTE_ENGINE_API_HOST+GOOGLE_COMPUTE_ENGINE_API_ROOT;
  if (!empty_or_null(_FCT_ANON_ARGS[0]))
    item += _FCT_ANON_ARGS[0];
  cmd = ereg_replace(pattern:"{URI}", replace:item, string:cmdt);
  return run_cmd(cmd:cmd);
}

##
# For remote scans / agent scans
##
function use_curl()
{
  local_var item, cmd, cmdt;
  cmdt = 'curl --header "Metadata-Flavor: Google" -s {URI}';
  item = "http://"+GOOGLE_COMPUTE_ENGINE_API_HOST+GOOGLE_COMPUTE_ENGINE_API_ROOT;
  if (!empty_or_null(_FCT_ANON_ARGS[0]))
    item += _FCT_ANON_ARGS[0];
  cmd  = ereg_replace(pattern:"{URI}", replace:item, string:cmdt);
  return run_cmd(cmd:cmd);
}

##
# For local host scans
##
function use_send_recv3()
{
  local_var item, ret;
  item = GOOGLE_COMPUTE_ENGINE_API_ROOT;
  if (!empty_or_null(_FCT_ANON_ARGS[0]))
    item += _FCT_ANON_ARGS[0];
  ret = http_send_recv3(
    target       : GOOGLE_COMPUTE_ENGINE_API_HOST,
    item         : item,
    port         : 80,
    method       : "GET",
    add_headers  : make_array("Metadata-Flavor", "Google"),
    exit_on_fail : FALSE
  );
  # Return response body
  if (!empty_or_null(ret))
    return ret[2];
  return NULL;
}

###
# Choose the function we will use to get API data with
#
# @remark The agent must run curl / wget to retrieve these
#         items, plugins run by the agent are not allowed to
#         open any sockets.
#
# @return FALSE when no suitable method of calling the API can be found
#         A function pointer for one of the use_* functions defined above
##
function choose_api_function()
{
  local_var pbuf;
  if (info_t == INFO_LOCAL && !get_kb_item("nessus/product/agent"))
    return @use_send_recv3;
  else
  {
    # We prefer cURL over wget
    pbuf = run_cmd(cmd:'curl --nessus_cmd_probe 2>&1');
    if ('nessus_cmd_probe' >< pbuf && 'curl --help' >< pbuf)
      return @use_curl;
    pbuf = run_cmd(cmd:'wget --nessus_cmd_probe 2>&1');
    if ('nessus_cmd_probe' >< pbuf && 'wget --help' >< pbuf)
      return @use_wget;
  }
  return FALSE;
}

###
#  Report success / Create KB items
#  @remark A helper function to reduce code duplication
#
function report_success(apitem, buf)
{
    replace_kb_item(name:kbbase+"/"+apitem, value:buf);
    apitem_tag = str_replace(string:apitem, find: '/',  replace: "-");
    report_xml_tag(tag:xtbase+"-"+apitem_tag, value:buf);
    success = make_list(success, apitem);
    results[apitem] = buf;
}

# Initialize command transport and determine how to talk to the API
init_trans();

if (!google_compute_engine_bios_check())
{
  if (info_t == INFO_SSH) ssh_close_connection();
  exit(0,"BIOS information indicates the system is likely not a Google Compute Engine instance.");
}

api_get_item = choose_api_function();
if (!api_get_item)
{
  if (info_t == INFO_SSH) ssh_close_connection();
  exit(1, "There are no suitable methods for retrieving Google Compute Engine metadata on the system.");
}

# Knowledge and xml tag bases
kbbase = GOOGLE_COMPUTE_ENGINE_KB_BASE;
xtbase = GOOGLE_COMPUTE_ENGINE_HOST_TAG_BASE;

# API items we want to get and their validation regexes
apitems = GOOGLE_COMPUTE_ENGINE_API_ITEMS;

# Check the API root first
buf = api_get_item();
if (isnull(buf) || "hostname" >!< buf || "network-interfaces/" >!< buf)
{
  if (info_t == INFO_SSH) ssh_close_connection();
  exit(1,"The remote host does not appear to be a Google Compute Engine instance.");
}

# Now get each item we're interested in and validate them
success = make_list();
failure = make_list();
results = make_array();
foreach apitem (keys(apitems))
{
  buf = api_get_item(apitem);
  rgx = apitems[apitem];

  if (empty_or_null(buf) || buf !~ rgx)
    failure = make_list(failure, apitem);
  else
  {
    ##
    #  If we have obtained 'hostname' return data,
    #   we can also parse out 'Project ID' 
    ##
    if (apitem == "hostname")
    {
      apitem = "project-id";
      hostparts = make_list();
      hostparts = split(buf, sep:".", keep:FALSE);

      projectid = hostparts[(max_index(hostparts) - 2)];
      report_success(apitem:apitem, buf:projectid);

      # now resume, using the final report_success() call to save 'hostname' info
      apitem = "hostname";
    }

    ##
    #  Zone returns more information than needed
    #   'zone' will be saved in short form (ie: "us-east1-b")
    #   'full-zone' will be saved in long form
    #     (ie: "projects/152814345686/zones/us-east1-b")
    #   'project-num' will be saved as well
    #     (ie: "152814345686")
    ##
    if (apitem == "zone")
    {
      zoneparts = make_list();
      zoneparts = split(buf, sep:"/", keep:FALSE);
      actualzone = zoneparts[(max_index(zoneparts) - 1)];
      report_success(apitem:apitem, buf:actualzone);

      apitem = "project-num";
      projectnum = zoneparts[(max_index(zoneparts) - 3)];
      report_success(apitem:apitem, buf:projectnum);

      # now resume, using the final report_success() call to save 'full-zone' info
      apitem = "full-zone";
    }

    report_success(apitem:apitem, buf:buf);
  }
}

# For grabbing IP addresses. X and Y are indexes.
# Internals are at /network-interfaces/X/ip
# Externals are at /network-interfaces/X/access-configs/Y/external-ip
# GOOGLE_COMPUTE_ENGINE_NETWORK_INTERFACES_LIST = "network-interfaces/";
# GOOGLE_COMPUTE_ENGINE_IP = "ip";
# GOOGLE_COMPUTE_ENGINE_ACCESS_CONFIGS_LIST = "access-configs/";
# GOOGLE_COMPUTE_ENGINE_EXTERNAL_IP = "external-ip";
network_interfaces = api_get_item(GOOGLE_COMPUTE_ENGINE_NETWORK_INTERFACES_LIST);
foreach interface (split(network_interfaces, keep:FALSE))
{
  # interface = "0/"

  # first grab internal ip
  # don't log failures, as this interface may not have an internal ip
  apitem = GOOGLE_COMPUTE_ENGINE_NETWORK_INTERFACES_LIST + interface + "ip";
  internal_ip = api_get_item(apitem);
  if (!empty_or_null(internal_ip) && internal_ip =~ "^\d+\.\d+\.\d+\.\d+$")
  {
    replace_kb_item(name:kbbase+"/"+apitem, value:internal_ip);
    apitem_tag = str_replace(string:apitem, find: '/',  replace: "-");
    report_xml_tag(tag:xtbase+"-"+apitem_tag, value:internal_ip);
    success = make_list(success, apitem);
    results[apitem] = internal_ip;
  }

  # then try enumerating external ips
  access_configs = api_get_item(
    GOOGLE_COMPUTE_ENGINE_NETWORK_INTERFACES_LIST +
    interface +
    GOOGLE_COMPUTE_ENGINE_ACCESS_CONFIGS_LIST
  );
  foreach config (split(access_configs, keep:FALSE))
  {
    apitem  = GOOGLE_COMPUTE_ENGINE_NETWORK_INTERFACES_LIST +
              interface +
              GOOGLE_COMPUTE_ENGINE_ACCESS_CONFIGS_LIST +
              config +
              "external-ip";
    external_ip = api_get_item(apitem);
    if (!empty_or_null(external_ip) && external_ip =~ "^\d+\.\d+\.\d+\.\d+$")
    {
      replace_kb_item(name:kbbase+"/"+apitem, value:external_ip);
      apitem_tag = str_replace(string:apitem, find: '/',  replace: "-");
      report_xml_tag(tag:xtbase+"-"+apitem_tag, value:external_ip);
      success = make_list(success, apitem);
      results[apitem] = external_ip;
    }
  }
}

if (info_t == INFO_SSH) ssh_close_connection();

# Report successful retrievals
report = "";
if (max_index(success) != 0)
{
  report +=
  '\n  It was possible to retrieve the following API items:\n';
  foreach apitem (success)
    report += '\n    - '+apitem+': '+results[apitem];
  report += '\n';
}

# Report failures, should always be blank, mostly to help out CS
if (max_index(failure) != 0)
{
  report +=
  '\n  The following items could not be retrieved:\n';
  foreach apitem (failure)
    report += '\n    - '+apitem;
  report += '\n';
}

security_report_v4(port:0, severity:SECURITY_NOTE, extra:report);