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