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.


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*.

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: 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 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"))

     // extract Velociraptor config for policy
     LET extracted_config <= SELECT * FROM foreach(
               row=config.server_urls,
               query={
                   SELECT
                       get_domain(URL=_value) AS DstAddr,
                       get_port(URL=_value) AS DstPort,
                       'VelociraptorFrontEnd' AS Description,
                       _value AS URL
                   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 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 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)

     // 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, (
                  pathToNFT, 'add', 'rule', 'inet', TableName, 'inbound_chain',

                  'ip', 'saddr', DstAddr, 'tcp', 'sport', '{', DstPort, '}',
                  'ct', 'state', 'established',
                  'accept') AS add_velociraptor_rule_to_inbound_chain_cmd
              FROM extracted_config
          },
          query={
            SELECT * FROM run_command(Cmd=add_velociraptor_rule_to_inbound_chain_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, (
                  pathToNFT, 'add', 'rule', 'inet', TableName, 'outbound_chain',
                  'ip', 'daddr', DstAddr, 'tcp', 'dport', '{', DstPort, '}',
                  'ct', 'state', 'established,new',
                  'accept') AS add_velociraptor_rule_to_outbound_chain_cmd
              FROM extracted_config
          },
          query={
              SELECT * FROM run_command(
                 Cmd=add_velociraptor_rule_to_outbound_chain_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)
         },
         query={
             SELECT format(format="Testing connectivity with %v: %v", args=[Url, Response]) AS Result
             FROM http_client(url=pem_url, disable_ssl_security='TRUE')
             WHERE Response = 200
             LIMIT 1
         })

     // final check to keep or remove policy
     // 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 = SELECT * FROM if(condition= test_connection,
               then={
                   SELECT
                       timestamp(epoch=now()) as Time,
                       if(condition=MessageBox,
                           then= TableName + ' connection test successful. MessageBox sent.',
                           else= TableName + ' connection test successful.'
                       ) AS Result
                    FROM if(condition=MessageBox,
                        then= {
                            SELECT * FROM execve(argv=['wall', MessageBox])
                        },
                        else={
                            SELECT * FROM scope()
                        })
               },
               else=run_command(
                      Cmd=delete_table_cmd,
                      Message= TableName + ' failed connection test. Removing quarantine table.'))

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

     // Execute content
     LET doit = SELECT * FROM if(condition=RemovePolicy,
               then=delete_quarantine_table,
               else={
                   SELECT * FROM chain(
                       a=delete_quarantine_table,
                       b=run_command(Cmd=add_table_cmd, Message=TableName + ' added.'),
                       c=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_outbound_chain_cmd,
                           Message='Added outbound_chain to ' +
                                     TableName + ' table.'),
                       g=add_velociraptor_rule_to_outbound_chain,
                       h=run_command(Cmd=add_tcp_rule_to_outbound_chain_cmd,
                           Message='Added tcp rule to outbound_chain in ' +
                                     TableName + ' table.'),
                       i=run_command(Cmd=add_udp_rule_to_outbound_chain_cmd,
                           Message='Added udp rule to outbound_chain in ' +
                                     TableName + ' table.'),
                       j=run_command(Cmd=add_forward_chain_cmd,
                           Message='Added forward_chain to ' +
                                     TableName + ' table.'),
                       k=final_check)
               })

     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
       })