Amazon Web Services EC2 Instance Metadata Enumeration (Unix)

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

Description

The remote host appears to be an Amazon Machine Image. Nessus was able to use the metadata API to collect information about the system.

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

include("compat.inc");

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

  script_name(english:"Amazon Web Services EC2 Instance Metadata Enumeration (Unix)");
  script_summary(english:"Attempts to retrieve EC2 metadata from a Unix like operating system.");

  script_set_attribute(attribute:"synopsis", value:
"The remote host is an AWS EC2 instance for which metadata could be
retrieved.");
  script_set_attribute(attribute:"description", value:
"The remote host appears to be an Amazon Machine Image. Nessus was able
to use the metadata API to collect information about the system.");
  script_set_attribute(attribute:"see_also", value:"https://docs.aws.amazon.com/ec2/index.html");
  script_set_attribute(attribute:"solution", value:"n/a");
  script_set_attribute(attribute:"risk_factor", value:"None");

  script_set_attribute(attribute:"plugin_publication_date", value:"2016/03/25");

  script_set_attribute(attribute:"plugin_type", value:"local");
  script_set_attribute(attribute:"agent", value:"unix");
  script_set_attribute(attribute:"cpe", value:"x-cpe:/o:amazon:ec2");
  script_end_attributes();

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

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

  script_dependencies("bios_get_info_ssh.nasl", "ifconfig_inet4.nasl");
  script_require_keys("Host/local_checks_enabled");

  exit(0);
}

include("agent.inc");
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");
include("data_protection.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("amazon_aws_ami.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, should this only
  # be Host/AmazonLinux/release ?
  supported = make_list(
    "Host/AmazonLinux/release",
    "Host/CentOS/release",
    "Host/Debian/release",
    "Host/FreeBSD/release",
    "Host/Gentoo/release",
    "Host/HP-UX/version",
    "Host/Mandrake/release",
    "Host/RedHat/release",
    "Host/Slackware/release",
    "Host/Solaris/Version",
    "Host/Solaris11/Version",
    "Host/SuSE/release",
    "Host/Ubuntu/release",
    "Host/AIX/version"
  );

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

  # Not a support OS, bail
  if (unsupported)
    exit(0, "Collection of AWS 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 an Amazon BIOS/version of Xen
#
# @remark used to prevent unnecessary requests to API Host
#
# @return TRUE if check passed FALSE otherwise
##
function amazon_bios_check()
{
  local_var kb_value, pbuf;

  # Check if DMI data has already been gathered
  kb_value = get_kb_item("BIOS/Vendor");
  if (kb_value =~ "Amazon EC2")
    return TRUE;
  
  kb_value = get_kb_item("Host/dmidecode");
  if (preg(string:kb_value, pattern:"(Vendor|Manufacturer): *Amazon EC2", icase:TRUE, multiline:TRUE))
    return TRUE;

  # HVM
  pbuf = run_cmd(cmd:'cat /sys/devices/virtual/dmi/id/uevent');
  if (pbuf =~ "amazon") return TRUE;
  pbuf = run_cmd(cmd:'cat /sys/devices/virtual/dmi/id/bios_version');
  if ("amazon" >< pbuf) return TRUE;
  pbuf = run_cmd(cmd:'dmidecode -s system-version 2>&1');
  if ("amazon" >< pbuf) return TRUE;

  # Paravirtualized AMIs
  pbuf = run_cmd(cmd:'cat /sys/hypervisor/version/extra');
  if ("amazon" >< pbuf) return TRUE;
  else return FALSE;
}

###
# Determines which API path to use: AWS_AMI_API_ROOT if not
# instance identity document which uses alternate endpoint
###
function api_get_item_wrapper()
{
  local_var result;
  if (_FCT_ANON_ARGS[0] == 'instance-identity-document')
    result = api_get_item(AWS_AMI_API_INSTANCE_IDENTITY_DOCUMENT);
  else
    result = api_get_item(AWS_AMI_API_ROOT + _FCT_ANON_ARGS[0]);
  return result;
}

##
# For remote scans / agent scans on systems without curl
##
function use_wget()
{
  local_var item, cmd, cmdt;
  cmdt =  "unset http_proxy HTTP_PROXY HTTPS_PROXY ALL_PROXY all_proxy > /dev/null 2>&1; "; # Avoid using proxy
  cmdt += "export NO_PROXY=169.254.169.254 > /dev/null 2>&1; "; # Further attempt to avoid proxy 
  cmdt += "wget -q -T 5 -O - {URI}";
  item = "http://"+AWS_AMI_API_HOST;
  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 =  "unset http_proxy HTTP_PROXY HTTPS_PROXY ALL_PROXY all_proxy > /dev/null 2>&1; "; # Avoid using proxy
  cmdt += "export NO_PROXY=169.254.169.254 > /dev/null 2>&1; "; # Further attempt to avoid proxy 
  cmdt += "curl -s -m 5 {URI}";
  item = "http://"+AWS_AMI_API_HOST;
  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 = '';
  # use api root for a bare request: get_api_item()
  if (!empty_or_null(_FCT_ANON_ARGS[0]))
    item = _FCT_ANON_ARGS[0];
  ret = http_send_recv3(
    target       : AWS_AMI_API_HOST,
    item         : item,
    port         : 80,
    method       : "GET",
    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;
}


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

# Amazon Linux is built for EC2 so we can skip the BIOS checks
check_bios = TRUE;
if (!isnull(get_kb_item("Host/AmazonLinux/release")))
  check_bios = FALSE;

# Basic EC2 checks before communication with API server
if (check_bios && !amazon_bios_check())
{
  if (info_t == INFO_SSH) ssh_close_connection();
  exit(0,"BIOS and Hypervisor information indicate the system is likely not an AWS 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 AMI data on the system.");
}

# Knowledge and xml tag bases
kbbase = AWS_AMI_KB_BASE;
xtbase = AWS_AMI_HOST_TAG_BASE;

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

# Check the API root first
buf = api_get_item_wrapper();
if (isnull(buf) || "ami-id" >!< buf || "instance-id" >!< buf)
{
  if (info_t == INFO_SSH) ssh_close_connection();
  exit(1,"The remote host does not appear to be an AWS Instance.");
}


# Now get each item we're interested in and validate them
apiresults = make_array();
success = make_list();
failure = make_list();

foreach apitem (keys(apitems))
{
  buf = api_get_item_wrapper(apitem);
  rgx = apitems[apitem];

  if (empty_or_null(buf) || buf !~ rgx)
    failure = make_list(failure, apitem);
  else
  {
    apiresults[apitem] = buf;
    if (apitem != 'instance-identity-document')
      success = make_list(success, apitem);
    else
    {
      # break apart and record metadata from instance identity JSON
      #
      # {
      #   "devpayProductCodes" : null,
      #   "marketplaceProductCodes" : null,
      #   "availabilityZone" : "us-east-2c",
      #   "version" : "2017-09-30",
      #   "region" : "us-east-2",
      #   "instanceId" : "i-02970e5aab6f4a924",
      #   "billingProducts" : null,
      #   "instanceType" : "t2.micro",
      #   "privateIp" : "172.31.45.131",
      #   "imageId" : "ami-47e5bf23",
      #   "accountId" : "232578266044",
      #   "architecture" : "x86_64",
      #   "kernelId" : null,
      #   "ramdiskId" : null,
      #   "pendingTime" : "2017-12-12T16:40:07Z"
      # }
      foreach line (split(buf, keep:FALSE))
      {
        pattern = '"(.*?)"[\\s]+:[\\s]+"?(.*?)[",]';
        json = pregmatch(pattern:pattern, string:line);

        if (!empty_or_null(json))
        {
          apiresults[json[1]] = json[2];
          success = make_list(success, json[1]);
        }
      }
    }
  }
}

# special case for vpc-id since it requires the mac address which is dynamic
mac = apiresults["mac"];
if (mac =~ "^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$")
{
  # valid mac
  vpc_id = api_get_item_wrapper("network/interfaces/macs/" + mac + "/vpc-id");
  if (vpc_id =~ "^vpc-[A-Za-z0-9]+$")
  {
    # valid vpc-id
    apiresults["vpc-id"] = vpc_id;
    success = make_list(success, "vpc-id");
  }
  else failure = make_list(failure, "vpc-id");
}


if (info_t == INFO_SSH) ssh_close_connection();

report = "";

# Check if the IP address gathered matches one of the host's IP addresses
# to ensure we did not retrieve a proxy's metadata
# Note: currently only IPv4 is supported
ipv4_addresses = get_kb_list("Host/ifconfig/IP4Addrs");
ip_address_matched = ip_address_check(apiresults:apiresults, ipv4_addresses:ipv4_addresses);

proxy_detected = false;

if (!isnull(ip_address_matched) && !ip_address_matched)
{
  proxy_detected = true;
  report += '\nThe EC2 instance metadata below appears to be from a proxy due to the' +
            '\nIP addresses not matching any collected IP addresses.\n';
}

# Report successful retrievals
if (max_index(success) != 0)
{
  report +=
  '\n  It was possible to retrieve the following API items :\n';

  foreach apitem (success)
  {
    report += '\n    - '+apitem+': '+data_protection::sanitize_user_enum(users:apiresults[apitem]);

    # Don't register XML tag if it appears that the metadata from a proxy was received
    if (proxy_detected)
    {
      replace_kb_item(name:kbbase+"/proxy_detected", value:TRUE);
      continue; 
    }

    replace_kb_item(name:kbbase+"/"+apitem, value:apiresults[apitem]);
    report_xml_tag(tag:xtbase+"-"+apitem, value:apiresults[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);