Query Details

LOTS Usage

Query

# Living Off Trusted Sites

## Query Information

#### Description
The Living Off Trusted Sites protject is included in the queries below. The project is about: *Attackers are using popular legitimate domains when conducting phishing, C&C, exfiltration and downloading tools to evade detection. The list of websites below allow attackers to use their domain or subdomain.* The query below can be used to hunt for websites which are rare in your organization or are executed by rare *InitiatingFiles*. This query can be used to list all found LOTS domains and how often they are executed, this can serve as input for further investigation or as start for you threat hunting case.

Due to the amount of legitimate traffic this query will likely result in a lot of false positives, therefor is it imporant to look at rare sightings rather then the most common once. Therefor this query only outputs statistics which can be used to build further queries. To limit the false positives, or put websites out of scope, the *WhitelistedDomains* can be used to whitelist domains.

**THIS SHOULD NOT BE USED AS DETECTION RULE**

This qeury levarages a txt file that is located in my repository, however the source of that file is: [lots-project.com](lots-project.com) from [@mrd0x](https://twitter.com/mrd0x).

#### Risk
An actor uses Living Off Trusted Sites to host their malicious infrastructure

#### References
- https://lots-project.com/


## Defender For Endpoint
```
// THIS QUERY IS ONLY FOR HUNTING, NOT FOR DETECTION. IT WILL GENERATE TO MUCH FPs.
// The query levarages the Living Off Trusted Sites from: https://lots-project.com/
let LOTS = externaldata(Data:string )[@"https://raw.githubusercontent.com/Bert-JanP/Hunting-Queries-Detection-Rules/main/Defender%20For%20Endpoint/Living%20Off%20The%20Land/lots-project.txt"] with (format="txt", ignoreFirstRecord=True);
// To finetune your hunt the whitelist below can be used. Use lowercase for the has_any to work.
let WhitelistedDomains = dynamic(['yourdomain.com', 'yoursharepoint.sharepoint.com']);
// Parse Input into Fields
let LotsParsed = LOTS
| where not(Data startswith "#")
| extend Fields = split(Data, ",")
| extend Website = Fields[0], Tags = Fields[1], ServiceProvider = Fields[2];
// Collect all unique domains for better performance.
let LotsDomains = LotsParsed
// Parse Websites to Lower to ensure that has_any can be used.
| extend WebsiteToLower = tolower(tostring(Website))
| distinct WebsiteToLower;
DeviceNetworkEvents
| where RemoteUrl has_any (LotsDomains)
// Filter whitelist.
| where not(RemoteUrl has_any (WhitelistedDomains))
// All upcomming lines below are used to create results which can be valueble. There is some explanation, but limited. If you want more info send me a message.
// The lines below are used to extract the domain -> <domain>.<tld> for good statistics.
// Method 1:
// Parse URL to get the host, if this can be parsed. Otherwise method 2 is used.
| extend ParseURL = parse_url(RemoteUrl)
| extend DomainParseURL = tostring(parse_json(ParseURL).Host)
// Method 2:
// for all domains that could not be parsed via the parseURL
| extend SplitUrl = split(RemoteUrl, ".")
// Combine splitted results and merge results from method one into the domain field.
| extend DomainParsed = iff(isempty(DomainParseURL), tostring(strcat( SplitUrl[array_length(SplitUrl) - 2] , ".", SplitUrl[array_length(SplitUrl) -1])), "na")
| extend SplitDomainParseURL = split(DomainParseURL, ".")
| extend Domain = iff(DomainParsed == "na", tostring(strcat( SplitDomainParseURL[array_length(SplitDomainParseURL) - 2] , ".", SplitDomainParseURL[array_length(SplitDomainParseURL) -1])), DomainParsed)
// Normal query continues, parsing is over.
// Summarize results. Based on the summarize you can investigate further.
| summarize TotalCount = count(), URLs = make_set(RemoteUrl), Devices = make_set(DeviceName), RemotePorts = make_set(RemotePort), InitiatingFiles = make_set(InitiatingProcessFileName) by Domain
// Some additional field for statistics
| extend TotalDevices = array_length(Devices), UniqueURLs = array_length(URLs), TotalInitiatingFiles = array_length(InitiatingFiles)
| sort by TotalCount
// Project all fields
| project Domain, TotalCount, UniqueURLs, TotalDevices, TotalInitiatingFiles, RemotePorts, URLs, Devices, InitiatingFiles
```
## Sentinel
```
// THIS QUERY IS ONLY FOR HUNTING, NOT FOR DETECTION. IT WILL GENERATE TO MUCH FPs.
// The query levarages the Living Off Trusted Sites from: https://lots-project.com/
let LOTS = externaldata(Data:string )[@"https://raw.githubusercontent.com/Bert-JanP/Hunting-Queries-Detection-Rules/main/Defender%20For%20Endpoint/Living%20Off%20The%20Land/lots-project.txt"] with (format="txt", ignoreFirstRecord=True);
// To finetune your hunt the whitelist below can be used. Use lowercase for the has_any to work.
let WhitelistedDomains = dynamic(['yourdomain.com', 'yoursharepoint.sharepoint.com']);
// Parse Input into Fields
let LotsParsed = LOTS
| where not(Data startswith "#")
| extend Fields = split(Data, ",")
| extend Website = Fields[0], Tags = Fields[1], ServiceProvider = Fields[2];
// Collect all unique domains for better performance.
let LotsDomains = LotsParsed
// Parse Websites to Lower to ensure that has_any can be used.
| extend WebsiteToLower = tolower(tostring(Website))
| distinct WebsiteToLower;
DeviceNetworkEvents
| where RemoteUrl has_any (LotsDomains)
// Filter whitelist.
| where not(RemoteUrl has_any (WhitelistedDomains))
// All upcomming lines below are used to create results which can be valueble. There is some explanation, but limited. If you want more info send me a message.
// The lines below are used to extract the domain -> <domain>.<tld> for good statistics.
// Method 1:
// Parse URL to get the host, if this can be parsed. Otherwise method 2 is used.
| extend ParseURL = parse_url(RemoteUrl)
| extend DomainParseURL = tostring(parse_json(ParseURL).Host)
// Method 2:
// for all domains that could not be parsed via the parseURL
| extend SplitUrl = split(RemoteUrl, ".")
// Combine splitted results and merge results from method one into the domain field.
| extend DomainParsed = iff(isempty(DomainParseURL), tostring(strcat( SplitUrl[array_length(SplitUrl) - 2] , ".", SplitUrl[array_length(SplitUrl) -1])), "na")
| extend SplitDomainParseURL = split(DomainParseURL, ".")
| extend Domain = iff(DomainParsed == "na", tostring(strcat( SplitDomainParseURL[array_length(SplitDomainParseURL) - 2] , ".", SplitDomainParseURL[array_length(SplitDomainParseURL) -1])), DomainParsed)
// Normal query continues, parsing is over.
// Summarize results. Based on the summarize you can investigate further.
| summarize TotalCount = count(), URLs = make_set(RemoteUrl), Devices = make_set(DeviceName), RemotePorts = make_set(RemotePort), InitiatingFiles = make_set(InitiatingProcessFileName) by Domain
// Some additional field for statistics
| extend TotalDevices = array_length(Devices), UniqueURLs = array_length(URLs), TotalInitiatingFiles = array_length(InitiatingFiles)
| sort by TotalCount
// Project all fields
| project Domain, TotalCount, UniqueURLs, TotalDevices, TotalInitiatingFiles, RemotePorts, URLs, Devices, InitiatingFiles
```

Explanation

The query is used for hunting purposes, not for detection. It leverages the Living Off Trusted Sites (LOTS) project from lots-project.com. The query hunts for websites that are rare in the organization or are executed by rare InitiatingFiles. It lists all found LOTS domains and how often they are executed, which can be used for further investigation or threat hunting. The query should not be used as a detection rule. WhitelistedDomains can be used to filter out certain domains. The query retrieves data from DeviceNetworkEvents and summarizes the results to provide statistics such as total count, unique URLs, total devices, total initiating files, remote ports, URLs, devices, and initiating files.

Details

Bert-Jan Pals profile picture

Bert-Jan Pals

Released: June 13, 2023

Tables

LOTS

Keywords

Keywords:LOTS,WhitelistedDomains,LotsParsed,LotsDomains,DeviceNetworkEvents,RemoteUrl,WhitelistedDomains,ParseURL,DomainParseURL,SplitUrl,DomainParsed,SplitDomainParseURL,Domain,TotalCount,URLs,Devices,RemotePorts,InitiatingFiles,TotalDevices,UniqueURLs,TotalInitiatingFiles.

Operators

letexternaldatawithformatignoreFirstRecorddynamicwherenotextendsplitdistinctparse_urlparse_jsoniffarray_lengthsummarizecountmake_setbysortproject

Actions