Improved malware scan. Added server and user purge

This commit is contained in:
Marcel Baumgartner
2023-08-28 05:08:52 +02:00
parent 95ba81eab4
commit 6972c2bb32
11 changed files with 176 additions and 94 deletions

View File

@@ -9,7 +9,7 @@ public class FakePlayerPluginScan : MalwareScan
public override string Name => "Fake player plugin scan"; public override string Name => "Fake player plugin scan";
public override string Description => "This scan is a simple fake player plugin scan provided by moonlight"; public override string Description => "This scan is a simple fake player plugin scan provided by moonlight";
public override async Task<MalwareScanResult[]> Scan(Server server, IServiceProvider serviceProvider) public override async Task<MalwareScanResult?> Scan(Server server, IServiceProvider serviceProvider)
{ {
var serverService = serviceProvider.GetRequiredService<ServerService>(); var serverService = serviceProvider.GetRequiredService<ServerService>();
var access = await serverService.CreateFileAccess(server, null!); var access = await serverService.CreateFileAccess(server, null!);
@@ -24,19 +24,16 @@ public class FakePlayerPluginScan : MalwareScan
{ {
if (fileElement.Name.ToLower().Contains("fakeplayer")) if (fileElement.Name.ToLower().Contains("fakeplayer"))
{ {
return new[] return new()
{ {
new MalwareScanResult Title = "Fake player plugin",
{ Description = $"Suspicious plugin file: {fileElement.Name}",
Title = "Fake player plugin", Author = "Marcel Baumgartner"
Description = $"Suspicious plugin file: {fileElement.Name}",
Author = "Marcel Baumgartner"
}
}; };
} }
} }
} }
return Array.Empty<MalwareScanResult>(); return null;
} }
} }

View File

@@ -9,7 +9,7 @@ public class MinerJarScan : MalwareScan
public override string Name => "Miner jar scan"; public override string Name => "Miner jar scan";
public override string Description => "This scan is a simple miner jar scan provided by moonlight"; public override string Description => "This scan is a simple miner jar scan provided by moonlight";
public override async Task<MalwareScanResult[]> Scan(Server server, IServiceProvider serviceProvider) public override async Task<MalwareScanResult?> Scan(Server server, IServiceProvider serviceProvider)
{ {
var serverService = serviceProvider.GetRequiredService<ServerService>(); var serverService = serviceProvider.GetRequiredService<ServerService>();
var access = await serverService.CreateFileAccess(server, null!); var access = await serverService.CreateFileAccess(server, null!);
@@ -23,18 +23,15 @@ public class MinerJarScan : MalwareScan
if (fileElements.Any(x => x.Name == "jdk" && !x.IsFile)) if (fileElements.Any(x => x.Name == "jdk" && !x.IsFile))
{ {
return new[] return new()
{ {
new MalwareScanResult Title = "Found Miner",
{ Description = "Detected suspicious library directory which may contain a script for miners",
Title = "Found Miner", Author = "Marcel Baumgartner"
Description = "Detected suspicious library directory which may contain a script for miners",
Author = "Marcel Baumgartner"
}
}; };
} }
} }
return Array.Empty<MalwareScanResult>(); return null;
} }
} }

View File

@@ -0,0 +1,35 @@
using Moonlight.App.Database.Entities;
using Moonlight.App.Models.Misc;
using Moonlight.App.Services;
namespace Moonlight.App.MalwareScans;
public class MinerScan : MalwareScan
{
public override string Name => "Miner (NEZHA)";
public override string Description => "Probably a miner";
public override async Task<MalwareScanResult?> Scan(Server server, IServiceProvider serviceProvider)
{
var serverService = serviceProvider.GetRequiredService<ServerService>();
var access = await serverService.CreateFileAccess(server, null!);
var files = await access.Ls();
foreach (var file in files.Where(x => x.IsFile && x.Name.EndsWith(".sh")))
{
var content = await access.Read(file);
if (content.ToLower().Contains("nezha"))
{
return new()
{
Title = "Miner",
Description = "Miner start script (NEZHA)",
Author = "Marcel Baumgartner"
};
}
}
return null;
}
}

View File

@@ -9,7 +9,7 @@ public class SelfBotCodeScan : MalwareScan
public override string Name => "Selfbot code scan"; public override string Name => "Selfbot code scan";
public override string Description => "This scan is a simple selfbot code scan provided by moonlight"; public override string Description => "This scan is a simple selfbot code scan provided by moonlight";
public override async Task<MalwareScanResult[]> Scan(Server server, IServiceProvider serviceProvider) public override async Task<MalwareScanResult?> Scan(Server server, IServiceProvider serviceProvider)
{ {
var serverService = serviceProvider.GetRequiredService<ServerService>(); var serverService = serviceProvider.GetRequiredService<ServerService>();
var access = await serverService.CreateFileAccess(server, null!); var access = await serverService.CreateFileAccess(server, null!);
@@ -21,18 +21,15 @@ public class SelfBotCodeScan : MalwareScan
if (rawScript.Contains("https://discord.com/api") && !rawScript.Contains("https://discord.com/api/oauth2") && !rawScript.Contains("https://discord.com/api/webhook") || rawScript.Contains("https://rblxwild.com")) //TODO: Export to plugins, add regex for checking if (rawScript.Contains("https://discord.com/api") && !rawScript.Contains("https://discord.com/api/oauth2") && !rawScript.Contains("https://discord.com/api/webhook") || rawScript.Contains("https://rblxwild.com")) //TODO: Export to plugins, add regex for checking
{ {
return new[] return new MalwareScanResult
{ {
new MalwareScanResult Title = "Potential selfbot",
{ Description = $"Suspicious script file: {script.Name}",
Title = "Potential selfbot", Author = "Marcel Baumgartner"
Description = $"Suspicious script file: {script.Name}",
Author = "Marcel Baumgartner"
}
}; };
} }
} }
return Array.Empty<MalwareScanResult>(); return null;
} }
} }

View File

@@ -9,7 +9,7 @@ public class SelfBotScan : MalwareScan
public override string Name => "Selfbot Scan"; public override string Name => "Selfbot Scan";
public override string Description => "This scan is a simple selfbot scan provided by moonlight"; public override string Description => "This scan is a simple selfbot scan provided by moonlight";
public override async Task<MalwareScanResult[]> Scan(Server server, IServiceProvider serviceProvider) public override async Task<MalwareScanResult?> Scan(Server server, IServiceProvider serviceProvider)
{ {
var serverService = serviceProvider.GetRequiredService<ServerService>(); var serverService = serviceProvider.GetRequiredService<ServerService>();
var access = await serverService.CreateFileAccess(server, null!); var access = await serverService.CreateFileAccess(server, null!);
@@ -17,17 +17,14 @@ public class SelfBotScan : MalwareScan
if (fileElements.Any(x => x.Name == "tokens.txt")) if (fileElements.Any(x => x.Name == "tokens.txt"))
{ {
return new[] return new MalwareScanResult
{ {
new MalwareScanResult Title = "Found SelfBot",
{ Description = "Detected suspicious 'tokens.txt' file which may contain tokens for a selfbot",
Title = "Found SelfBot", Author = "Marcel Baumgartner"
Description = "Detected suspicious 'tokens.txt' file which may contain tokens for a selfbot",
Author = "Marcel Baumgartner"
}
}; };
} }
return Array.Empty<MalwareScanResult>(); return null;
} }
} }

View File

@@ -6,5 +6,5 @@ public abstract class MalwareScan
{ {
public abstract string Name { get; } public abstract string Name { get; }
public abstract string Description { get; } public abstract string Description { get; }
public abstract Task<MalwareScanResult[]> Scan(Server server, IServiceProvider serviceProvider); public abstract Task<MalwareScanResult?> Scan(Server server, IServiceProvider serviceProvider);
} }

View File

@@ -15,7 +15,7 @@ public class MalwareBackgroundScanService
public bool IsRunning => !ScanTask?.IsCompleted ?? false; public bool IsRunning => !ScanTask?.IsCompleted ?? false;
public bool ScanAllServers { get; set; } public bool ScanAllServers { get; set; }
public readonly Dictionary<Server, MalwareScanResult[]> ScanResults; public readonly Dictionary<Server, MalwareScanResult> ScanResults;
public string Status { get; private set; } = "N/A"; public string Status { get; private set; } = "N/A";
private Task? ScanTask; private Task? ScanTask;
@@ -70,16 +70,16 @@ public class MalwareBackgroundScanService
Status = $"[{i} / {servers.Length}] Scanning server {server.Name}"; Status = $"[{i} / {servers.Length}] Scanning server {server.Name}";
await Event.Emit("malwareScan.status", IsRunning); await Event.Emit("malwareScan.status", IsRunning);
var results = await malwareScanService.Perform(server); var result = await malwareScanService.Perform(server);
if (results.Any()) if (result != null)
{ {
lock (ScanResults) lock (ScanResults)
{ {
ScanResults.Add(server, results); ScanResults.Add(server, result);
} }
await Event.Emit("malwareScan.result"); await Event.Emit("malwareScan.result", server);
} }
i++; i++;

View File

@@ -5,7 +5,7 @@ using Moonlight.App.Services.Plugins;
namespace Moonlight.App.Services; namespace Moonlight.App.Services;
public class MalwareScanService //TODO: Make this moddable using plugins public class MalwareScanService
{ {
private readonly PluginService PluginService; private readonly PluginService PluginService;
private readonly IServiceScopeFactory ServiceScopeFactory; private readonly IServiceScopeFactory ServiceScopeFactory;
@@ -16,34 +16,29 @@ public class MalwareScanService //TODO: Make this moddable using plugins
ServiceScopeFactory = serviceScopeFactory; ServiceScopeFactory = serviceScopeFactory;
} }
public async Task<MalwareScanResult[]> Perform(Server server) public async Task<MalwareScanResult?> Perform(Server server)
{ {
var defaultScans = new List<MalwareScan> var defaultScans = new List<MalwareScan>
{ {
new SelfBotScan(), new SelfBotScan(),
new MinerJarScan(), new MinerJarScan(),
new SelfBotCodeScan(), new SelfBotCodeScan(),
new FakePlayerPluginScan() new FakePlayerPluginScan(),
new MinerScan()
}; };
var scans = await PluginService.BuildMalwareScans(defaultScans.ToArray()); var scans = await PluginService.BuildMalwareScans(defaultScans.ToArray());
var results = new List<MalwareScanResult>();
using var scope = ServiceScopeFactory.CreateScope(); using var scope = ServiceScopeFactory.CreateScope();
foreach (var scan in scans) foreach (var scan in scans)
{ {
var result = await scan.Scan(server, scope.ServiceProvider); var result = await scan.Scan(server, scope.ServiceProvider);
if (result.Any()) if (result != null)
{ return result;
foreach (var scanResult in result)
{
results.Add(scanResult);
}
}
} }
return results.ToArray(); return null;
} }
} }

View File

@@ -113,19 +113,17 @@ public class ServerService
if (ConfigService.Get().Moonlight.Security.MalwareCheckOnStart && signal == PowerSignal.Start || if (ConfigService.Get().Moonlight.Security.MalwareCheckOnStart && signal == PowerSignal.Start ||
signal == PowerSignal.Restart) signal == PowerSignal.Restart)
{ {
var results = await new MalwareScanService( var result = await new MalwareScanService(
PluginService, PluginService,
ServiceScopeFactory ServiceScopeFactory
).Perform(server); ).Perform(server);
if (results.Any()) if (result != null)
{ {
var resultText = string.Join(" ", results.Select(x => x.Title)); Logger.Warn($"Found malware on server {server.Uuid}. Result: " + result.Title);
Logger.Warn($"Found malware on server {server.Uuid}. Results: " + resultText);
throw new DisplayException( throw new DisplayException(
$"Unable to start server. Found following malware on this server: {resultText}. Please contact the support if you think this detection is a false positive", $"Unable to start server. Found following malware on this server: {result.Title}. Please contact the support if you think this detection is a false positive",
true); true);
} }
} }

View File

@@ -4,12 +4,23 @@
@using Moonlight.App.Services.Background @using Moonlight.App.Services.Background
@using Moonlight.App.Services @using Moonlight.App.Services
@using BlazorTable @using BlazorTable
@using Microsoft.EntityFrameworkCore
@using Moonlight.App.ApiClients.Wings
@using Moonlight.App.Database.Entities @using Moonlight.App.Database.Entities
@using Moonlight.App.Events @using Moonlight.App.Events
@using Moonlight.App.Helpers
@using Moonlight.App.Models.Misc @using Moonlight.App.Models.Misc
@using Moonlight.App.Repositories
@using Moonlight.App.Services.Interop
@using Moonlight.App.Services.Sessions
@inject MalwareBackgroundScanService MalwareBackgroundScanService @inject MalwareBackgroundScanService MalwareBackgroundScanService
@inject SmartTranslateService SmartTranslateService @inject SmartTranslateService SmartTranslateService
@inject ServerService ServerService
@inject ToastService ToastService
@inject SessionServerService SessionServerService
@inject Repository<Server> ServerRepository
@inject Repository<User> UserRepository
@inject EventSystem Event @inject EventSystem Event
@implements IDisposable @implements IDisposable
@@ -38,7 +49,7 @@
} }
else else
{ {
<div class="mb-3"> <div class="mb-5">
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="scanAllServers" @bind="MalwareBackgroundScanService.ScanAllServers"> <input class="form-check-input" type="checkbox" id="scanAllServers" @bind="MalwareBackgroundScanService.ScanAllServers">
<label class="form-check-label" for="scanAllServers"> <label class="form-check-label" for="scanAllServers">
@@ -48,9 +59,14 @@
</div> </div>
<WButton Text="@(SmartTranslateService.Translate("Start scan"))" <WButton Text="@(SmartTranslateService.Translate("Start scan"))"
CssClasses="btn-success" CssClasses="btn-success me-3"
OnClick="MalwareBackgroundScanService.Start"> OnClick="MalwareBackgroundScanService.Start">
</WButton> </WButton>
<WButton Text="@(SmartTranslateService.Translate("Purge page"))"
CssClasses="btn-danger"
OnClick="PurgeSelected">
</WButton>
} }
</div> </div>
</div> </div>
@@ -65,39 +81,14 @@
<div class="card-body"> <div class="card-body">
<LazyLoader @ref="LazyLoaderResults" Load="LoadResults"> <LazyLoader @ref="LazyLoaderResults" Load="LoadResults">
<div class="table-responsive"> <div class="table-responsive">
<Table TableItem="Server" Items="ScanResults.Keys" PageSize="25" TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3" TableHeadClass="fw-bold text-muted"> <Table @ref="Table" TableItem="Server" Items="ScanResults.Keys" PageSize="25" TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3" TableHeadClass="fw-bold text-muted">
<Column TableItem="Server" Title="@(SmartTranslateService.Translate("Server"))" Field="@(x => x.Id)" Sortable="false" Filterable="false"> <Column TableItem="Server" Title="@(SmartTranslateService.Translate("Server"))" Field="@(x => x.Id)" Sortable="false" Filterable="false">
<Template> <Template>
<a href="/server/@(context.Uuid)">@(context.Name)</a> <a href="/server/@(context.Uuid)">@(context.Name)</a>
</Template> </Template>
</Column> </Column>
<Column TableItem="Server" Title="@(SmartTranslateService.Translate("Results"))" Field="@(x => x.Id)" Sortable="false" Filterable="false"> <Column TableItem="Server" Title="@(SmartTranslateService.Translate("Title"))" Field="@(x => ScanResults[x].Title)" Sortable="false" Filterable="true" />
<Template> <Column TableItem="Server" Title="@(SmartTranslateService.Translate("Description"))" Field="@(x => ScanResults[x].Description)" Sortable="false" Filterable="true" />
<div class="row">
@foreach (var result in ScanResults[context])
{
<div class="col-12 col-md-6 p-3">
<div class="accordion" id="scanResult@(result.GetHashCode())">
<div class="accordion-item">
<h2 class="accordion-header" id="scanResult-header@(result.GetHashCode())">
<button class="accordion-button fs-4 fw-semibold collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#scanResult-body@(result.GetHashCode())" aria-expanded="false" aria-controls="scanResult-body@(result.GetHashCode())">
<span>@(result.Title)</span>
</button>
</h2>
<div id="scanResult-body@(result.GetHashCode())" class="accordion-collapse collapse" aria-labelledby="scanResult-header@(result.GetHashCode())" data-bs-parent="#scanResult">
<div class="accordion-body">
<p>
@(result.Description)
</p>
</div>
</div>
</div>
</div>
</div>
}
</div>
</Template>
</Column>
<Pager ShowPageNumber="true" ShowTotalCount="true"/> <Pager ShowPageNumber="true" ShowTotalCount="true"/>
</Table> </Table>
</div> </div>
@@ -109,7 +100,9 @@
@code @code
{ {
private readonly Dictionary<Server, MalwareScanResult[]> ScanResults = new(); private readonly Dictionary<Server, MalwareScanResult> ScanResults = new();
private Table<Server> Table;
private LazyLoader LazyLoaderResults; private LazyLoader LazyLoaderResults;
@@ -117,7 +110,15 @@
{ {
await Event.On<Object>("malwareScan.status", this, async o => { await InvokeAsync(StateHasChanged); }); await Event.On<Object>("malwareScan.status", this, async o => { await InvokeAsync(StateHasChanged); });
await Event.On<Object>("malwareScan.result", this, async o => { await LazyLoaderResults.Reload(); }); await Event.On<Server>("malwareScan.result", this, async server =>
{
lock (MalwareBackgroundScanService.ScanResults)
{
ScanResults.Add(server, MalwareBackgroundScanService.ScanResults[server]);
}
await InvokeAsync(StateHasChanged);
});
} }
private Task LoadResults(LazyLoader arg) private Task LoadResults(LazyLoader arg)
@@ -140,4 +141,69 @@
await Event.Off("malwareScan.status", this); await Event.Off("malwareScan.status", this);
await Event.Off("malwareScan.result", this); await Event.Off("malwareScan.result", this);
} }
private async Task PurgeSelected()
{
int users = 0;
int servers = 0;
int allServersCount = Table.FilteredItems.Count();
int position = 0;
await ToastService.CreateProcessToast("purgeProcess", "Purging");
foreach (var item in Table.FilteredItems)
{
position++;
if (item == null)
continue;
try
{
var server = ServerRepository.Get()
.Include(x => x.Owner)
.FirstOrDefault(x => x.Id == item.Id);
if(server == null)
continue;
await ToastService.UpdateProcessToast("purgeProcess", $"[{position}/{allServersCount}] {server.Name}");
ScanResults.Remove(item);
await InvokeAsync(StateHasChanged);
// Owner
server.Owner.Status = UserStatus.Banned;
UserRepository.Update(server.Owner);
users++;
try
{
await SessionServerService.ReloadUserSessions(server.Owner);
}
catch (Exception) {/* Ignored */}
// Server itself
await ServerService.SetPowerState(server, PowerSignal.Kill);
await ServerService.Delete(server);
servers++;
}
catch (Exception e)
{
Logger.Warn($"Error purging server: {item.Uuid}");
Logger.Warn(e);
await ToastService.Error(
$"Failed to purge server '{item.Name}': {e.Message}"
);
}
}
await ToastService.RemoveProcessToast("purgeProcess");
await ToastService.Success($"Successfully purged {servers} servers by {users} users");
}
} }

View File

@@ -190,7 +190,7 @@
.Include(x => x.Variables) .Include(x => x.Variables)
.Include(x => x.MainAllocation) .Include(x => x.MainAllocation)
.Include(x => x.Owner) .Include(x => x.Owner)
.First(x => x.Uuid == uuid); .FirstOrDefault(x => x.Uuid == uuid);
if (CurrentServer != null) if (CurrentServer != null)
{ {