Linux.Remediation.Quarantine

This artifact applies quarantine to Linux systems via nftables. It expects the target system to have nftables installed, and hence the availability of nft CLI.

This artifact will create a table, with the default name vrr_quarantine_table, which contains three chains. One for inbound traffic, one for outbound traffic, and the other for forwarding traffic. The chains will cut off all traffics except those for DNS lookup and velociraptor itself.

To unquarantine the system, set the RemovePolicy parameter to True.

The parameter ForbiddenTestURL is used for testing if the quarantine is working as expected, so set it to a URL that should not be reachable from a quarantined system.


name: Linux.Remediation.Quarantine
description: |
  This artifact applies quarantine to Linux systems via nftables.
  It expects the target system to have nftables installed, and
  hence the availability of nft CLI.

  This artifact will create a table, with the default name
  *vrr_quarantine_table*, which contains three chains. One
  for inbound traffic, one for outbound traffic, and the other
  for forwarding traffic. The chains will cut off all traffics
  except those for DNS lookup and velociraptor itself.

  To unquarantine the system, set the *RemovePolicy* parameter to
  *True*.

  The parameter *ForbiddenTestURL* is used for testing if the
  quarantine is working as expected, so set it to a URL that should
  not be reachable from a quarantined system.

precondition: SELECT OS From info() where OS = 'linux'

type: CLIENT

required_permissions:
  - EXECVE

parameters:
  - name: pathToNFT
    default: /usr/sbin/nft
    description: We depend on nft to manage the tables, chains, and rules.

  - name: TableName
    default: vrr_quarantine_table
    description: Name of the quarantine table

  - name: ForbiddenTestURL
    default: https://www.google.com
    description: URL for forbidden connection check

  - name: MessageBox
    description: |
        Optional message box notification to send to logged in users. 256
        character limit.

  - name: RemovePolicy
    type: bool
    description: Tickbox to remove policy.

sources:
  - query: |
       LET State <= dict(installed=FALSE)

       LET SetInstalled = set(item=State, field="installed", value=TRUE)

       LET ClearInstalled = set(item=State, field="installed", value=FALSE) || TRUE

       LET run_command(Cmd, Message) = SELECT
           timestamp(epoch=now()) AS Time,
           format(format="Running %v: %v, Returned %v %v",
                  args=[Cmd, Stdout || Stderr, ReturnCode,
                    Message || ""]) AS Result
         FROM execve(argv=Cmd, length=10000)

       // If a MessageBox configured truncate to 256 character limit
       LET MessageBox <= parse_string_with_regex(regex='^(?P<Message>.{0,255}).*',
                                                 string=MessageBox).Message

       // Parse a URL to get domain name.
       LET get_domain(URL) = split(string=url(parse=URL).Host, sep=":")[0]

       LET get_port(URL) = if(condition=url(parse=URL).Host =~ ":",
                              then=split(string=url(parse=URL).Host, sep=":")[1],
                              else=if(condition=url(parse=URL).Scheme = "https",
                                      then="443",
                                      else="80"))

       LET add_to_in_chain(DstAddr, DstPort) = (pathToNFT, 'add', 'rule', 'inet',
             TableName, 'inbound_chain', 'ip', 'saddr', host(name=DstAddr)[0],
             'tcp', 'sport', '{', DstPort, '}', 'ct', 'state', 'established', 'accept')

       LET add_to_out_chain(DstAddr, DstPort) = (pathToNFT, 'add', 'rule', 'inet',
             TableName, 'outbound_chain', 'ip', 'daddr', host(name=DstAddr)[0],
             'tcp', 'dport', '{', DstPort, '}', 'ct', 'state', 'established,new', 'accept')

       // extract Velociraptor config for policy
       LET extracted_config <= SELECT get_domain(URL=_value) AS DstAddr,
                                      get_port(URL=_value) AS DstPort,
                                      'VelociraptorFrontEnd' AS Description,
                                      _value AS URL
         FROM foreach(row=config.server_urls)

       LET send_message_box(Msg) = SELECT timestamp(epoch=now()) AS Time,
                                          Result
         FROM if(condition=MessageBox,
                 then={
           SELECT Msg + ' MessageBox sent.' AS Result
           FROM execve(argv=['wall', MessageBox])
         },
                 else={
           SELECT Msg AS Result
           FROM scope()
         })

       // delete table
       LET delete_table_cmd = (pathToNFT, 'delete', 'table', 'inet',
             TableName)

       // add table
       LET add_table_cmd = (pathToNFT, 'add', 'table', 'inet',
             TableName)

       // add inbound chain
       LET add_inbound_chain_cmd = (pathToNFT, 'add', 'chain', 'inet',
             TableName, 'inbound_chain', '{', 'type', 'filter', 'hook', 'input', 'priority', '0\;', 'policy', 'drop\;', '}')

       // add udp rule inbound chain to allow DNS lookups
       LET add_udp_rule_to_inbound_chain_cmd = (pathToNFT, 'add', 'rule', 'inet',
             TableName, 'inbound_chain', 'udp', 'sport', 'domain', 'ct', 'state', 'established', 'accept')

       // add localhost inbound rule to allow DNS lookups (needed on some ubuntu systems)
       // This is where systemd-resolved listens for local dns cache
       LET add_localhost_rule_to_inbound_chain_cmd = (pathToNFT, 'add', 'rule', 'inet',
             TableName, 'inbound_chain', 'ip', 'daddr', '127.0.0.53', 'accept')

       // add outbound chain
       LET add_outbound_chain_cmd = (pathToNFT, 'add', 'chain', 'inet',
             TableName, 'outbound_chain', '{', 'type', 'filter', 'hook', 'output', 'priority', '0\;', 'policy', 'drop\;', '}')

       // add tcp rule outbound chain to allow DNS traffics
       LET add_tcp_rule_to_outbound_chain_cmd = (pathToNFT, 'add', 'rule', 'inet',
             TableName, 'outbound_chain', 'tcp', 'dport', '{', '53', '}', 'ct', 'state', 'new,established', 'accept')

       // add udp rule outbound chain to allow DNS and DHCP traffics
       LET add_udp_rule_to_outbound_chain_cmd = (pathToNFT, 'add', 'rule', 'inet',
             TableName, 'outbound_chain', 'udp', 'dport', '{', '53,67,68', '}', 'ct', 'state', 'new,established', 'accept')

       // add localhost outbound rule to allow DNS lookups (needed on some ubuntu systems)
       LET add_localhost_rule_to_outbound_chain_cmd = (pathToNFT, 'add', 'rule', 'inet',
             TableName, 'outbound_chain', 'ip', 'saddr', '127.0.0.53', 'accept')

       // add forward chain
       LET add_forward_chain_cmd = (pathToNFT, 'add', 'chain', 'inet',
             TableName, 'forward_chain', '{', 'type', 'filter', 'hook', 'forward', 'priority', '0\;', 'policy', 'drop\;', '}')

       // delete quarantine table
       LET delete_quarantine_table = SELECT timestamp(epoch=now()) AS Time,
                                            TableName + ' table removed.' AS Result
         FROM execve(argv=delete_table_cmd, length=10000)
         WHERE ClearInstalled

       // add tcp rule to inbound_chain to allow connections from Velociraptor
       // FIXME(gye): may need to add IPv6 rules if DstAddr is an IPv6 address
       LET add_velociraptor_rule_to_inbound_chain = SELECT *
         FROM foreach(row={
           SELECT DstAddr,
                  DstPort,
                  add_to_in_chain(DstAddr=DstAddr, DstPort=DstPort) AS cmd
           FROM extracted_config
         },
                      query={
           SELECT *
           FROM run_command(Cmd=cmd,
                            Message='Added tcp rule to inbound_chain in ' +
                              TableName + ' table.')
         })

       // add tcp rule to inbound_chain to allow connections from Velociraptor
       // FIXME(gye): may need to add IPv6 rules if DstAddr is an IPv6 address
       LET add_velociraptor_rule_to_outbound_chain = SELECT *
         FROM foreach(row={
           SELECT DstAddr,
                  DstPort,
                  add_to_out_chain(DstAddr=DstAddr, DstPort=DstPort) AS cmd
           FROM extracted_config
         },
                      query={
           SELECT *
           FROM run_command(Cmd=cmd,
                            Message='Added tcp rule to inbound_chain in ' +
                              TableName + ' table.')
         })

       // test connection to a frontend server
       LET test_connection = SELECT *
         FROM foreach(row={
           SELECT DstAddr,
                  DstPort,
                  URL + 'server.pem' AS pem_url
           FROM extracted_config
           WHERE log(message="Will check connectivity with " + pem_url, dedup=-1)
         },
                      query={
           SELECT format(format="Testing connectivity with %v: %v",
                         args=[Url, Response]) AS Result
           FROM http_client(url=pem_url)
           WHERE Response = 200 AND log(dedup=-1,
                message="got %v for url %v", args=[Response, Url])
           LIMIT 1
         })

       // test connection to the ForbiddenTestURL - if the connection
       // can not be made, this will run in the http_client timeout that
       // can not be set manually. thus, the artifact will run for
       // approx. 2 minutes. Returns true if we are able to connect.
       LET test_forbidden_connection = SELECT *
         FROM http_client(url=log(message="Testing forbidden connection to " +
                                    ForbiddenTestURL,
                                  dedup=-1)
                           && ForbiddenTestURL)
         WHERE NOT Response =~ '^5..$' AND log(dedup=-1,
             message="got %v for url %v", args=[Response, Url])
         LIMIT 1

       // final checks to keep or remove policy
       // first check if connection to velociraptor server can be made
       LET final_check_allowed = SELECT *
         FROM if(
           condition=test_connection,
           then=send_message_box(Msg=TableName + ' connection test successful.'),
           else={
           SELECT *
           FROM run_command(
             Cmd=log(
               dedup=-1,
               message="%v failed connection test. Removing quarantine table.",
               args=TableName,
               level="ERROR")
              && delete_table_cmd,
             Message=TableName + ' failed connection test. Removing quarantine table.')
           WHERE ClearInstalled
         })

       // then check if connection to ForbiddenTestURL can NOT be made
       // TODO(gyee): for now we are using the wall commmand to send the message.
       // Will need to look into using libnotify instead.
       LET final_check_forbidden = SELECT *
         FROM if(
           condition=State.installed
            AND test_forbidden_connection,
           then={
           SELECT *
           FROM run_command(
             Cmd=log(
               dedup=-1,
               message="%v failed forbidden connection test - connection to %v could be established. Removing quarantine table.",
               args=[TableName, ForbiddenTestURL],
               level="ERROR")
              && delete_table_cmd,
             Message=TableName + ' failed forbidden connection test. Removing quarantine table.')
           WHERE ClearInstalled
         },
           else=send_message_box(
             Msg=TableName + ' forbidden connection test successful.'))

       LET check_nft_cmd = (pathToNFT, "--version")

       // Execute content
       LET doit = SELECT *
         FROM if(condition=RemovePolicy,
                 then={
           SELECT *
           FROM delete_quarantine_table
         },
                 else={
           SELECT *
           FROM chain(
             a=delete_quarantine_table,
             b={
           SELECT *
           FROM run_command(Cmd=add_table_cmd,
                            Message=SetInstalled
                             && TableName + ' added.')
         },
             c={
           SELECT *
           FROM run_command(
             Cmd=add_inbound_chain_cmd,
             Message='Added inbound_chain to ' + TableName + ' table.')
         },
             d=add_velociraptor_rule_to_inbound_chain,
             e=run_command(Cmd=add_udp_rule_to_inbound_chain_cmd,
                           Message='Added udp rule to inbound_chain in ' +
                             TableName + ' table.'),
             f=run_command(Cmd=add_localhost_rule_to_inbound_chain_cmd,
                           Message='Added localhost rule to inbound_chain in ' +
                             TableName + ' table.'),
             g=run_command(
               Cmd=add_outbound_chain_cmd,
               Message='Added outbound_chain to ' + TableName + ' table.'),
             h=add_velociraptor_rule_to_outbound_chain,
             i=run_command(Cmd=add_tcp_rule_to_outbound_chain_cmd,
                           Message='Added tcp rule to outbound_chain in ' +
                             TableName + ' table.'),
             j=run_command(Cmd=add_udp_rule_to_outbound_chain_cmd,
                           Message='Added udp rule to outbound_chain in ' +
                             TableName + ' table.'),
             k=run_command(Cmd=add_localhost_rule_to_outbound_chain_cmd,
                           Message='Added localhost rule to outbound_chain in ' +
                             TableName + ' table.'),
             l=run_command(
               Cmd=add_forward_chain_cmd,
               Message='Added forward_chain to ' + TableName + ' table.'),
             m={
           SELECT *
           FROM final_check_allowed
         },
             n={
           SELECT *
           FROM if(condition=State.installed,
                   then=final_check_forbidden)
         })
         })

       SELECT *
       FROM if(condition={
           SELECT *
           FROM run_command(Cmd=check_nft_cmd, Message='Check for ' + pathToNFT)
           WHERE Result =~ "nftables"
         },
               then=doit,
               else={
           SELECT *
           FROM scope()
           WHERE log(level="ERROR",
                     message="nftables is not installed - quarantine not supported")
            AND FALSE
         })