This APT (Advanced Persistent Threat) detection recipe ingests EDR (Endpoint Detection and Response) and network traffic logs, while monitoring for an IoB (Indicator of Behavior) that matches malicious data exfiltration patterns.
Download to same directory as Quine.
title: APT Detection
summary: Endpoint logs and network traffic data merge to auto-detect exfiltration
contributor: https://github.com/rrwright
version: 1
description: |-
This APT (Advanced Persistent Threat) detection recipe ingests EDR (Endpoint
Detection and Response) and network traffic logs, while monitoring for an IoB
(Indicator of Behavior) that matches malicious data exfiltration patterns.
SCENARIO:
Using a standing query, the recipe monitors for covert interprocess
communication using a file to pass data. When that pattern is matched, with a
network SEND event, we have our smoking gun and a URL is logged linking to
the Quine Exploration UI with the full activity and context for investigation.
In this scenario, a malicious Excel macro collects personal data and stores
it in a temporary file. The APT process "ntclean" infiltrated the system
previously through an SSH exploit, and now reads from that temporary file
and exfiltrates data from the network--hiding it as an HTTP GET request--
before deleting the temporary file to cover its tracks.
The source of the SSH exploit that planted the APT and the destination
for exfiltrated data utilize the same IP address.
SAMPLE DATA:
endpoint.json - https://recipes.quine.io/apt-detection/endpoint-json
network.json - https://recipes.quine.io/apt-detection/network-json
Download the sample data to the same directory where Quine will be run.
RESULTS:
When the standing query detects the WRITE->READ->SEND->DELETE pattern, it
will output a link to the console that can be copied and pasted into a
browser to explore the event in the Quine Exploration UI.
ingestStreams:
- type: FileIngest
path: endpoint.json
format:
type: CypherJson
query: >-
MATCH (proc), (event), (object)
WHERE id(proc) = idFrom($that.pid)
AND id(event) = idFrom($that)
AND id(object) = idFrom($that.object)
SET proc.id = $that.pid,
proc: Process,
event.type = $that.event_type,
event: EndpointEvent,
event.time = $that.time,
object.data = $that.object
CREATE (proc)-[:EVENT]->(event)-[:EVENT]->(object)
- type: FileIngest
path: network.json
format:
type: CypherJson
query: >-
MATCH (src), (dst), (event)
WHERE id(src) = idFrom($that.src_ip+":"+$that.src_port)
AND id(dst) = idFrom($that.dst_ip+":"+$that.dst_port)
AND id(event) = idFrom('network_event', $that)
SET src.ip = $that.src_ip+":"+$that.src_port,
src: IP,
dst.ip = $that.dst_ip+":"+$that.dst_port,
dst: IP,
event.proto = $that.proto,
event.time = $that.time,
event.detail = $that.detail,
event: NetTraffic
CREATE (src)-[:NET_TRAFFIC]->(event)-[:NET_TRAFFIC]->(dst)
standingQueries:
- pattern:
type: Cypher
query: >-
MATCH (e1)-[:EVENT]->(f)<-[:EVENT]-(e2),
(f)<-[:EVENT]-(e3)<-[:EVENT]-(p2)-[:EVENT]->(e4)
WHERE e1.type = "WRITE"
AND e2.type = "READ"
AND e3.type = "DELETE"
AND e4.type = "SEND"
RETURN DISTINCT id(f) as fileId
outputs:
stolen-data:
type: CypherQuery
query: >-
MATCH (p1)-[:EVENT]->(e1)-[:EVENT]->(f)<-[:EVENT]-(e2)<-[:EVENT]-(p2),
(f)<-[:EVENT]-(e3)<-[:EVENT]-(p2)-[:EVENT]->(e4)-[:EVENT]->(ip)
WHERE id(f) = $that.data.fileId
AND e1.type = "WRITE"
AND e2.type = "READ"
AND e3.type = "DELETE"
AND e4.type = "SEND"
AND e1.time < e2.time
AND e2.time < e3.time
AND e2.time < e4.time
CREATE (e1)-[:NEXT]->(e2)-[:NEXT]->(e4)-[:NEXT]->(e3)
WITH e1, e2, e3, e4, p1, p2, f, ip, "http://localhost:8080/#MATCH" + text.urlencode(" (e1),(e2),(e3),(e4),(p1),(p2),(f),(ip) WHERE id(p1)='"+strId(p1)+"' AND id(e1)='"+strId(e1)+"' AND id(f)='"+strId(f)+"' AND id(e2)='"+strId(e2)+"' AND id(p2)='"+strId(p2)+"' AND id(e3)='"+strId(e3)+"' AND id(e4)='"+strId(e4)+"' AND id(ip)='"+strId(ip)+"' RETURN e1, e2, e3, e4, p1, p2, f, ip") as URL
RETURN URL
andThen:
type: PrintToStandardOut
nodeAppearances:
- predicate:
propertyKeys: []
knownValues: {}
dbLabel: Process
icon: ion-load-a
label:
type: Property
key: id
prefix: "Process: "
- predicate:
propertyKeys: []
knownValues: {}
dbLabel: IP
icon: ion-ios-world
label:
type: Property
key: ip
prefix: ""
- predicate:
propertyKeys: []
knownValues: {}
dbLabel: EndpointEvent
icon: ion-android-checkmark-circle
label:
type: Property
key: type
prefix: ""
- predicate:
propertyKeys: []
knownValues: {}
dbLabel: NetTraffic
icon: ion-network
label:
type: Property
key: proto
prefix: ""
- predicate:
propertyKeys: []
knownValues: {}
icon: ion-ios-copy
label:
type: Property
key: data
prefix: ""
quickQueries:
- predicate:
propertyKeys: []
knownValues: {}
quickQuery:
name: Adjacent Nodes
querySuffix: MATCH (n)--(m) RETURN DISTINCT m
queryLanguage: Cypher
sort: Node
- predicate:
propertyKeys: []
knownValues: {}
quickQuery:
name: Refresh
querySuffix: RETURN n
queryLanguage: Cypher
sort: Node
- predicate:
propertyKeys: []
knownValues: {}
quickQuery:
name: Local Properties
querySuffix: RETURN id(n), properties(n)
queryLanguage: Cypher
sort: Text
- predicate:
propertyKeys: []
knownValues: {}
dbLabel: Process
quickQuery:
name: Files Read
querySuffix: MATCH (n)-[:EVENT]->(e)-[:EVENT]->(f) WHERE e.type = "READ" RETURN f
queryLanguage: Cypher
sort: Node
edgeLabel: read
- predicate:
propertyKeys: []
knownValues: {}
dbLabel: Process
quickQuery:
name: Files Written
querySuffix: MATCH (n)-[:EVENT]->(e)-[:EVENT]->(f) WHERE e.type = "WRITE" RETURN f
queryLanguage: Cypher
sort: Node
edgeLabel: wrote
- predicate:
propertyKeys:
- data
knownValues: {}
quickQuery:
name: Read By
querySuffix: MATCH (n)<-[:EVENT]-(e)<-[:EVENT]-(p) WHERE e.type = "READ" RETURN p
queryLanguage: Cypher
sort: Node
edgeLabel: written by
- predicate:
propertyKeys:
- data
knownValues: {}
quickQuery:
name: Written By
querySuffix: MATCH (n)<-[:EVENT]-(e)<-[:EVENT]-(p) WHERE e.type = "WRITE" RETURN p
queryLanguage: Cypher
sort: Node
edgeLabel: written by
- predicate:
propertyKeys: []
knownValues: {}
dbLabel: Process
quickQuery:
name: Received Data
querySuffix: MATCH (n)-[:EVENT]->(e)-[:EVENT]->(i) WHERE e.type = "RECEIVE" RETURN i
queryLanguage: Cypher
sort: Node
edgeLabel: received
- predicate:
propertyKeys: []
knownValues: {}
dbLabel: Process
quickQuery:
name: Sent Data
querySuffix: MATCH (n)-[:EVENT]->(e)-[:EVENT]->(i) WHERE e.type = "SEND" RETURN i
queryLanguage: Cypher
sort: Node
edgeLabel: sent
- predicate:
propertyKeys: []
knownValues: {}
dbLabel: Process
quickQuery:
name: Started By
querySuffix: MATCH (n)<-[:EVENT]-(e)<-[:EVENT]-(p) WHERE e.type = "SPAWN" RETURN p
queryLanguage: Cypher
sort: Node
edgeLabel: parent process
- predicate:
propertyKeys: []
knownValues: {}
dbLabel: Process
quickQuery:
name: Started Other Process
querySuffix: MATCH (n)-[:EVENT]->(e)-[:EVENT]->(p) WHERE e.type = "SPAWN" RETURN p
queryLanguage: Cypher
sort: Node
edgeLabel: child process
- predicate:
propertyKeys: []
knownValues: {}
dbLabel: IP
quickQuery:
name: Network Send
querySuffix: MATCH (n)-[:NET_TRAFFIC]->(net) RETURN net
queryLanguage: Cypher
sort: Node
- predicate:
propertyKeys: []
knownValues: {}
dbLabel: IP
quickQuery:
name: Network Receive
querySuffix: MATCH (n)<-[:NET_TRAFFIC]-(net) RETURN net
queryLanguage: Cypher
sort: Node
- predicate:
propertyKeys: []
knownValues: {}
dbLabel: IP
quickQuery:
name: Network Communication
querySuffix: MATCH (n)-[:NET_TRAFFIC]-(net)-[:NET_TRAFFIC]-(ip) RETURN ip
queryLanguage: Cypher
sort: Node
edgeLabel: Communication
sampleQueries: []