├── app ├── static │ ├── images │ │ ├── blauhaunt.png │ │ └── font-awesome │ │ │ ├── user.svg │ │ │ ├── desktop.svg │ │ │ └── server.svg │ ├── js │ │ ├── dark-mode-switch.min.js │ │ └── veloAPI.js │ └── css │ │ ├── style.css │ │ └── dark-mode.css └── index.html ├── test_data ├── HostIpMap.csv ├── clientinfo.json └── TestEvents.json ├── LICENSE ├── parser ├── velociraptor │ ├── quick_velo.ps1 │ ├── blauhaunt_client_info.yml │ ├── monitoring_artifact │ ├── velo_artifact_old.yaml │ └── velo_artifact.yaml ├── Defender365_Query.md └── deprecated_powershell │ └── blauhaunt_script.ps1 └── README.md /app/static/images/blauhaunt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgosec/Blauhaunt/HEAD/app/static/images/blauhaunt.png -------------------------------------------------------------------------------- /test_data/HostIpMap.csv: -------------------------------------------------------------------------------- 1 | Host, Ip 2 | a, 10.10.10.1 3 | b, 10.10.10.2 4 | c, 10.10.10.3 5 | wrong, abcd 6 | exclude, bad -------------------------------------------------------------------------------- /test_data/clientinfo.json: -------------------------------------------------------------------------------- 1 | {"os_info": {"hostname": "a", "release": "Windows 10"},"labels": ["Touched", "C2", "CredDumped", "PikaBot"]} 2 | {"os_info": {"hostname": "b", "release": "Windows Server 2016"},"labels": ["Touched"]} -------------------------------------------------------------------------------- /app/static/images/font-awesome/user.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/static/images/font-awesome/desktop.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/static/images/font-awesome/server.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/static/js/dark-mode-switch.min.js: -------------------------------------------------------------------------------- 1 | const darkSwitch = document.getElementById("darkSwitch"); 2 | 3 | function initTheme() { 4 | const e = null !== localStorage.getItem("darkSwitch") && "dark" === localStorage.getItem("darkSwitch"); 5 | darkSwitch.checked = e, e ? document.body.setAttribute("data-theme", "dark") : document.body.removeAttribute("data-theme") 6 | } 7 | 8 | function resetTheme() { 9 | darkSwitch.checked ? (document.body.setAttribute("data-theme", "dark"), localStorage.setItem("darkSwitch", "dark")) : (document.body.removeAttribute("data-theme"), localStorage.removeItem("darkSwitch")) 10 | } 11 | 12 | window.addEventListener("load", () => { 13 | darkSwitch && (initTheme(), darkSwitch.addEventListener("change", () => { 14 | resetTheme() 15 | })) 16 | }); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 cgosec 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 | -------------------------------------------------------------------------------- /parser/velociraptor/quick_velo.ps1: -------------------------------------------------------------------------------- 1 | # script for when you got some event logs and you want to quickly run blauhaunt over it 2 | # uses the blauhaunt velo artifact for data collection 3 | # example of use: .\quick_velo.ps1 -EventLogDirectory C:\Windows\System32\winevt\Logs\ 4 | # will put the resulting json in the directory, from which the script is executed 5 | # will overwrite previous results, if they are not renamed 6 | 7 | [CmdletBinding()] 8 | param( 9 | [Parameter( 10 | Mandatory = $true, 11 | ValueFromPipeline = $true, 12 | ValueFromPipelineByPropertyName = $true, 13 | Position = 1, 14 | HelpMessage = "Directory containing the event logs (evtx)")] 15 | [System.IO.FileInfo] $EventLogDirectory, 16 | [Parameter( 17 | Position = 2, 18 | HelpMessage = "Name of the output file with path (default: .\BlauhauntData.json)")] 19 | [System.IO.FileInfo] $OutfileName = ".\BlauhauntData.json" 20 | ) 21 | 22 | $blauhauntArtifact = (Get-ChildItem . | Where-Object -Property Name -Like "velo_artifact.yaml").FullName 23 | $velociraptorPath = (Get-ChildItem . | Where-Object -Property Name -Like "velociraptor*exe").FullName 24 | "$velociraptorPath artifacts --definitions $blauhauntArtifact collect --format=jsonl Custom.Windows.EventLogs.Blauhaunt --args Security='$EventLogDirectory\Security.evtx' --args System='$EventLogDirectory\System.evtx' --args LocalSessionManager='$EventLogDirectory\Microsoft-Windows-TerminalServices-LocalSessionManager%4Operational.evtx' --args RemoteConnectionManager='$EventLogDirectory\Microsoft-Windows-TerminalServices-RemoteConnectionManager%4Operational.evtx' --args RDPClientOperational='$EventLogDirectory\Microsoft-Windows-TerminalServices-RDPClient%4Operational.evtx'" | Invoke-Expression | Out-File -FilePath $OutfileName 25 | -------------------------------------------------------------------------------- /parser/velociraptor/blauhaunt_client_info.yml: -------------------------------------------------------------------------------- 1 | name: Custom.Server.Monitoring.Blauhaunt.Clients 2 | description: | 3 | This artifact pushes the results of the clients() method to your blauhaunt server. This is needed for having the os info 4 | and lables available in blauhaunt. 5 | 6 | # Can be CLIENT, CLIENT_EVENT, SERVER, SERVER_EVENT 7 | # toDo: make send data as list an not every entry as single event 8 | type: SERVER_EVENT 9 | 10 | parameters: 11 | - name: blauhaunt_url 12 | default: "http://127.0.0.1:8082" 13 | description: url to your blauhaunt server 14 | - name: blauhaunt_token 15 | description: token from your case with write permission 16 | - name: push_interval 17 | description: intervall when the server will push the data in seconds 18 | default: 300 19 | type: int 20 | 21 | 22 | sources: 23 | - precondition: 24 | SELECT OS From info() where OS = 'windows' OR OS = 'linux' OR OS = 'darwin' 25 | 26 | query: | 27 | LET clients = SELECT _value as data FROM items(item={SELECT * FROM clients()}) 28 | LET b_url = blauhaunt_url + "/app/api/v1/push_client_info/" 29 | LET send_massage = SELECT * FROM foreach(row=clients, 30 | query={ 31 | SELECT client_id, Hostname, LastSeen, Content, Response 32 | FROM http_client( 33 | method="POST", 34 | url=b_url, 35 | headers=dict(`Content-Type`="application/json"), 36 | data=serialize(item=dict(case_token=blauhaunt_token, 37 | clients=data), 38 | format="json") 39 | ) 40 | }) 41 | 42 | SELECT * FROM foreach( 43 | row={SELECT * FROM clock(period=push_interval)}, 44 | query=send_massage) 45 | 46 | -------------------------------------------------------------------------------- /app/static/css/style.css: -------------------------------------------------------------------------------- 1 | #loading { 2 | position: absolute; 3 | left: 0; 4 | top: 50%; 5 | width: 100%; 6 | text-align: center; 7 | margin-top: -0.5em; 8 | font-size: 4em; 9 | color: #000; 10 | } 11 | 12 | #loading.loaded { 13 | display: none; 14 | } 15 | 16 | table.floatThead-table { 17 | border-top: none; 18 | border-bottom: none; 19 | background-color: #efefef; 20 | } 21 | 22 | .dropdown-menu { 23 | z-index: 10000; 24 | } 25 | 26 | .custom-checkbox .custom-control-input:checked ~ .custom-control-label::before { 27 | background-color:blue; 28 | } 29 | 30 | .my_svg { 31 | height: 50px; 32 | width: 200px; 33 | } 34 | 35 | .navbar { 36 | z-index: 10000; 37 | } 38 | 39 | .modal { 40 | z-index: 100000; 41 | } 42 | 43 | .tooltip { 44 | z-index: 110000; 45 | } 46 | 47 | .bgcolorSun { 48 | background-color: #ff7f50 !important; 49 | } 50 | 51 | .bgcolorSat { 52 | background-color: #b0c4de !important; 53 | } 54 | 55 | .bgcolorDay { 56 | background-color: #efefef !important; 57 | } 58 | 59 | .bgcolornormal { 60 | background-color: #ffeaee !important; 61 | } 62 | 63 | .bgcolorlow { 64 | background-color: #ffbaee !important; 65 | } 66 | 67 | .bgcolormid { 68 | background-color: #ff8aee !important; 69 | } 70 | 71 | .bgcolorhigh { 72 | background-color: #ff5aee !important; 73 | } 74 | 75 | .login-form { 76 | max-width: none !important; 77 | width: 415px; 78 | } 79 | 80 | .login-form input#username, input#password, input#password1, input#password2, input#case { 81 | max-width: none !important; 82 | width: 370px; 83 | height: 2.4em; 84 | padding: 0 16px; 85 | border-radius: 4px; 86 | border: none; 87 | box-shadow: 0 0 0 1px #ccc inset; 88 | appearance: none; 89 | -webkit-appearance: none; 90 | -moz-appearance: none; 91 | } 92 | 93 | .drop-hover:hover > .dropdown-menu { 94 | display: block !important; 95 | margin-left: 100%; 96 | margin-top: -22%; 97 | } 98 | -------------------------------------------------------------------------------- /parser/Defender365_Query.md: -------------------------------------------------------------------------------- 1 | # Query 2 | let starttime = datetime("1970-01-01T00:00:00.0000000Z"); 3 | let endtime = datetime("2270-01-01T00:00:00.0000000Z"); 4 | let exclude_longon_types = dynamic(["Batch", "Interactive", "Unlock", "Service"]); 5 | DeviceLogonEvents 6 | | where Timestamp between (starttime .. endtime) 7 | | where AccountName !startswith "umfd" and AccountName !startswith "dwm" and AccountName !endswith "$" 8 | | where LogonType !in (exclude_longon_types) 9 | | order by Timestamp asc 10 | | summarize LogonTimes = make_list(Timestamp) by AccountName, DeviceName, RemoteIP, RemoteDeviceName, LogonType, Protocol, InitiatingProcessFileName 11 | | extend Distinction=strcat(InitiatingProcessFileName, " Protocol: " , Protocol) 12 | | project-rename UserName=AccountName, Destination=DeviceName, EventID=LogonType, SourceIP=RemoteIP, SourceHostname=RemoteDeviceName 13 | | extend LogonCount="", SID="" 14 | | extend BlauhauntData = pack_all() 15 | | project BlauhauntData 16 | 17 | 18 | Simply export it and upload it to Blauhaunt... 19 | 20 | (Likely you have to chunk by using the starttime and endtime variable since Denfender is limited to 10k lines of export *measly*) 21 | 22 | If the splitting by Datetime does not suite your needs, you can extend the query like this to do some kind of pageination: 23 | 24 | let exclude_longon_types = dynamic(["Batch", "Interactive", "Unlock", "Service", "Unknown"]); 25 | let row_start=0; 26 | let row_end=1000; 27 | let FullData= 28 | DeviceLogonEvents 29 | | where (AccountName contains "-a" or AccountName contains "external" or InitiatingProcessAccountUpn contains "-a" or InitiatingProcessAccountUpn contains "external") 30 | | where LogonType !in (exclude_longon_types) 31 | | order by Timestamp asc 32 | | summarize LogonTimes = make_list(Timestamp) by AccountName, DeviceName, RemoteIP, RemoteDeviceName, LogonType, Protocol, InitiatingProcessFileName 33 | | extend Distinction=strcat(InitiatingProcessFileName, " Protocol: " , Protocol) 34 | | project-rename UserName=AccountName, Destination=DeviceName, EventID=LogonType, SourceIP=RemoteIP, SourceHostname=RemoteDeviceName 35 | | extend LogonCount="", SID="" 36 | | extend BlauhauntData = pack_all() 37 | | project BlauhauntData 38 | | serialize 39 | | extend row = row_number() 40 | | where row between(row_start..row_end); 41 | FullData 42 | | project BlauhauntData 43 | 44 | *simply change row_start and row_end as you need it* 45 | 46 | # IP to Host Mapping: 47 | DeviceLogonEvents 48 | | where RemoteDeviceName != "" 49 | | where RemoteIP !startswith "127." 50 | | where RemoteIP !in ("", "-") 51 | | project RemoteDeviceName, RemoteIP 52 | | summarize by RemoteDeviceName, RemoteIP 53 | 54 | Import this into last input field: 55 |  56 | -------------------------------------------------------------------------------- /test_data/TestEvents.json: -------------------------------------------------------------------------------- 1 | {"LogonTimes":["2018-09-07T23:14:55Z","2018-09-04T17:27:14Z","2018-09-04T16:18:13Z"],"UserName":"user_a","SID":"S-1-5-21-00000-2530590580-3149308974-1184","Destination":"a","Description":"","Distinction": "Test1", "EventID":4624,"LogonType":3,"SourceIP":"10.10.10.2","SourceHostname":"-","LogonCount":3} 2 | {"LogonTimes":["2018-09-05T23:14:55Z","2018-09-06T17:27:14Z","2018-09-08T16:18:13Z"],"UserName":"user_b","SID":"S-1-5-21-00000-2530590580-3149308974-1185","Destination":"a","Description":"","Distinction": "Test2", "EventID":4624,"LogonType":3,"SourceIP":"10.10.10.10","SourceHostname":"z","LogonCount":3} 3 | {"LogonTimes":["2018-09-08T23:14:55Z","2018-09-08T17:27:14Z","2018-09-07T23:18:13Z"],"UserName":"user_c","SID":"S-1-5-21-00000-2530590580-3149308974-1185","Destination":"b","Description":"","Distinction": "Test3", "EventID":4624,"LogonType":3,"SourceIP":"10.10.10.10","SourceHostname":"z","LogonCount":3} 4 | {"LogonTimes":["2018-09-08T23:14:55Z","2018-09-08T17:27:14Z","2018-09-07T16:18:13Z"],"UserName":"user_a","SID":"S-1-5-21-00000-2530590580-3149308974-1185","Destination":"c","Description":"","Distinction": "Test1", "EventID":4624,"LogonType":3,"SourceIP":"10.10.10.11","SourceHostname":"d","LogonCount":3} 5 | {"LogonTimes":["2018-09-08T23:14:55Z","2018-09-08T17:27:14Z","2018-09-10T16:18:13Z"],"UserName":"user_b","SID":"S-1-5-21-00000-2530590580-3149308974-1185","Destination":"d","Description":"","Distinction": "Test2", "EventID":4624,"LogonType":3,"SourceIP":"10.10.10.5","SourceHostname":"c","LogonCount":3} 6 | {"LogonTimes":["2018-09-08T23:14:55Z","2018-09-08T17:26:14Z","2018-09-08T02:18:13Z"],"UserName":"user_c","SID":"S-1-5-21-00000-2530590580-3149308974-1185","Destination":"a","Description":"","Distinction": "Test3", "EventID":4624,"LogonType":3,"SourceIP":"10.10.10.3","SourceHostname":"-","LogonCount":3} 7 | {"LogonTimes":["2018-09-08T23:14:55Z","2018-09-08T07:27:14Z","2018-09-08T16:18:13Z"],"UserName":"user_b","SID":"S-1-5-21-00000-2530590580-3149308974-1185","Destination":"d","Description":"","Distinction": "Test1", "EventID":4625,"LogonType":3,"SourceIP":"10.10.10.10","SourceHostname":"z","LogonCount":3} 8 | {"LogonTimes":["2018-09-05T23:14:55Z","2018-09-07T17:27:14Z","2018-09-08T16:18:13Z"],"UserName":"user_b","SID":"S-1-5-21-00000-2530590580-3149308974-1185","Destination":"z","Description":"","Distinction": "Test2", "EventID":4625,"LogonType":3,"SourceIP":"10.10.10.1","SourceHostname":"-","LogonCount":3} 9 | {"LogonTimes":["2018-09-05T23:14:55Z","2018-09-07T17:27:14Z","2018-09-08T16:18:13Z"],"UserName":"user_b","SID":"S-1-5-21-00000-2530590580-3149308974-1185","Destination":"z","Description":"","Distinction": "Test3", "EventID":4625,"LogonType":3,"SourceIP":"10.10.10.4","SourceHostname":"q","LogonCount":3} 10 | {"LogonTimes":["2018-09-04T19:14:55Z","2018-09-07T22:27:14Z","2018-09-08T23:18:13Z"],"UserName":"user_f","SID":"S-1-5-21-00000-2530590580-3149308974-1185","Destination":"z","Description":"","Distinction": "Test1", "EventID":4624,"LogonType":3,"SourceIP":"10.10.10.4","SourceHostname":"not_dis","LogonCount":3} 11 | {"LogonTimes":["2018-09-09T19:14:55Z","2018-09-09T22:27:14Z","2018-09-10T23:18:13Z"],"UserName":"boss_admin","SID":"S-1-5-21-00000-2530590580-3149308974-1185","Destination":"z","Description":"","Distinction": "Test2", "EventID":4624,"LogonType":3,"SourceIP":"10.10.10.4","SourceHostname":"-","LogonCount":3} 12 | {"LogonTimes":["2018-09-09T19:14:55Z","2018-09-09T22:27:14Z","2018-12-10T23:18:13Z"],"UserName":"boss_admin","SID":"S-1-5-21-00000-2530590580-3149308974-1185","Destination":"z","Description":"", "Distinction": "Test3", "EventID":4624,"LogonType":3,"SourceIP":"10.10.10.4","SourceHostname":"-","LogonCount":3} 13 | {"LogonTimes":["2018-09-09T19:14:55Z","2018-09-09T22:27:14Z","2018-12-10T23:18:13Z"],"UserName":"user_with_no_system","SID":"S-1-5-21-00000-2530590580-3149308974-1185","Destination":"z","Description":"", "Distinction": "Test1","EventID":4624,"LogonType":3,"SourceIP":"-","SourceHostname":"-","LogonCount":3} -------------------------------------------------------------------------------- /app/static/css/dark-mode.css: -------------------------------------------------------------------------------- 1 | [data-theme="dark"] { 2 | background-color: #202020; 3 | color: #eee; 4 | } 5 | 6 | [data-theme="dark"] .bg-light { 7 | background-color: #292929 !important; 8 | } 9 | 10 | [data-theme="dark"] .bg-white { 11 | background-color: #000; 12 | } 13 | 14 | [data-theme="dark"] .bg-black { 15 | background-color: #eee; 16 | } 17 | 18 | [data-theme="dark"] .list-group-item-light.list-group-item-action:focus { 19 | color: WHITE; 20 | background-color: #6c757d; 21 | } 22 | 23 | [data-theme="dark"] .list-group-item-light.list-group-item-action:hover { 24 | color: WHITE; 25 | background-color: #6c757d; 26 | } 27 | 28 | [data-theme="dark"] .list-group-item-light { 29 | color: WHITE; 30 | background-color: #202020; 31 | border: 1px solid WHITE; 32 | } 33 | 34 | [data-theme="dark"] .my_svg { 35 | fill: WHITE; 36 | stroke: WHITE; 37 | height: 50px; 38 | width: 200px; 39 | } 40 | 41 | [data-theme="dark"] .table { 42 | color: WHITE; 43 | background-color: #202020; 44 | } 45 | 46 | [data-theme="dark"] .tbody { 47 | color: WHITE; 48 | background-image: none; 49 | border-color: WHITE; 50 | } 51 | 52 | [data-theme="dark"] .table .table-light th { 53 | color: WHITE; 54 | background-color: #202020; 55 | border-color: WHITE; 56 | } 57 | 58 | [data-theme="dark"] .table tr { 59 | background-color: #202020; 60 | } 61 | 62 | [data-theme="dark"] .table tr:hover { 63 | color: WHITE; 64 | background-color: #6c757d; 65 | transition: background-color .3s; 66 | } 67 | 68 | [data-theme="dark"] .table-striped tbody tr { 69 | background-color: #202020; 70 | } 71 | 72 | [data-theme="dark"] .table-striped tbody tr:hover { 73 | color: WHITE; 74 | background-color: #6c757d; 75 | transition: background-color .3s; 76 | } 77 | 78 | [data-theme="dark"] .btn-primary { 79 | color: WHITE; 80 | background-color: #6c757d; 81 | border-color: #6c757d; 82 | } 83 | 84 | [data-theme="dark"] .btn-primary:hover { 85 | color: WHITE; 86 | background-color: #545b62; 87 | border-color: #545b62; 88 | transition: background-color .3s; 89 | } 90 | 91 | [data-theme="dark"] .btn-outline-primary { 92 | color: WHITE; 93 | background-color: transparent; 94 | background-image: none; 95 | border-color: WHITE; 96 | } 97 | 98 | [data-theme="dark"] .btn-outline-primary:hover { 99 | color: WHITE; 100 | background-color: #6c757d; 101 | border-color: #ccc; 102 | transition: background-color .3s; 103 | } 104 | 105 | [data-theme="dark"] .btn-outline-secondary { 106 | color: WHITE; 107 | background-color: #202020; 108 | background-image: none; 109 | border-color: WHITE; 110 | } 111 | 112 | [data-theme="dark"] .btn-outline-secondary:hover { 113 | color: WHITE; 114 | background-color: #6c757d; 115 | border-color: #ccc; 116 | transition: background-color .3s; 117 | } 118 | 119 | [data-theme="dark"] .btn-outline-secondary:focus, .btn-outline-secondary.focus { 120 | color: WHITE; 121 | background-color: #6c757d; 122 | border-color: #ccc; 123 | } 124 | 125 | [data-theme="dark"] .modal-content { 126 | background-color: #222; 127 | } 128 | 129 | [data-theme="dark"] .page-link { 130 | color: WHITE; 131 | background-color: transparent; 132 | border: 1px solid WHITE; 133 | } 134 | 135 | [data-theme="dark"] .page-link:hover { 136 | background-color: #6c757d; 137 | transition: background-color .3s; 138 | } 139 | 140 | [data-theme="dark"] .bs-tooltip-top .arrow::before, .bs-tooltip-auto[x-placement^="top"] .arrow::before { 141 | border-top-color: #444; 142 | } 143 | 144 | [data-theme="dark"] .bs-tooltip-bottom .arrow::before, .bs-tooltip-auto[x-placement^="bottom"] .arrow::before { 145 | border-bottom-color: #444; 146 | } 147 | 148 | [data-theme="dark"] .tooltip-inner { 149 | background-color: #444; 150 | } 151 | 152 | [data-theme="dark"] .fa-refresh { 153 | color: WHITE; 154 | } 155 | 156 | [data-theme="dark"] .fa-times { 157 | color: WHITE; 158 | } 159 | 160 | [data-theme="dark"] .dropdown-menu { 161 | color: WHITE; 162 | background-color: #202020; 163 | } 164 | 165 | [data-theme="dark"] .dropdown-item { 166 | color: WHITE; 167 | } 168 | 169 | [data-theme="dark"] .navbar-light .navbar-nav .nav-link { 170 | color: rgba(255, 255, 255, 0.55); 171 | } 172 | 173 | [data-theme="dark"] .dropdown-item:hover { 174 | color: Black; 175 | } 176 | 177 | [data-theme="dark"] .dropdown-item:active { 178 | color: Black; 179 | } 180 | 181 | [data-theme="dark"] .bgcolorSun { 182 | background-color: #ff5050 !important; 183 | } 184 | 185 | [data-theme="dark"] .bgcolorSat { 186 | background-color: #5891db !important; 187 | } 188 | 189 | [data-theme="dark"] .bgcolorDay { 190 | background-color: #202020 !important; 191 | } 192 | 193 | [data-theme="dark"] .bgcolornormal { 194 | background-color: #4d0715 !important; 195 | } 196 | 197 | [data-theme="dark"] .bgcolorlow { 198 | background-color: #800b23 !important; 199 | } 200 | 201 | [data-theme="dark"] .bgcolormid { 202 | background-color: #b31031 !important; 203 | } 204 | 205 | [data-theme="dark"] .bgcolorhigh { 206 | background-color: #dc143c !important; 207 | } 208 | -------------------------------------------------------------------------------- /parser/velociraptor/monitoring_artifact: -------------------------------------------------------------------------------- 1 | name: Custom.Windows.Events.Blauhaunt 2 | description: | 3 | Blauhaunt Monitoring Artifact 4 | author: cgo SEC Consult 5 | # toDo: make sent as list and add more relevant events 6 | type: CLIENT_EVENT 7 | 8 | parameters: 9 | - name: LogonTypeRegex 10 | description: Specify LogonTypes to monitor 11 | type: json_array 12 | default: '[1,2,3,4,5,6,7,8,9,9,10,11,12]' 13 | - name: Security 14 | description: path to Security event log. 15 | default: '%SystemRoot%\System32\Winevt\Logs\Security.evtx' 16 | #- name: System 17 | # description: path to System event log. 18 | # default: '%SystemRoot%\System32\Winevt\Logs\System.evtx' 19 | - name: LocalSessionManager 20 | description: path to TerminalServices-LocalSessionManager operational event log. 21 | default: '%SystemRoot%\System32\Winevt\Logs\Microsoft-Windows-TerminalServices-LocalSessionManager%4Operational.evtx' 22 | - name: RemoteConnectionManager 23 | description: path to TerminalServices-RemoteConnectionManager operational event log. 24 | default: '%SystemRoot%\System32\Winevt\Logs\Microsoft-Windows-TerminalServices-RemoteConnectionManager%4Operational.evtx' 25 | - name: RDPClientOperational 26 | description: path to TerminalServices-ClientActiveXCore RDPClient%4Operational event log. 27 | default: '%SystemRoot%\System32\Winevt\Logs\Microsoft-Windows-TerminalServices-RDPClient%4Operational.evtx' 28 | - name: SourceIPRegex 29 | default: .* 30 | type: regex 31 | - name: UserNameRegex 32 | default: .* 33 | type: regex 34 | - name: UserNameWhitelist 35 | default: '^DWM-|^UMFD-|\$$' 36 | type: regex 37 | 38 | 39 | sources: 40 | - precondition: 41 | SELECT OS From info() where OS = 'windows' 42 | query: | 43 | LET fspaths <= SELECT OSPath 44 | FROM glob(globs=[ 45 | expand(path=Security), 46 | --expand(path=System), 47 | expand(path=LocalSessionManager), 48 | expand(path=RemoteConnectionManager), 49 | expand(path=RDPClientOperational)]) 50 | LET process_security_events(path) = SELECT 51 | timestamp(epoch=int(int=System.TimeCreated.SystemTime)) AS EventTime, 52 | System.EventID.Value as EventID, 53 | if( 54 | condition=EventData.LogonType, 55 | then=EventData.LogonType, 56 | else="") as LogonType, 57 | if( 58 | condition=System.EventID.Value = 4624, 59 | then=if( 60 | condition=EventData.ElevatedToken="%%1843", 61 | then="ELEVATED: YES", 62 | else="ELEVATED: NO"), 63 | else=if( 64 | condition=System.EventID.Value = 4776, 65 | then=if( 66 | condition=EventData.Status= 0, 67 | then="SUCCESS", 68 | else="FAILURE: " + get(item=dict( 69 | `3221225572`='USER NAME DOES NOT EXIST', 70 | `3221225578`='WRONG PASSWORD', 71 | `3221225581`='GENERIC LOGON ERROR', 72 | `3221225583`='NOT AUTHORIZED WORKSTATION', 73 | `3221225585`='PASSWORD EXPIRED', 74 | `3221225586`='ACCOUNT DEACTIVATED BY ADMIN', 75 | `3221225875`='ACCOUNT EXPIRED', 76 | `3221226020`='PASSWORD HAS TO BE CHANGED ON NEXT LOGON', 77 | `3221226036`='ACCOUNT LOCKED', 78 | `3221226353`='NO SECRET IN LOCAL ACCOUNT STORAGE'), 79 | member=str(str=EventData.Status)) 80 | ), 81 | else=if( 82 | condition=System.EventID.Value = 4648, 83 | then="Process: " + EventData.ProcessName, 84 | else=if( 85 | condition=System.EventID.Value = 4778, 86 | then="", 87 | else="") 88 | ) 89 | ) 90 | ) as Distinction, 91 | if( 92 | condition=System.EventID.Value = 4672, 93 | then=EventData.SubjectUserName, 94 | else=if( 95 | condition=System.EventID.Value = 4778, 96 | then=EventData.AccountName, 97 | else=EventData.TargetUserName) 98 | ) as UserName, 99 | if(condition=EventData.IpAddress, 100 | then= EventData.IpAddress, 101 | else=if( 102 | condition=System.EventID.Value = 4778, 103 | then=EventData.ClientAddress, 104 | else="-") 105 | )as SourceIP, 106 | if(condition=System.EventID.Value = 4648, 107 | then=System.Computer, 108 | else=if( 109 | condition=System.EventID.Value = 4776, 110 | then=EventData.Workstation, 111 | else=if( 112 | condition=System.EventID.Value = 4778, 113 | then=EventData.ClientName, 114 | else=parse_string_with_regex(string=get(field="Message"),regex='''Workstation Name:\s(\S*)''').g1 115 | ) 116 | ) 117 | ) as SourceHostname, 118 | if( 119 | condition=System.EventID.Value = 4648, 120 | then=if( 121 | condition=EventData.TargetServerName = 'localhost', 122 | then=System.Computer, 123 | else=EventData.TargetServerName), 124 | else=if( 125 | condition=System.EventID.Value = 4776, 126 | then=EventData.Workstation, 127 | else=System.Computer) 128 | ) as Destination, 129 | get(item=dict( 130 | `4624`='SUCCESSFUL LOGON', 131 | `4625`='FAILED LOGON', 132 | `4648`='LOGON USING EXPLICIT CREDENTIALS', 133 | `4672`='SPECIAL LOGON', 134 | `4776`='NTLM LOGON', 135 | `4778`='REMOTE SESSION RECONNECTED'), 136 | member=str(str=System.EventID.Value) 137 | ) as Description, 138 | -- this is to avoid having a wrong SID in 4648 events: 139 | if( 140 | condition=System.EventID.Value=4648, 141 | then="-", 142 | else=get(field="Message") 143 | ) 144 | as Message 145 | 146 | FROM watch_evtx(filename=path) 147 | WHERE (System.EventID.Value in (4624, 4625) AND EventData.LogonType in LogonTypeRegex) 148 | OR System.EventID.Value in (4648, 4776, 4778) 149 | 150 | LET process_rdp_remote_connection_events(path) = SELECT 151 | timestamp(epoch=int(int=System.TimeCreated.SystemTime)) AS EventTime, 152 | System.EventID.Value as EventID, 153 | UserData.EventXML.Param1 as UserName, 154 | UserData.EventXML.Param3 as SourceIP, 155 | "-" as LogonType, 156 | "-" as SourceHostname, 157 | "-" as Distinction, 158 | "RDP User authentication succeeded" as Description, 159 | System.Computer as Destination, 160 | get(field="Message") as Message 161 | FROM watch_evtx(filename=OSPath) 162 | WHERE EventID IN (1149,) 163 | AND UserName =~ UserNameRegex 164 | AND NOT UserName =~ UserNameWhitelist 165 | AND SourceIP =~ SourceIPRegex 166 | 167 | 168 | LET process_rdp_local_session_events(path) = SELECT 169 | timestamp(epoch=int(int=System.TimeCreated.SystemTime)) AS EventTime, 170 | System.EventID.Value as EventID, 171 | split(string=UserData.EventXML.User,sep='\\\\')[1] as UserName, 172 | UserData.EventXML.Address as SourceIP, 173 | "-" as LogonType, 174 | "-" as SourceHostname, 175 | "-" as Distinction, 176 | "RDP Session Logon Succeeded" as Description, 177 | System.Computer as Destination, 178 | get(field="Message") as Message 179 | FROM watch_evtx(filename=OSPath) 180 | WHERE EventID IN (21,) 181 | AND UserName =~ UserNameRegex 182 | AND NOT UserName =~ UserNameWhitelist 183 | AND SourceIP =~ SourceIPRegex 184 | 185 | LET process_system_events(path) = SELECT 186 | timestamp(epoch=int(int=System.TimeCreated.SystemTime)) AS EventTime, 187 | System.EventID.Value as EventID, 188 | EventData.TargetUserName as UserName, 189 | UserData.EventXML.Address as SourceIP, 190 | "-" as LogonType, 191 | "-" as SourceHostname, 192 | "-" as Distinction, 193 | "-" as Description, 194 | System.Computer as Destination 195 | FROM watch_evtx(filename=OSPath) 196 | WHERE EventID IN (9009,) 197 | AND UserName =~ UserNameRegex 198 | AND NOT UserName =~ UserNameWhitelist 199 | AND SourceIP =~ SourceIPRegex 200 | 201 | 202 | LET process_rdp_client_events(path) = SELECT 203 | timestamp(epoch=int(int=System.TimeCreated.SystemTime)) AS EventTime, 204 | System.EventID.Value as EventID, 205 | System.Security.UserID as UserName, 206 | UserData.EventXML.Address as SourceIP, 207 | "-" as LogonType, 208 | "-" as SourceIP, 209 | System.Computer as SourceHostname, 210 | "-" as Distinction, 211 | "RDP ClientActiveX is trying to connect to the server (collected on origin system) from Microsoft-Windows-TerminalServices-RDPClient/Operational" as Description, 212 | if( 213 | condition=EventData.Value = "127.0.0.1", 214 | then=System.Computer, 215 | else=EventData.Value 216 | ) as Destination, 217 | EventData.Value as Destination, 218 | Message as Message 219 | FROM watch_evtx(filename=OSPath) 220 | WHERE EventID IN (1024,) 221 | AND UserName =~ UserNameRegex 222 | AND NOT UserName =~ UserNameWhitelist 223 | AND SourceIP =~ SourceIPRegex 224 | 225 | LET evtxsearch(PathList) = SELECT * FROM 226 | foreach( 227 | row=PathList, 228 | query={ 229 | SELECT * FROM 230 | if( 231 | condition=split(string=OSPath,sep='\\\\')[-1]=~"Security", 232 | then=process_security_events(path=OSPath), 233 | else=if( 234 | condition=split(string=OSPath,sep='\\\\')[-1]=~"Microsoft-Windows-TerminalServices-LocalSessionManager%4Operational", 235 | then=process_rdp_local_session_events(path=OSPath), 236 | else=if( 237 | condition=split(string=OSPath,sep='\\\\')[-1]=~"Microsoft-Windows-TerminalServices-RemoteConnectionManager%4Operational", 238 | then=process_rdp_remote_connection_events(path=OSPath), 239 | else=if( 240 | condition=split(string=OSPath,sep='\\\\')[-1]=~"Microsoft-Windows-TerminalServices-RDPClient%4Operational", 241 | then=process_rdp_client_events(path=OSPath), 242 | else=if( 243 | condition=split(string=OSPath,sep='\\\\')[-1]=~"System", 244 | then=process_system_events(path=OSPath), 245 | else=scope() 246 | ) 247 | ) 248 | ) 249 | ) 250 | ) 251 | }, 252 | async=true 253 | ) 254 | 255 | LET search = SELECT * 256 | FROM evtxsearch(PathList={SELECT OSPath FROM fspaths}) 257 | 258 | SELECT EventTime, array(a=array(a=EventTime)[9]) as LogonTimes, Destination, UserName, parse_string_with_regex(string=Message,regex='''Security\sID:\s(.*)\s''').g1 as SID, EventID, LogonType, Description, Distinction, SourceIP, SourceHostname, "1" as LogonCount 259 | FROM search 260 | -------------------------------------------------------------------------------- /parser/velociraptor/velo_artifact_old.yaml: -------------------------------------------------------------------------------- 1 | name: Custom.Windows.EventLogs.Blauhaunt 2 | author: CGO (SEC Consult) 3 | description: | 4 | Known issues: 1149 Event extracting the SourceIP does not work 5 | 6 | This artifact will extract Event Logs related to Remote sessions, logon and logoff. 7 | To reduce data, the logonns will be grouped by EventID, Computer, UserName, LogonType, SourceIP 8 | then the results are sorted by EventTime in ascending order, 9 | Security channel - EventID in 4624 AND LogonType 3, 7, or 10. 10 | Security channel - EventID in 4648, 4672. 11 | Microsoft-Windows-TerminalServices-RemoteConnectionManager/Operational - EventID 1149. 12 | Microsoft-Windows-TerminalServices-LocalSessionManager/Operational - EventID 22,21. 13 | Best use of this artifact is to collect RDP and Authentication events around 14 | a timeframe of interest and order by EventTime to scope RDP activity. 15 | reference: 16 | - https://github.com/cgosec/Blauhaunt 17 | - This artifact is based on the Windows.EventLogs.RDPAuth artifact from Matt Green - @mgreen27 18 | type: CLIENT 19 | precondition: SELECT OS From info() where OS = 'windows' 20 | parameters: 21 | - name: Security 22 | description: path to Security event log. 23 | default: '%SystemRoot%\System32\Winevt\Logs\Security.evtx' 24 | - name: System 25 | description: path to System event log. 26 | default: '%SystemRoot%\System32\Winevt\Logs\System.evtx' 27 | - name: LocalSessionManager 28 | description: path to TerminalServices-LocalSessionManager operational event log. 29 | default: '%SystemRoot%\System32\Winevt\Logs\Microsoft-Windows-TerminalServices-LocalSessionManager%4Operational.evtx' 30 | - name: RemoteConnectionManager 31 | description: path to TerminalServices-RemoteConnectionManager operational event log. 32 | default: '%SystemRoot%\System32\Winevt\Logs\Microsoft-Windows-TerminalServices-RemoteConnectionManager%4Operational.evtx' 33 | - name: RDPClientOperational 34 | description: path to TerminalServices-ClientActiveXCore RDPClient%4Operational event log. 35 | default: '%SystemRoot%\System32\Winevt\Logs\Microsoft-Windows-TerminalServices-RDPClient%4Operational.evtx' 36 | - name: DateAfter 37 | description: "search for events after this date. YYYY-MM-DDTmm:hh:ss Z" 38 | type: timestamp 39 | - name: DateBefore 40 | description: "search for events before this date. YYYY-MM-DDTmm:hh:ss Z" 41 | type: timestamp 42 | - name: SourceIPRegex 43 | default: .* 44 | type: regex 45 | - name: UserNameRegex 46 | default: .* 47 | type: regex 48 | - name: UserNameWhitelist 49 | default: '^DWM-|^UMFD-|\$$' 50 | type: regex 51 | - name: SearchVSS 52 | description: "add VSS into query." 53 | type: bool 54 | sources: 55 | - query: | 56 | -- firstly set timebounds for performance 57 | 58 | LET DateAfterTime <= if(condition=DateAfter, 59 | then=DateAfter, else=timestamp(epoch="1600-01-01")) 60 | LET DateBeforeTime <= if(condition=DateBefore, 61 | then=DateBefore, else=timestamp(epoch="2200-01-01")) 62 | 63 | -- expand provided glob into a list of paths on the file system (fs) 64 | LET fspaths <= SELECT OSPath 65 | FROM glob(globs=[ 66 | expand(path=Security), 67 | expand(path=System), 68 | expand(path=LocalSessionManager), 69 | expand(path=RemoteConnectionManager)]) 70 | -- function returning list of VSS paths corresponding to path 71 | LET vsspaths(path) = SELECT OSPath 72 | FROM Artifact.Windows.Search.VSS(SearchFilesGlob=path) 73 | 74 | -- function returning query hits 75 | LET evtxsearch(PathList) = SELECT * FROM foreach( 76 | row=PathList, 77 | query={ 78 | SELECT System.EventRecordID as RecordID, timestamp(epoch=int(int=System.TimeCreated.SystemTime)) AS EventTime, 79 | -- Select Destination 80 | if( 81 | condition= System.Channel='Security', 82 | then=if( 83 | condition=System.EventID.Value = 4648, 84 | then=if( 85 | condition=EventData.TargetServerName = 'localhost', 86 | then=System.Computer, 87 | else=EventData.TargetServerName 88 | ), 89 | else=System.Computer 90 | ), 91 | else=if( 92 | condition=System.Channel='Microsoft-Windows-TerminalServices-RDPClient/Operational', 93 | then=EventData.Value, 94 | else=System.Computer 95 | ) 96 | ) 97 | as Destination, 98 | 99 | System.Channel as Channel, 100 | -- Select EventID 101 | if( 102 | condition=System.Channel='Security', 103 | then=if( 104 | condition=System.EventID.Value = 4772, 105 | then=if( 106 | condition=EventData.Status != "0x0", 107 | then="4772" + (EventData.Status), 108 | else=System.EventID.Value 109 | ), 110 | else=System.EventID.Value 111 | ), 112 | else=System.EventID.Value 113 | ) 114 | as EventID, 115 | 116 | -- Select DomainName 117 | if( 118 | condition= System.Channel='Security', 119 | then=EventData.TargetDomainName, 120 | else=if( 121 | condition= UserData.EventXML.User, 122 | then= split(string=UserData.EventXML.User,sep='\\\\')[0], 123 | else=if( 124 | condition= UserData.EventXML.Param2, 125 | then= UserData.EventXML.Param2, 126 | else= 'null' 127 | ) 128 | ) 129 | ) 130 | as DomainName, 131 | 132 | -- Select UserName 133 | if( 134 | condition= System.Channel='Security', 135 | then= EventData.TargetUserName, 136 | else=if( 137 | condition= UserData.EventXML.User, 138 | then= split(string=UserData.EventXML.User,sep='\\\\')[1], 139 | else= if( 140 | condition= UserData.EventXML.Param1, 141 | then= UserData.EventXML.Param1, 142 | else= 'UNKNOWN' 143 | ) 144 | ) 145 | ) 146 | as UserName, 147 | 148 | -- Select LogonType 149 | if( 150 | condition= System.Channel='Security', 151 | then= if( 152 | condition= EventData.LogonType, 153 | then= EventData.LogonType, 154 | else= '' 155 | ), 156 | else= '' 157 | ) 158 | as LogonType, 159 | 160 | -- Select SourceIP 161 | if( 162 | condition= System.Channel='Security', 163 | then=if( 164 | condition= EventData.IpAddress, 165 | then= EventData.IpAddress, 166 | else= '' 167 | ), 168 | else=if( 169 | condition= System.Channel=~'TerminalServices', 170 | then=if( 171 | condition=UserData.EventXML.Param3, 172 | then=UserData.EventXML.Param3, 173 | else=if( 174 | condition=UserData.EventXML.Address, 175 | then=UserData.EventXML.Address, 176 | else='' 177 | ) 178 | ), 179 | else= '' 180 | ) 181 | ) 182 | as SourceIP, 183 | 184 | -- Select Description 185 | if( 186 | condition= System.Channel=~'TerminalServices|System', 187 | then=get(item=dict( 188 | `21`='RDP_LOCAL_CONNECTED', 189 | `22`='RDP_REMOTE_CONNECTED', 190 | `23`='RDP_SESSION_LOGOFF', 191 | `24`='RDP_LOCAL_DISCONNECTED', 192 | `25`='RDP_REMOTE_RECONNECTION', 193 | `39`='RDP_REMOTE_DISCONNECTED_FORMAL', 194 | `40`='RDP_REMOTE_DISCONNECTED_REASON', 195 | `1149`='RDP_INITIATION_SUCCESSFUL', 196 | `9009`='DESKTOPWINDOWMANAGER_CLOSED'), 197 | member=str(str=System.EventID.Value)), 198 | else=if( 199 | condition= System.EventID.Value = 4624 AND EventData.LogonType = 10, 200 | then='RDP_LOGON_SUCCESSFUL_NEW', 201 | else=if( 202 | condition= System.EventID.Value = 4624 AND EventData.LogonType = 3, 203 | then='LOGON_SUCCESSFUL', 204 | else=if( 205 | condition= System.EventID.Value = 4624 AND EventData.LogonType = 7, 206 | then='LOGON_SUCCESSFUL_OLD', 207 | else=if( 208 | condition= System.EventID.Value = 4625 AND EventData.LogonType = 3, 209 | then='LOGON_FAILED', 210 | else=if( 211 | condition= System.EventID.Value = 4625 AND EventData.LogonType = 10, 212 | then='RDP_LOGON_FAILED', 213 | else=get(item=dict( 214 | `4778`='LOGON_RECONNECT_EXISTING', 215 | `4779`='SESSION_DISCONNECT', 216 | `4647`='USER_INITIATED_LOGOFF', 217 | `4634`='LOGOFF_DISCONNECT'), member=str(str=System.EventID.Value)) 218 | ) 219 | ) 220 | ) 221 | ) 222 | ) 223 | ) 224 | as Description, 225 | 226 | -- Select SourceHostname 227 | if( 228 | condition=System.Channel='Microsoft-Windows-TerminalServices-RDPClient/Operational', 229 | then="Workstation Name: " + System.Computer, 230 | else=if( 231 | condition=System.Channel='Security', 232 | then=if( 233 | condition=System.EventID.Value = 4648, 234 | then=System.Computer, 235 | else=if( 236 | condition=System.EventID.Value = 4776, 237 | then=EventData.Workstation, 238 | else=parse_string_with_regex(string=get(field="Message"),regex='''Workstation Name:\s(\S*)''').g1 239 | ) 240 | ) 241 | ) 242 | ) 243 | as SourceHostname, 244 | 245 | get(field="Message") as Message, 246 | 247 | System.EventRecordID as EventRecordID, 248 | OSPath 249 | FROM parse_evtx(filename=OSPath) 250 | WHERE 251 | (Channel = 'Security' AND 252 | ((EventID in (4624, 4625) AND LogonType in (3,10,7,9)) OR EventID in (4648, 4776)) 253 | ) 254 | OR 255 | (Channel = 'Microsoft-Windows-TerminalServices-RemoteConnectionManager/Operational' AND EventID = 1149) 256 | OR 257 | (Channel = 'Microsoft-Windows-TerminalServices-LocalSessionManager/Operational' AND EventID = (21) AND SourceIP != 'LOCAL') 258 | OR 259 | (Channel = 'Microsoft-Windows-TerminalServices-RDPClient/Operational' AND EventID in (1024, 1102)) 260 | AND EventTime < DateBeforeTime 261 | AND EventTime > DateAfterTime 262 | AND if( 263 | condition= UserNameWhitelist, 264 | then= NOT UserName =~ UserNameWhitelist, 265 | else= True 266 | ) 267 | AND UserName =~ UserNameRegex 268 | AND SourceIP =~ SourceIPRegex 269 | } 270 | ) 271 | 272 | -- include VSS in calculation and deduplicate with GROUP BY by file 273 | LET include_vss = SELECT * FROM 274 | foreach( 275 | row=fspaths, 276 | query={ 277 | SELECT * 278 | FROM 279 | evtxsearch(PathList={ 280 | SELECT OSPath FROM vsspaths(path=OSPath) 281 | GROUP BY EventRecordID,Channel 282 | } 283 | ) 284 | } 285 | ) 286 | 287 | -- exclude VSS in EvtxHunt 288 | LET exclude_vss = SELECT * 289 | FROM evtxsearch(PathList={SELECT OSPath FROM fspaths}) 290 | 291 | -- return rows 292 | LET search = SELECT * 293 | FROM 294 | if( 295 | condition=SearchVSS, 296 | then=include_vss, 297 | else=exclude_vss 298 | ) 299 | 300 | LET sort = SELECT * FROM search 301 | order by EventTime 302 | 303 | let grouped = SELECT EventTime, enumerate(items=EventTime) as LogonTimes, Destination, UserName, 304 | parse_string_with_regex(string=Message,regex='''Security\sID:\s(.*)\s''').g1 as SID, 305 | EventID, LogonType, Description, SourceIP, SourceHostname, count() as LogonCount 306 | FROM sort 307 | GROUP BY EventID, Destination, UserName, LogonType, SourceIP 308 | order by EventTime 309 | 310 | SELECT * FROM grouped 311 | -------------------------------------------------------------------------------- /parser/velociraptor/velo_artifact.yaml: -------------------------------------------------------------------------------- 1 | name: Custom.Windows.EventLogs.Blauhaunt 2 | author: CGO (SEC Consult) 3 | description: | 4 | This artifact will extract Event Logs related to Remote sessions, logon and logoff. 5 | To reduce data, the logons will be grouped by EventID, Computer, UserName, LogonType, SourceIP, Distinction (customizable). 6 | then the results are sorted by EventTime in ascending order, 7 | Security channel - EventID in 4624 AND LogonType 3, 7, or 10. 8 | Security channel - EventID in 4648, 4778, 4776. 9 | Microsoft-Windows-TerminalServices-RemoteConnectionManager/Operational - EventID 1149. 10 | Microsoft-Windows-TerminalServices-LocalSessionManager/Operational - EventID 21. 11 | Best use of this artifact is to collect RDP and Authentication events around 12 | a timeframe of interest and order by EventTime to scope RDP activity. 13 | reference: 14 | - https://github.com/cgosec/Blauhaunt 15 | - This artifact is based on the Windows.EventLogs.RDPAuth artifact from Matt Green - @mgreen27 16 | type: CLIENT 17 | precondition: SELECT OS From info() where OS = 'windows' 18 | parameters: 19 | - name: LogonTypes 20 | description: "LogonTypes to include in the query." 21 | default: '[2,3,9,10]' 22 | type: regex 23 | - name: Security 24 | description: path to Security event log. 25 | default: '%SystemRoot%\System32\Winevt\Logs\Security.evtx' 26 | #- name: System 27 | # description: path to System event log. 28 | # default: '%SystemRoot%\System32\Winevt\Logs\System.evtx' 29 | - name: LocalSessionManager 30 | description: path to TerminalServices-LocalSessionManager operational event log. 31 | default: '%SystemRoot%\System32\Winevt\Logs\Microsoft-Windows-TerminalServices-LocalSessionManager%4Operational.evtx' 32 | - name: RemoteConnectionManager 33 | description: path to TerminalServices-RemoteConnectionManager operational event log. 34 | default: '%SystemRoot%\System32\Winevt\Logs\Microsoft-Windows-TerminalServices-RemoteConnectionManager%4Operational.evtx' 35 | - name: RDPClientOperational 36 | description: path to TerminalServices-ClientActiveXCore RDPClient%4Operational event log. 37 | default: '%SystemRoot%\System32\Winevt\Logs\Microsoft-Windows-TerminalServices-RDPClient%4Operational.evtx' 38 | - name: DateAfter 39 | description: "search for events after this date. YYYY-MM-DDTmm:hh:ss Z" 40 | type: timestamp 41 | - name: DateBefore 42 | description: "search for events before this date. YYYY-MM-DDTmm:hh:ss Z" 43 | type: timestamp 44 | - name: SourceIPRegex 45 | default: .* 46 | type: regex 47 | - name: UserNameRegex 48 | default: .* 49 | type: regex 50 | - name: UserNameWhitelist 51 | default: '^DWM-|^UMFD-|\$$' 52 | type: regex 53 | - name: SearchVSS 54 | description: "add VSS into query." 55 | type: bool 56 | sources: 57 | - query: | 58 | -- firstly set time bounds for performance 59 | 60 | LET DateAfterTime <= if(condition=DateAfter, 61 | then=DateAfter, else=timestamp(epoch="1600-01-01")) 62 | LET DateBeforeTime <= if(condition=DateBefore, 63 | then=DateBefore, else=timestamp(epoch="2200-01-01")) 64 | 65 | -- expand provided glob into a list of paths on the file system (fs) 66 | LET fspaths <= SELECT OSPath 67 | FROM glob(globs=[ 68 | expand(path=Security), 69 | --expand(path=System), 70 | expand(path=LocalSessionManager), 71 | expand(path=RemoteConnectionManager), 72 | expand(path=RDPClientOperational)]) 73 | -- function returning list of VSS paths corresponding to path 74 | LET vsspaths(path) = SELECT OSPath 75 | FROM Artifact.Windows.Search.VSS(SearchFilesGlob=path) 76 | 77 | LET process_security_events(path) = SELECT 78 | timestamp(epoch=int(int=System.TimeCreated.SystemTime)) AS EventTime, 79 | System.EventID.Value as EventID, 80 | if( 81 | condition=EventData.LogonType, 82 | then=EventData.LogonType, 83 | else="") as LogonType, 84 | if( 85 | condition=System.EventID.Value = 4624, 86 | then=if( 87 | condition=EventData.ElevatedToken="%%1843", 88 | then="ELEVATED: YES", 89 | else="ELEVATED: NO"), 90 | else=if( 91 | condition=System.EventID.Value = 4776, 92 | then=if( 93 | condition=EventData.Status= 0, 94 | then="SUCCESS", 95 | else="FAILURE: " + get(item=dict( 96 | `3221225572`='USER NAME DOES NOT EXIST', 97 | `3221225578`='WRONG PASSWORD', 98 | `3221225581`='GENERIC LOGON ERROR', 99 | `3221225583`='NOT AUTHORIZED WORKSTATION', 100 | `3221225585`='PASSWORD EXPIRED', 101 | `3221225586`='ACCOUNT DEACTIVATED BY ADMIN', 102 | `3221225875`='ACCOUNT EXPIRED', 103 | `3221226020`='PASSWORD HAS TO BE CHANGED ON NEXT LOGON', 104 | `3221226036`='ACCOUNT LOCKED', 105 | `3221226353`='NO SECRET IN LOCAL ACCOUNT STORAGE'), 106 | member=str(str=EventData.Status)) 107 | ), 108 | else=if( 109 | condition=System.EventID.Value = 4648, 110 | then="Process: " + EventData.ProcessName, 111 | else=if( 112 | condition=System.EventID.Value = 4778, 113 | then="", 114 | else="") 115 | ) 116 | ) 117 | ) as Distinction, 118 | if( 119 | condition=System.EventID.Value = 4672, 120 | then=EventData.SubjectUserName, 121 | else=if( 122 | condition=System.EventID.Value = 4778, 123 | then=EventData.AccountName, 124 | else=EventData.TargetUserName) 125 | ) as UserName, 126 | if(condition=EventData.IpAddress, 127 | then= EventData.IpAddress, 128 | else=if( 129 | condition=System.EventID.Value = 4778, 130 | then=EventData.ClientAddress, 131 | else="-") 132 | )as SourceIP, 133 | if(condition=System.EventID.Value = 4648, 134 | then=System.Computer, 135 | else=if( 136 | condition=System.EventID.Value = 4776, 137 | then=EventData.Workstation, 138 | else=if( 139 | condition=System.EventID.Value = 4778, 140 | then=EventData.ClientName, 141 | else=parse_string_with_regex(string=get(field="Message"),regex='''Workstation Name:\s(\S*)''').g1 142 | ) 143 | ) 144 | ) as SourceHostname, 145 | if( 146 | condition=System.EventID.Value = 4648, 147 | then=if( 148 | condition=EventData.TargetServerName = 'localhost', 149 | then=System.Computer, 150 | else=EventData.TargetServerName), 151 | else=if( 152 | condition=System.EventID.Value = 4776, 153 | then=EventData.Workstation, 154 | else=System.Computer) 155 | ) as Destination, 156 | get(item=dict( 157 | `4624`='SUCCESSFUL LOGON', 158 | `4625`='FAILED LOGON', 159 | `4648`='LOGON USING EXPLICIT CREDENTIALS', 160 | `4672`='SPECIAL LOGON', 161 | `4776`='NTLM LOGON', 162 | `4778`='REMOTE SESSION RECONNECTED'), 163 | member=str(str=System.EventID.Value) 164 | ) as Description, 165 | -- this is to avoid having a wrong SID in 4648 events: 166 | if( 167 | condition=System.EventID.Value=4648, 168 | then="-", 169 | else=get(field="Message") 170 | ) 171 | as Message 172 | FROM parse_evtx(filename=OSPath) 173 | WHERE (EventID IN (4624, 4625) 174 | AND LogonType =~ LogonTypes) 175 | OR EventID IN (4648, 4776, 4778) 176 | AND EventTime < DateBeforeTime 177 | AND EventTime > DateAfterTime 178 | AND if( 179 | condition= UserNameWhitelist, 180 | then= NOT UserName =~ UserNameWhitelist, 181 | else= True 182 | ) 183 | AND UserName =~ UserNameRegex 184 | ORDER BY EventTime 185 | 186 | 187 | LET process_rdp_remote_connection_events(path) = SELECT 188 | timestamp(epoch=int(int=System.TimeCreated.SystemTime)) AS EventTime, 189 | System.EventID.Value as EventID, 190 | UserData.EventXML.Param1 as UserName, 191 | UserData.EventXML.Param3 as SourceIP, 192 | "-" as LogonType, 193 | "-" as SourceHostname, 194 | "-" as Distinction, 195 | "RDP User authentication succeeded" as Description, 196 | System.Computer as Destination, 197 | get(field="Message") as Message 198 | FROM parse_evtx(filename=OSPath) 199 | WHERE EventID IN (1149,) 200 | AND EventTime < DateBeforeTime 201 | AND EventTime > DateAfterTime 202 | AND UserName =~ UserNameRegex 203 | AND NOT UserName =~ UserNameWhitelist 204 | AND SourceIP =~ SourceIPRegex 205 | ORDER BY EventTime 206 | 207 | 208 | LET process_rdp_local_session_events(path) = SELECT 209 | timestamp(epoch=int(int=System.TimeCreated.SystemTime)) AS EventTime, 210 | System.EventID.Value as EventID, 211 | split(string=UserData.EventXML.User,sep='\\\\')[1] as UserName, 212 | UserData.EventXML.Address as SourceIP, 213 | "-" as LogonType, 214 | "-" as SourceHostname, 215 | "-" as Distinction, 216 | "RDP Session Logon Succeeded" as Description, 217 | System.Computer as Destination, 218 | get(field="Message") as Message 219 | FROM parse_evtx(filename=OSPath) 220 | WHERE EventID IN (21,) 221 | AND EventTime < DateBeforeTime 222 | AND EventTime > DateAfterTime 223 | AND UserName =~ UserNameRegex 224 | AND NOT UserName =~ UserNameWhitelist 225 | AND SourceIP =~ SourceIPRegex 226 | ORDER BY EventTime 227 | 228 | 229 | LET process_system_events(path) = SELECT 230 | timestamp(epoch=int(int=System.TimeCreated.SystemTime)) AS EventTime, 231 | System.EventID.Value as EventID, 232 | EventData.TargetUserName as UserName, 233 | UserData.EventXML.Address as SourceIP, 234 | "-" as LogonType, 235 | "-" as SourceHostname, 236 | "-" as Distinction, 237 | "-" as Description, 238 | System.Computer as Destination 239 | FROM parse_evtx(filename=OSPath) 240 | WHERE EventID IN (9009,) 241 | AND EventTime < DateBeforeTime 242 | AND EventTime > DateAfterTime 243 | AND UserName =~ UserNameRegex 244 | AND NOT UserName =~ UserNameWhitelist 245 | AND SourceIP =~ SourceIPRegex 246 | ORDER BY EventTime 247 | 248 | LET process_rdp_client_events(path) = SELECT 249 | timestamp(epoch=int(int=System.TimeCreated.SystemTime)) AS EventTime, 250 | System.EventID.Value as EventID, 251 | System.Security.UserID as UserName, 252 | UserData.EventXML.Address as SourceIP, 253 | "-" as LogonType, 254 | "-" as SourceIP, 255 | System.Computer as SourceHostname, 256 | "-" as Distinction, 257 | "RDP ClientActiveX is trying to connect to the server (collected on origin system) from Microsoft-Windows-TerminalServices-RDPClient/Operational" as Description, 258 | if( 259 | condition=EventData.Value = "127.0.0.1", 260 | then=System.Computer, 261 | else=EventData.Value 262 | ) as Destination, 263 | EventData.Value as Destination, 264 | Message as Message 265 | FROM parse_evtx(filename=OSPath) 266 | WHERE EventID IN (1024,) 267 | AND EventTime < DateBeforeTime 268 | AND EventTime > DateAfterTime 269 | AND UserName =~ UserNameRegex 270 | AND NOT UserName =~ UserNameWhitelist 271 | AND SourceIP =~ SourceIPRegex 272 | ORDER BY EventTime 273 | 274 | 275 | LET evtxsearch(PathList) = SELECT * FROM 276 | foreach( 277 | row=PathList, 278 | query={ 279 | SELECT * FROM 280 | if( 281 | condition=split(string=OSPath,sep='\\\\')[-1]=~"Security", 282 | then=process_security_events(path=OSPath), 283 | else=if( 284 | condition=split(string=OSPath,sep='\\\\')[-1]=~"Microsoft-Windows-TerminalServices-LocalSessionManager%4Operational", 285 | then=process_rdp_local_session_events(path=OSPath), 286 | else=if( 287 | condition=split(string=OSPath,sep='\\\\')[-1]=~"Microsoft-Windows-TerminalServices-RemoteConnectionManager%4Operational", 288 | then=process_rdp_remote_connection_events(path=OSPath), 289 | else=if( 290 | condition=split(string=OSPath,sep='\\\\')[-1]=~"Microsoft-Windows-TerminalServices-RDPClient%4Operational", 291 | then=process_rdp_client_events(path=OSPath), 292 | else=if( 293 | condition=split(string=OSPath,sep='\\\\')[-1]=~"System", 294 | then=process_system_events(path=OSPath), 295 | else=scope() 296 | ) 297 | ) 298 | ) 299 | ) 300 | ) 301 | }, 302 | async=true 303 | ) 304 | 305 | LET include_vss = SELECT * FROM 306 | foreach( 307 | row=fspaths, 308 | query={ 309 | SELECT * 310 | FROM evtxsearch(PathList={ 311 | SELECT OSPath FROM vsspaths(path=OSPath) 312 | GROUP BY EventRecordID, Channel 313 | } 314 | ) 315 | } 316 | ) 317 | 318 | -- exclude VSS in EvtxHunt 319 | LET exclude_vss = SELECT * 320 | FROM evtxsearch(PathList={SELECT OSPath FROM fspaths}) 321 | 322 | -- return rows 323 | LET search = SELECT * 324 | FROM 325 | if( 326 | condition=SearchVSS, 327 | then=include_vss, 328 | else=exclude_vss 329 | ) 330 | 331 | LET sort = SELECT * FROM search 332 | ORDER BY EventTime 333 | 334 | let grouped = SELECT EventTime, enumerate(items=EventTime) as LogonTimes, Destination, UserName, 335 | parse_string_with_regex(string=Message,regex='''Security\sID:\s(.*)\s''').g1 as SID, 336 | EventID, LogonType, Description, Distinction, SourceIP, SourceHostname, count() as LogonCount 337 | FROM sort 338 | GROUP BY EventID, Destination, UserName, LogonType, SourceIP, Distinction 339 | ORDER BY EventTime 340 | 341 | SELECT * FROM grouped 342 | -------------------------------------------------------------------------------- /parser/deprecated_powershell/blauhaunt_script.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | This script utilizes Get-WinEvent in serveral ways, filters and processes the data and write it into a specific json file. 4 | 5 | .DESCRIPTION 6 | https://github.com/cgosec/Blauhaunt/ 7 | This script collects events form the local system of a given path (recursive is possible) for the use in Blauhaunt. 8 | Collected Security Events: 4624, 4625, 4648, 4672, 4776 9 | Colleced RDP Operational Events: 21 10 | Collected RDP Connection Events: 1149 11 | LogonTypes: 3,9, 10 12 | Microsoft-Windows-TerminalServices-LocalSessionManager%4Operational Events: 21 13 | 14 | .PARAMETER Path 15 | OPTIONAL 16 | Giva Path to a folder where the Security.evtx and Microsoft-Windows-TerminalServices-LocalSessionManager%4Operational.evtx is resident. If only one is present the scripts will skip the other one. 17 | If no Path is provided the local System Drive will be taken to seach for files in /Windows/System32/winevt/Logs/ 18 | 19 | .PARAMETER OutPath 20 | OPTIONAL 21 | Specify the path where the output files will be written to. Do not provide file names since they are automatically generated. 22 | If no OutPath is provided the files are written to the current directory 23 | 24 | .PARAMETER StartDate 25 | OPTIONAL 26 | Format: yyyy-MM-dd 27 | Filter events to only process event starting from this date 28 | 29 | .PARAMETER EndDate 30 | OPTIONAL 31 | Format: yyyy-MM-dd 32 | Filter events to only process event to this date 33 | 34 | .PARAMETER Recursive 35 | OPTIONAL 36 | This only has effekt when -Path is set. 37 | If this is set the script will crawl through the folders and search for matching .evtx files to process. This is useful if you have a folder full of triage data from a DFIR investigation 38 | 39 | .EXAMPLE 40 | blauhaunt_script.ps1 -Path ./TriageLogs -Recursive -OutPath ./BlauhauntFiles/ -StartDate 2023-10-10 -EndDate 2023-10-14 41 | This is an example of how to use the script with different parameters. 42 | 43 | .NOTES 44 | Author: Christopher Golitschek 45 | Version: 0.2 46 | Date: 2023-10-15 47 | GIT: https://github.com/cgosec/Blauhaunt/ 48 | #> 49 | 50 | param ( 51 | [string]$Path, 52 | [string]$OutPath = "", 53 | [DateTime]$StartDate = "1970-01-01", 54 | [DateTime]$EndDate = "2200-01-01", 55 | [switch]$Recursive = $False 56 | ) 57 | 58 | # Specify the security event IDs to filter 59 | $securityEventIds = @(4624, 4625, 4648, 4776, 4672) 60 | $operationEventIds = @(21) 61 | $sessionEventIds = @(1149) 62 | $logontypes = @(3, 9, 10) 63 | $NewLine = [environment]::NewLine 64 | 65 | Write-Output "Parameter Path: $Path" 66 | Write-Output "Parameter Recursive: $Recursive" 67 | Write-Output "Parameter OutPath: $OutPath" 68 | Write-Output "Parameter StartDate: $StartDate" 69 | Write-Output "Parameter EndDate: $EndDate" 70 | Write-Verbose "running in verbose mode" 71 | 72 | function Write-SecurityEvents{ 73 | param ( 74 | $Events 75 | ) 76 | 77 | $Hostname = $Events[0].MachineName 78 | $table = $Events | ForEach-Object { 79 | if (($_.Id -eq 4624) -and ($logontypes -contains $_.Properties[8].Value) -and (!$_.Properties[5].Value.Split(".")[0].EndsWith("$"))){ 80 | $entry = [PSCustomObject]@{ 81 | 'TimeCreated' = $_.TimeCreated 82 | 'UserName' = $_.Properties[5].Value.Split(".")[0] 83 | 'SID' = $_.Properties[4].Value 84 | 'Destination' = $_.MachineName.Split(".")[0] 85 | 'Description' = "" 86 | 'EventID' = $_.Id 87 | 'LogonType' = $_.Properties[8].Value 88 | 'SourceIP' = $_.Properties[18].Value 89 | 'SourceHostname' = $_.Properties[11].Value.Split(".")[0] 90 | } 91 | $entry 92 | } 93 | elseif (($_.Id -eq 4625) -and ($logontypes -contains $_.Properties[10].Value) -and (!$_.Properties[5].Value.Split(".")[0].EndsWith("$"))){ 94 | $entry = [PSCustomObject]@{ 95 | 'TimeCreated' = $_.TimeCreated 96 | 'UserName' = $_.Properties[5].Value.Split(".")[0] 97 | 'SID' = $_.Properties[4].Value 98 | 'Destination' = $_.MachineName.Split(".")[0] 99 | 'Description' = "" 100 | 'EventID' = $_.Id 101 | 'LogonType' = $_.Properties[10].Value 102 | 'SourceIP' = $_.Properties[19].Value 103 | 'SourceHostname' = $_.Properties[13].Value.Split(".")[0] 104 | } 105 | $entry 106 | } 107 | elseif ($_.Id -eq 4648 -and (!$_.Properties[5].Value.Split(".")[0].EndsWith("$"))){ 108 | if ($_.Properties[8].Value -eq "localhost"){ 109 | $dst = $_.MachineName.Split(".")[0] 110 | } 111 | else { 112 | $dst = $_.Properties[8].Value.Split(".")[0] 113 | } 114 | $entry = [PSCustomObject]@{ 115 | 'TimeCreated' = $_.TimeCreated 116 | 'UserName' = $_.Properties[5].Value.Split(".")[0] 117 | 'SID' = "-" 118 | 'Destination' = $dst 119 | 'Description' = "Using explicied credentials" 120 | 'EventID' = $_.Id 121 | 'LogonType' = "-" 122 | 'SourceIP' = $_.Properties[12].Value 123 | 'SourceHostname' = $_.MachineName.Split(".")[0] 124 | } 125 | $entry 126 | } 127 | elseif ($_.Id -eq 4776 -and (!$_.Properties[1].Value.Split(".")[0].EndsWith("$"))){ 128 | if (!$_.Properties[3].Value -eq 0) { 129 | $EventID = "4776(0x" + $_.Properties[3].Value.ToString("X") + ")" 130 | } 131 | else { 132 | $EventID = 4776 133 | } 134 | $entry = [PSCustomObject]@{ 135 | 'TimeCreated' = $_.TimeCreated 136 | 'UserName' = $_.Properties[1].Value.Split(".")[0] 137 | 'SID' = "-" 138 | 'Destination' = $_.MachineName.Split(".")[0] 139 | 'Description' = $_.Properties[3].Value 140 | 'EventID' = $EventID 141 | 'LogonType' = "-" 142 | 'SourceIP' = "" 143 | 'SourceHostname' = $_.Properties[2].Value.Split(".")[0] 144 | } 145 | $entry 146 | } 147 | elseif ($_.Id -eq 4672 -and (!$_.Properties[1].Value.Split(".")[0].EndsWith("$"))){ 148 | $entry = [PSCustomObject]@{ 149 | 'TimeCreated' = $_.TimeCreated 150 | 'UserName' = $_.Properties[1].Value.Split(".")[0] 151 | 'SID' = $_.Properties[0].Value 152 | 'Destination' = $_.MachineName.Split(".")[0] 153 | 'Description' = $_.Properties[4].Value 154 | 'EventID' = $_.Id 155 | 'LogonType' = "-" 156 | 'SourceIP' = "" 157 | 'SourceHostname' = "" 158 | } 159 | $entry 160 | } 161 | } 162 | $grouped = "" 163 | $grouped += $table | Group-Object -Property UserName, SID, SourceIP, EventID, LogonType, SourceIP, SourceHostname | ForEach-Object { 164 | $g = $_.Group[0] 165 | [System.Collections.ArrayList]$times = @() 166 | $times += $table | ForEach-Object { 167 | if (($g.UserName -eq $_.UserName) -and ($g.SID -eq $_.SID) -and ($g.Destination -eq $_.Destination) -and ($g.EventID -eq $_.EventID) -and ($g.LogonType -eq $_.LogonType) -and ($g.SourceIP -eq $_.SourceIP) -and ($g.SourceHostname -eq $_.SourceHostname)){ 168 | $_.TimeCreated.ToString("yyyy-MM-ddTHH:mm:ssZ") 169 | } 170 | } 171 | 172 | $entry = [PSCustomObject]@{ 173 | 'LogonTimes' = $times 174 | 'UserName' = $g.UserName 175 | 'SID' = $g.SID.Value 176 | 'Destination' = $g.Destination 177 | 'Description' = $g.Description 178 | 'EventID' = $g.EventID 179 | 'LogonType' = $g.LogonType 180 | 'SourceIP' = $g.SourceIP 181 | 'SourceHostname' = $g.SourceHostname 182 | 'LogonCount' = $times.Count 183 | } 184 | $line = $entry | ConvertTo-Json -compress 185 | $line = $line.Trim() 186 | $line += $NewLine 187 | $line 188 | } 189 | $results = $grouped 190 | $file = $OutPath + "BlauHaunt_" + $Hostname + "_Security" + ".json" 191 | $counter = 1 192 | while (Test-Path $file -PathType leaf) { 193 | $file = $OutPath + "BlauHaunt_" + $Hostname + "_Security_" + $counter + ".json" 194 | $counter++ 195 | } 196 | $results | Out-File -FilePath $file -Encoding ascii 197 | } 198 | 199 | function Write-RDPEvents { 200 | param ( 201 | $Events 202 | ) 203 | $Hostname = $Events[0].MachineName 204 | $table = $Events | ForEach-Object { 205 | if ((@(21) -contains $_.Id)){ 206 | [xml]$data = $_.ToXml() 207 | $user = $data.Event.UserData.EventXML.User.Split("\\") 208 | $user = $user[$user.Count-1] 209 | $ip = $data.Event.UserData.EventXML.Address 210 | $destination = $data.Event.System.Computer.Split(".")[0] 211 | $source = "-" 212 | if (@("LOCAL", "LOKAL", "127.0.0.1") -contains $ip){ 213 | $source = $destination 214 | $ip = "-" 215 | } 216 | $entry = [PSCustomObject]@{ 217 | 'TimeCreated' = $_.TimeCreated 218 | 'UserName' = $user 219 | 'SID' = "-" 220 | 'Destination' = $destination 221 | 'Description' = "" 222 | 'EventID' = $_.Id 223 | 'LogonType' = "" 224 | 'SourceIP' = $ip 225 | 'SourceHostname' = $source 226 | } 227 | $entry 228 | } 229 | } 230 | 231 | $grouped = "" 232 | $grouped += $table | Group-Object -Property UserName, SID, SourceIP, EventID, LogonType, SourceIP, SourceHostname | ForEach-Object { 233 | $g = $_.Group[0] 234 | [System.Collections.ArrayList]$times = @() 235 | $times += $table | ForEach-Object { 236 | if (($g.UserName -eq $_.UserName) -and ($g.SID -eq $_.SID) -and ($g.Destination -eq $_.Destination) -and ($g.EventID -eq $_.EventID) -and ($g.LogonType -eq $_.LogonType) -and ($g.SourceIP -eq $_.SourceIP) -and ($g.SourceHostname -eq $_.SourceHostname)){ 237 | $_.TimeCreated.ToString("yyyy-MM-ddTHH:mm:ssZ") 238 | } 239 | } 240 | 241 | $entry = [PSCustomObject]@{ 242 | 'LogonTimes' = $times 243 | 'UserName' = $g.UserName 244 | 'SID' = $g.SID.Value 245 | 'Destination' = $g.Destination 246 | 'Description' = $g.Description 247 | 'EventID' = $g.EventID 248 | 'LogonType' = $g.LogonType 249 | 'SourceIP' = $g.SourceIP 250 | 'SourceHostname' = $g.SourceHostname 251 | 'LogonCount' = $times.Count 252 | } 253 | $line = $entry | ConvertTo-Json -compress 254 | $line = $line.Trim() 255 | $line += $NewLine 256 | $line 257 | } 258 | $results = $grouped 259 | $file = $OutPath + "BlauHaunt_" + $Hostname + "_RDP" + ".json" 260 | $counter = 1 261 | while (Test-Path $file -PathType leaf) { 262 | $file = $OutPath + "BlauHaunt_" + $Hostname + "_RDP_" + $counter + ".json" 263 | $counter++ 264 | } 265 | $results | Out-File -FilePath $file -Encoding ascii 266 | } 267 | 268 | function Write-RDPConnectionEvents { 269 | param ( 270 | $Events 271 | ) 272 | $Hostname = $Events[0].MachineName 273 | $table = $Events | ForEach-Object { 274 | if ((@(1149) -contains $_.Id)){ 275 | $user = $_.Properties[0].value.Split("\\") 276 | $user = $user[$user.Count-1] 277 | $ip = $_.Properties[2].value 278 | $destination = $_.MachineName.Split(".")[0] 279 | $source = "-" 280 | if (@("LOCAL", "LOKAL", "127.0.0.1") -contains $ip){ 281 | $source = $destination 282 | $ip = "-" 283 | } 284 | $entry = [PSCustomObject]@{ 285 | 'TimeCreated' = $_.TimeCreated 286 | 'UserName' = $user 287 | 'SID' = "-" 288 | 'Destination' = $destination 289 | 'Description' = "" 290 | 'EventID' = $_.Id 291 | 'LogonType' = "" 292 | 'SourceIP' = $ip 293 | 'SourceHostname' = $source 294 | } 295 | $entry 296 | } 297 | } 298 | 299 | $grouped = "" 300 | $grouped += $table | Group-Object -Property UserName, SID, SourceIP, EventID, LogonType, SourceIP, SourceHostname | ForEach-Object { 301 | $g = $_.Group[0] 302 | [System.Collections.ArrayList]$times = @() 303 | $times += $table | ForEach-Object { 304 | if (($g.UserName -eq $_.UserName) -and ($g.SID -eq $_.SID) -and ($g.Destination -eq $_.Destination) -and ($g.EventID -eq $_.EventID) -and ($g.LogonType -eq $_.LogonType) -and ($g.SourceIP -eq $_.SourceIP) -and ($g.SourceHostname -eq $_.SourceHostname)){ 305 | $_.TimeCreated.ToString("yyyy-MM-ddTHH:mm:ssZ") 306 | } 307 | } 308 | 309 | $entry = [PSCustomObject]@{ 310 | 'LogonTimes' = $times 311 | 'UserName' = $g.UserName 312 | 'SID' = $g.SID.Value 313 | 'Destination' = $g.Destination 314 | 'Description' = $g.Description 315 | 'EventID' = $g.EventID 316 | 'LogonType' = $g.LogonType 317 | 'SourceIP' = $g.SourceIP 318 | 'SourceHostname' = $g.SourceHostname 319 | 'LogonCount' = $times.Count 320 | } 321 | $line = $entry | ConvertTo-Json -compress 322 | $line = $line.Trim() 323 | $line += $NewLine 324 | $line 325 | } 326 | $results = $grouped 327 | $file = $OutPath + "BlauHaunt_" + $Hostname + "_RDPCon" + ".json" 328 | $counter = 1 329 | while (Test-Path $file -PathType leaf) { 330 | $file = $OutPath + "BlauHaunt_" + $Hostname + "_RDPCon_" + $counter + ".json" 331 | $counter++ 332 | } 333 | $results | Out-File -FilePath $file -Encoding ascii 334 | } 335 | 336 | # Query security events using Get-WinEvent 337 | try{ 338 | if ($Path.length -eq 0){ 339 | Write-Output "collecting Security events from current system" 340 | $Drive = (Get-WmiObject Win32_OperatingSystem).SystemDrive 341 | $Path = $Drive + "\Windows\system32\winevt\Logs" 342 | } 343 | if ($Recursive){ 344 | Get-ChildItem -Path $Path -Recurse -Filter Security.evtx | 345 | Foreach-Object { 346 | Write-Output "collection Security events from "$_.FullName 347 | $events = Get-WinEvent -FilterHashTable @{Path=$_.FullName; StartTime=$StartDate; EndTime=$EndDate; ID=$securityEventIds} 348 | Write-Output "Security Events collected" 349 | Write-SecurityEvents -Events $events 350 | } 351 | } 352 | else { 353 | Write-Output ("collecting Security events from $Path" + "\Security.evtx") 354 | $events = Get-WinEvent -FilterHashTable @{Path=$Path + "\Security.evtx"; StartTime=$StartDate; EndTime=$EndDate; ID=$securityEventIds} 355 | Write-Output "Security Events collected" 356 | Write-SecurityEvents -Events $events 357 | } 358 | } 359 | catch { 360 | Write-Output("Error on Security Events") 361 | } 362 | 363 | #Query RDP Events 364 | try{ 365 | if ($Recursive){ 366 | Get-ChildItem -Path $Path -Recurse -Filter "Microsoft-Windows-TerminalServices-LocalSessionManager%4Operational.evtx" | 367 | Foreach-Object { 368 | Write-Output "collection LocalSessionManager events from "$_.FullName 369 | $events = Get-WinEvent -FilterHashTable @{Path=$_.FullName; StartTime=$StartDate; EndTime=$EndDate; ID=$operationEventIds} 370 | Write-Output "LocalSessionManager Events collected" 371 | Write-RDPEvents -Events $events 372 | } 373 | } 374 | else{ 375 | Write-Output ("collecting LocalSessionManager events from $Path" + "\Microsoft-Windows-TerminalServices-LocalSessionManager%4Operational.evtx") 376 | $events = Get-WinEvent -FilterHashTable @{Path = $Path + "\Microsoft-Windows-TerminalServices-LocalSessionManager%4Operational.evtx"; StartTime=$StartDate; EndTime=$EndDate; ID=$operationEventIds} 377 | Write-Output "LocalSessionManager Events collected" 378 | Write-RDPEvents -Events $events 379 | } 380 | } 381 | catch { 382 | Write-Output("Error on RDP Events") 383 | } 384 | # Microsoft-Windows-TerminalServices-RemoteConnectionManager%4Operational.evtx 385 | try{ 386 | if ($Recursive){ 387 | Get-ChildItem -Path $Path -Recurse -Filter "Microsoft-Windows-TerminalServices-RemoteConnectionManager%4Operational.evtx" | 388 | Foreach-Object { 389 | Write-Output "collection TerminalServices events from "$_.FullName 390 | $events = Get-WinEvent -FilterHashTable @{Path=$_.FullName; StartTime=$StartDate; EndTime=$EndDate; ID=$sessionEventIds} 391 | Write-Output "TerminalServices Events collected" 392 | Write-RDPConnectionEvents -Events $events 393 | } 394 | } 395 | else{ 396 | Write-Output ("collecting TerminalServices events from $Path" + "\Microsoft-Windows-TerminalServices-RemoteConnectionManager%4Operational.evtx") 397 | $events = Get-WinEvent -FilterHashTable @{Path = $Path + "\Microsoft-Windows-TerminalServices-RemoteConnectionManager%4Operational.evtx"; StartTime=$StartDate; EndTime=$EndDate; ID=$sessionEventIds} 398 | Write-Output "TerminalServices Events collected" 399 | Write-RDPConnectionEvents -Events $events 400 | } 401 | } 402 | catch { 403 | Write-Output("Error on Terminal Events") 404 | } 405 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **Basic Worflow: Set Filters -> click Apply Filters -> click Render** 2 | 3 | # Blauhaunt 4 | A tool collection for filtering and visualizing logon events. Designed to help answering the "Cotton Eye Joe" question (Where did you come from where did you go) in Security Incidents and Threat Hunts. 5 | 6 | ***This tool is designed for experienced DFIR specialists. You may have little to none usage from it without experience in Threat Hunting*** 7 |  8 | 9 | 10 | ## Table of Contents 11 | - [Get started](#get-started) 12 | - [Integration in investigation](#integration-in-investigation) 13 | - [Architecture](#architekture) 14 | - [PowerShell Script](#powershell-script) 15 | - [Velociraptor Artifact](#velociraptor-artifact) 16 | - [Defender 365 KUSTO Query](#defender) 17 | - [Acknowledgements](#acknowledgements) 18 | 19 | ### Interactive User Graph 20 |  21 | ### Heatmap of User activities 22 |  23 | ### Timeline 24 |  25 | 26 | ## Get started 27 | Running Blauhaunt is as simple as that: 28 | 29 | open https://cgosec.github.io/Blauhaunt/app/ since there is no backend no data will leave your local system. *(third party libraries integrated and I do not take any responsibilities of their communication behavior. Imports are in the index.html file on top)* 30 | 31 | run a cmd or bash or what ever you like... 32 | 33 | Then: 34 | 35 | git clone https://github.com/cgosec/Blauhaunt 36 | cd Blauhaunt/app 37 | python -m http.server 38 | 39 | Now you can navigate to http://localhost:8000/ in your browser and start blau haunting the baddies. 40 | 41 | Some random test data is in the directory test_data to get started. However this is just randomly generated and nothing to start investigate with. 42 | 43 | ## Integrate into Velociraptor 44 | 45 | You can use Velociraptors reverse proxy capability to host Blauhaunt directly within your instance. Blauhaunt is Velo Aware. If You do so, Blauhaunt will get the Data automaticall from Velociraptor and you do not have to upload data. 46 | 47 | You need to start a Hunt with the Velo Artifact. You can use the Monitoring Artifact too to get real time data form Velo. 48 | 49 | ### Velo Settings: 50 | 51 |  52 | 53 | see: [Velo Docs](https://docs.velociraptor.app/docs/deployment/references/#GUI.reverse_proxy) 54 | 55 | *hint* I did not get this running having the GUI hosted on windows. But you can use the URI to a hosted instance on a https server there too 56 | 57 | **UPDATE** Since you can set Tags for Hunts now you need to add the Tag "Blauhaunt" to your Hunt to be processed. Otherwise Blauhaunt will not find it! 58 | 59 | Thats basically all you have to do.... :) 60 | 61 | big big thanks to Mike Cohen who helped me with the workflow for CSRF-Tokens and the not documented REST-API of Velo. 62 | 63 | ### Upload Data 64 |  65 | Klick "Upload Data" (surprising isn't it :-P) 66 | 67 |  68 | Upload the json export of the velo artifact or the result(s) of the powershell script here. 69 | *Do not upload the client_info.json or anything in here!* 70 | 71 |  72 | This is optional and just needed for having system tags and their os info. 73 | Upload your client_info.json extract here. This is just an export of the Velociraptor clients() function. 74 | Just use this query: 75 | 76 | SELECT * FROM clients() 77 | 78 | and export the json 79 | 80 |  81 | This is optional too. 82 | Upload a mapping for having IP-Addresses resolved to their hostnames. You need to have a file where there is one col for Hostnames and a col for IP-Addresses. 83 | If a System has multiple IP-Addresses you can have them in this one col separated by an arbitrarily symbol e.G. "/". 84 | 85 | Example: 86 | | Hostname | IP-Addresses | MaybeSomeNotNeededStuff | 87 | | -------- | ------- | ------- | 88 | | System_A | 10.10.10.100 | bonjour | 89 | | System_B | 10.10.10.100 / 10.10.20.100 | hello | 90 | | System_C | 10.10.10.100 | hola | 91 | 92 |  93 | 94 | Once a proper file is selected a delimiter (if non is specified a comma is expected). And click Load Map. 95 | 96 |  97 | 1. Choose the name of the col where the hostname is in 98 | 2. OPTIONAL: Specify if there are any entries you want to exclude from parsing e.g. lines having an "UNKNOWN" in the Hostname Ip Mapping. 99 | 3. Choose the name of the col where the IP-Address is in 100 | 4. Specify the delimiter for multiple IP-Addresses in this line 101 | When everything is correct click  102 | 103 | When done click  104 | 105 |  106 | If everything was processed as intended you should now see the number of total nodes and edges 107 | 108 | ### Filtering 109 | 110 | Click  to open the sidebar. 111 | 112 |  113 | 114 | The Filter Sidepar shows up 115 | 116 | **MOST FILTERS HAVE TOOLTIPS SO I WILL NOT EXPLAIN EVERY FILTER IN DETAIL** 117 | 118 |  119 | 120 | Filter for a time span for activities. 121 | 122 |  123 | 124 | The Daily times filter specifies from what time we are interested in the events. This is useful if nightly user logons are not common in your environment. This is regardless of the date - that means in your timespan only events that occurred during that hourly timespan are in the set. (Works over night like in the example picture too) 125 | 126 |  127 | 128 | **Highlighted**: You can permanently highlight edges by holding CTRL and clicking on them. This also works for every element where temporary highlighting is actice - just hold CRL and click on the element to highlight edges permanently. (Elements are e.g. Timeline on the left; Stats on mouse over; when clicking the destination host) 129 | 130 | **ToSelf**: By default events where source and destination are the same node are not displayed. If you want to display them active it by clicking. 131 | 132 |  133 | 134 | Filtering for EventIDs is a good idea to reduce the data. There is no difference in choosing all or none. 135 | 136 |  137 | 138 | Logon Types are only relevant for 4624 or 4625 events. I assume you know them already when you are using this tool. 139 | 140 |  141 | 142 | Filtering for Tags only is available when client infos are uploaded. Those are your tags specified in Velociraptor for the Systems. It does not have an effect if all on none are chosen. 143 | Those apply only for the source not for the destination system. 144 | 145 | ### Source: System or User 146 | Usually I am rather focused on system -> system activity in favour of identifying the initial access. Since there are a lot of situations you want to focus on user behavior you can choose what your source should be: System or User. 147 | 148 |  149 | 150 | ### Render Graph, Timeline or Heatmap 151 | When your filters are set you need to press render to display the results. 152 |  153 | 154 | #### Graph 155 | The default Graph calculates the position of systems according to their activitie median time (Y-Axis) and their total number of connections (X-Axis). 156 | 157 | **Y-Axis**: Calculated Activitie time early-top to latest-down 158 | 159 | **X-Axis**: The more centered a system is, the more connections have this system either as source or destination. Left to right is randomly distributed. (The more outside the less active a system has been) 160 | 161 | **Size**: The Size of the nodes indicates their outgoing activities 162 | 163 | The Graph is calculated every time before rendering. Position and size is always relative according to the filters set. 164 | 165 |  166 | 167 | When clicking on a Node you can get further systems information. (Some need the clients() output like OS or Tags. 168 | 169 | IPs can be more than one. When data is loaded every Event that has the hostname and an IP in it, will create a list that is presented here. (Multiple entries can be e.g. because of NAT-Devices or Multiple Network Adapters / IP Changes) 170 | 171 |  172 | 173 | When clicking on an edge you get further information about the connection. You can open up a list of Timestamps that shows you when this event has occured. 174 | 175 | 176 | #### Timeline 177 | 178 | The Timeline is the timeline... 179 | 180 | #### Heatmap 181 | 182 | The heatmap gives you a quick overview of the usual day by day behavior of users. You can click on a day to quickly switch to the graph of the day and the users connections. 183 | 184 | The color indicator is not per user but in total. It takes account of your filters. 185 | 186 | 187 | If you want to change from one view to another: choose the view you need and then click render. 188 | 'Be careful with Timeline! Few nodes and edges can still have a huge timeline!* Checking the Stats  is a good idea before rendering a timeline. 189 | 190 | ### Graph Style 191 | 192 | You can choose between some variations... 193 | 194 | ### Tag vizualisation 195 | 196 | You can choose a color for a Tag. The number to specify indicates the priorities when multiple Tags match. The highest number wins. 197 | 198 | ### Exports 199 | 200 | You can Export: 201 | 202 | - Timeline as CSV 203 | - Graph as PNG / JPEG 204 | - GraphJSON (from the library cytoscape) 205 | 206 | ### Stats 207 | 208 | Stats give you a good indication for what to filter out or to pivot for when starting the investigation. 209 | Stats take account of your filters. 210 | 211 |  212 | 213 | System Stats: 214 | - To Systems = Number of Systems connected to followed by (Sum of connections to systems in total) 215 | - From Systems = Number of Systems that connected to this System followed by (Sum of connections to this systems in total) 216 | - Users out = Number of Users that were observed connection to other systems from this System 217 | - Users in = Number of Users that were observed connecting to this System 218 | 219 |  220 | 221 | User Stats: 222 | - To Systems = Number of Systems the User connected to followed by (Sum of connections in total) 223 | 224 | 225 | ## Integration in investigation 226 | I recommend using Blauhaunt with [Velociraptor](https://github.com/Velocidex/velociraptor) since it is the fastest way to get data from multiple systems. The Blauhaunt import format for event data and client info is the one that can be exported from Velo. 227 | The blauhaunt_script.ps1 works well if you prefer working with e.g. [KAPE](https://www.kroll.com/en/insights/publications/cyber/kroll-artifact-parser-extractor-kape) triage data. 228 | 229 | Blauhaunt really gets useful if you have multiple systems to identify your next pivot system or sus users. Blauhaunt standalone will not magically bring you to the compromised systems and users. But if you have hundreds of systems to check it really speeds up your game. 230 | 231 | ### Example workflow 232 | 233 | #### Known compromised system 234 | (e.g. from a Velo hunt) -> Check in Blauhaunt what users connected to this system -> sus user -> sus systems -> further sus users -> the story goes on. You have good chances identifying the systems where deeper forensics will speed you up in your hunt. 235 | If you e.g. identify compromised users on that system again you can go back to Blauhaunt and repeat the game. 236 | 237 | #### No idea where to start 238 | With several filters Blauhaunt gives you statistical and visual possibilities identifying unusual connections. You can e.g. check for user activities occurring at night. Or simply see a logon fire coming form a system where an attacker is enumerating the AD-Infrastructure. 239 | 240 | #### Lucky shot 241 | If you are really lucky and have a noisy attacker + solid administration in the network, Blauhaunt can potentially deliver you an optical attack map with the timeline of compromised systems along the y-axis in the center. 242 | 243 | ## Architecture 244 | Blauhaunt is designed to run entirely without a backend system. 245 | I suggest simply starting a python http server on the local system from a shell in the directory where the index.html is in with this command: 246 | 247 | python -m http.server 248 | 249 | if you are using linux likely you have to type python3 instead of python - but if you are using this tool you should be technical skilled enough to figure that out yourself ;) 250 | 251 | *Some day I will create a backend in Django with an API to get realtime data to display for better threat hunting* 252 | 253 | ### Default Layout 254 | The layout of the graph is calculated according to the set filters. 255 | The icon size of a node is calculated by its activities within the set filters. 256 | The x-axis position of a node is calculated by its outgoing connections. Nodes having many outgoing connections are rather in the center of the graph. Nodes with fewer outgoing connections are at the left and the right of the graph. 257 | The y-axis is calculated by the first quatile of the nodes activity time. 258 | 259 | To not have too many nodes at the same spot there is some movement when there are too many on the same spot. 260 | 261 | The other layouts are defaults from the cytoscape universe that can be chosen as well. 262 | 263 | 264 | ### Displays 265 | description comming soon 266 | 267 | ### General Data Schema 268 | There are three types of data - only the event data is mandatory 269 | 270 | #### Event Data 271 | This is the input Schema for the Event data that is needed by Blauhaunt to process it: 272 | 273 | { 274 | "LogonTimes":[ 275 | "2023-07-28T20:30:19Z", 276 | "2023-07-27T21:12:12Z", 277 | "2023-07-27T21:10:49Z" 278 | ], 279 | "UserName":"Dumdidum", 280 | "SID":"-", 281 | "Destination":"Desti-LAPTOP", 282 | "Description":"using explicit credentials", 283 | "Distinction": "SomeCustomFieldToDistionctEdgesAndFilterFor" 284 | "EventID":4648, 285 | "LogonType":"-", 286 | "SourceIP":"-", 287 | "SourceHostname":"Sourci-LAPTOP", 288 | "LogonCount":3 289 | } 290 | ***To correctly process the files each dataset starting with { and ending with } must be in a new line*** 291 | 292 | #### Client Info 293 | { 294 | "os_info": { 295 | "hostname": "Desti-LAPTOP" 296 | "release": "Windows 10" 297 | }, 298 | "labels": [ 299 | "Touched", 300 | "C2", 301 | "CredDumped" 302 | ] 303 | } 304 | ***To correctly process the files each dataset starting with { and ending with } must be in a new line*** 305 | 306 | 307 | #### Host IP Mapping 308 | Can be any CSV File. Delimiter can be specified and cols for Hostname and IP can be choosen 309 | 310 | 311 | ## PowerShell Script (deprectated - use the quick velo instead) 312 | blauhaunt_script.ps1 313 | If you face any issues with execution policy the easiest thing to do is to spawn a powershell with execution policy bypass like this: 314 | 315 | PowerShell.exe -ExecutionPolicy Bypass powershell 316 | 317 | To get information about usage and parameters use Get-Help 318 | 319 | Get-Help blauhaunt_script.ps1 -Detailed 320 | 321 | ### Usage 322 |  323 | 324 | Depending on the size, StartDate and EndDate this can take quiet some time so be a little patient 325 | 326 | ## Velociraptor Artifact 327 | This speeds up collecting the relevant data on scale. 328 | I recommend creating a notebook (template may be provided soon here too) where all the results are listed. 329 | You can simply take the json export from this artefact to import it into Blauhaunt 330 | 331 | The client_info import is designed to work directly with the client_info from Velociraptor too. You can simply export the json file and upload it into Blauhaunt. 332 | 333 | ### Usage 334 | 335 | If you want to parse event logs collected from a system offline using velociraptor, you can do so like this: 336 | 337 | .\velociraptor*.exe artifacts --definitions Blauhaunt\parser\velociraptor\ collect --format=jsonl Custom.Windows.EventLogs.Blauhaunt --args Security='C:\my\awesome\storage\path\Security.evtx' --args System='C:\my\awesome\storage\path\System.evtx' --args LocalSessionManager='C:\my\awesome\storage\path\Microsoft-Windows-TerminalServices-LocalSessionManager%4Operational.evtx' --args RemoteConnectionManager='C:\my\awesome\storage\path\Microsoft-Windows-TerminalServices-RemoteConnectionManager%4Operational.evtx' --args RDPClientOperational='C:\my\awesome\storage\path\Microsoft-Windows-TerminalServices-RDPClient%4Operational.evtx' 338 | 339 | If you dislike typing long paths, feel free to use the provided quick script: 340 | 341 | .\quick_velo.ps1 -EventLogDirectory C:\my\awesome\storage\path 342 | 343 | ## Defender 344 | 345 | You can import Data from Defender365 into Blauhaunt by using this Hunting Query: 346 | 347 | [Defender 365 Query](https://github.com/cgosec/Blauhaunt/blob/main/parser/Defender365_Query.md) 348 | 349 | run the query, export the csv and direktly load it into Blauhaunt... 350 | 351 | 352 | ## Acknowledgements 353 | - [SEC Consult](https://sec-consult.com/de/) This work was massively motivated by my work in and with the SEC Defence team 354 | - [Velociraptor](https://github.com/Velocidex/velociraptor/) is the game changer making it possible to collect the data to display at scale (tested with > 8000 systems already!) 355 | - [Cytoscape.js](https://js.cytoscape.org/) is the library making the interactive graph visualisation possible 356 | - [LogonTracer](https://github.com/JPCERTCC/LogonTracer) inspired the layout and part of the techstack of this project 357 | - [CyberChef](https://gchq.github.io/CyberChef/) inspired the idea of creating a version of Blauhaunt running without backend system all browser based 358 | 359 | 360 | (The icon is intentionally shitty - this is how I actually look while hunting... just the look in the face not the big arms though :-P ) 361 | -------------------------------------------------------------------------------- /app/static/js/veloAPI.js: -------------------------------------------------------------------------------- 1 | let artifactName = "Custom.Windows.EventLogs.Blauhaunt" 2 | let monitoringArtifact = "Custom.Windows.Events.Blauhaunt" 3 | let velo_url = window.location.origin 4 | let BLAUHAUNT_TAG = "Blauhaunt" 5 | let header = {} 6 | checkForVelociraptor() 7 | 8 | function selectionModal(title, selectionList) { 9 | // remove duplicates from selectionList 10 | selectionList = [...new Set(selectionList)] 11 | let modal = new Promise((resolve, reject) => { 12 | // create modal 13 | let modal = document.createElement("div"); 14 | modal.id = "modal"; 15 | modal.className = "modal"; 16 | let modalContent = document.createElement("div"); 17 | modalContent.className = "modal-content"; 18 | let modalHeader = document.createElement("h2"); 19 | modalHeader.innerHTML = title; 20 | modalContent.appendChild(modalHeader); 21 | let modalBody = document.createElement("div"); 22 | modalBody.className = "modal-body"; 23 | selectionList.forEach(option => { 24 | let notebookButton = document.createElement("button"); 25 | notebookButton.innerHTML = option; 26 | notebookButton.onclick = function () { 27 | modal.remove(); 28 | return option; 29 | } 30 | modalBody.appendChild(notebookButton); 31 | }); 32 | modalContent.appendChild(modalBody); 33 | modal.appendChild(modalContent); 34 | document.body.appendChild(modal); 35 | // show modal 36 | modal.style.display = "block"; 37 | // close modal when clicked outside of it 38 | window.onclick = function (event) { 39 | if (event.target === modal) { 40 | modal.remove(); 41 | return null; 42 | } 43 | } 44 | }); 45 | return modal; 46 | } 47 | 48 | function getNotebook(huntID) { 49 | let notebooks = [] 50 | fetch(velo_url + '/api/v1/GetHunt?hunt_id=' + huntID, {headers: header}).then(response => { 51 | return response.json() 52 | }).then(data => { 53 | let artifacts = data.artifacts; 54 | let notebookID = "" 55 | artifacts.forEach(artifact => { 56 | notebookID = "N." + huntID 57 | if (artifact === artifactName) { 58 | notebooks.push(notebookID); 59 | } 60 | }); 61 | if (notebooks.length === 0) { 62 | return; 63 | } 64 | // if there are more notebooks wit the artifact name, show a modal to select the notebook to use 65 | if (notebooks.length > 1) { 66 | selectionModal("Select Notebook", notebooks).then(selectedNotebook => { 67 | if (selectedNotebook === null) { 68 | return; 69 | } 70 | getCells(selectedNotebook); 71 | }); 72 | } else { 73 | getCells(notebooks[0]); 74 | } 75 | }); 76 | } 77 | 78 | function getCells(notebookID) { 79 | fetch(velo_url + `/api/v1/GetNotebooks?notebook_id=${notebookID}&include_uploads=true`, {headers: header}).then(response => { 80 | // get the X-Csrf-Token form the header of the response 81 | localStorage.setItem('csrf-token', response.headers.get("X-Csrf-Token")) 82 | return response.json() 83 | }).then(data => { 84 | console.debug("Notebook Data:") 85 | console.debug(data) 86 | let cells = data.items; 87 | if (cells.length > 1) { 88 | let cellIDs = {} 89 | cells.forEach(cell => { 90 | cell.cell_metadata.forEach(metadata => { 91 | let suffix = "" 92 | let i = 0 93 | while (cellIDs[metadata.cell_id + suffix] !== undefined) { 94 | suffix = "_" + i 95 | } // check if the cell_id is already in the list, if so add a suffix to it 96 | cellIDs[metadata.cell_id + suffix] = {cell_id: metadata.cell_id, version: metadata.timestamp}; 97 | }); 98 | }); 99 | selectionModal("Select Cell", cellIDs.keys()).then(selectedCell => { 100 | if (selectedCell === null) { 101 | return; 102 | } 103 | updateData(notebookID, cellIDs[selectedCell].cell_id, cellIDs[selectedCell].version, localStorage.getItem('csrf-token')); 104 | }); 105 | } 106 | cells.forEach(cell => { 107 | cell.cell_metadata.forEach(metadata => { 108 | updateData(notebookID, metadata.cell_id, metadata.timestamp, localStorage.getItem('csrf-token')); 109 | }); 110 | }); 111 | }); 112 | } 113 | 114 | function updateData(notebookID, cellID, version, csrf_token) { 115 | header["X-Csrf-Token"] = csrf_token 116 | fetch(velo_url + '/api/v1/UpdateNotebookCell', { 117 | method: 'POST', 118 | headers: header, 119 | body: JSON.stringify({ 120 | "notebook_id": notebookID, 121 | "cell_id": cellID, 122 | "env": [{"key": "ArtifactName", "value": artifactName}], 123 | "input": "\n/*\n# BLAUHAUNT\n*/\nSELECT * FROM source(artifact=\"" + artifactName + "\")\n", 124 | "type": "vql" 125 | }) 126 | }).then(response => { 127 | return response.json() 128 | }).then(data => { 129 | console.debug("Notebook Data:") 130 | console.debug(data) 131 | loadData(notebookID, data.cell_id, data.current_version); 132 | }); 133 | } 134 | 135 | let dataRows = [] 136 | 137 | function loadData(notebookID, cellID, version, startRow = 0, toRow = 1000) { 138 | fetch(velo_url + `/api/v1/GetTable?notebook_id=${notebookID}&client_id=&cell_id=${cellID}-${version}&table_id=1&TableOptions=%7B%7D&Version=${version}&start_row=${startRow}&rows=${toRow}&sort_direction=false`, 139 | {headers: header} 140 | ).then(response => { 141 | return response.json() 142 | }).then(data => { 143 | console.debug("Cell Data:") 144 | console.debug(data) 145 | if (!data.rows) { 146 | console.debug("no data found") 147 | return; 148 | } 149 | let keys = data.columns; 150 | data.rows.forEach(row => { 151 | let rowData = JSON.parse(row.json) 152 | let entry = {} 153 | for (i = 0; i < rowData.length; i++) { 154 | entry[keys[i]] = rowData[i]; 155 | } 156 | dataRows.push(JSON.stringify(entry)); 157 | }); 158 | // show loading spinner 159 | document.getElementById("loading").style.display = "block"; 160 | processJSONUpload(dataRows.join("\n")).then(() => { 161 | document.getElementById("loading").style.display = "none"; 162 | }); 163 | // if there are more rows, load them 164 | if (data.total_rows > toRow) { 165 | loadData(notebookID, cellID, version, startRow + toRow, toRow + 1000); 166 | } 167 | storeDataToIndexDB(header["Grpc-Metadata-Orgid"]); 168 | }); 169 | } 170 | 171 | function getHunts(orgID) { 172 | velo_url = window.location.origin 173 | const oldAPI = '/api/v1/ListHunts?count=2000&offset=0&summary=true&user_filter='; 174 | const newAPI = "/api/v1/GetHuntTable?version=1&start_row=0&rows=20000&sort_direction=false" 175 | fetch(velo_url + newAPI, {headers: header}).then(response => { 176 | return response.json() 177 | }).then(data => { 178 | try { 179 | console.debug(data) 180 | let keys = data.columns; 181 | let huntList = [] 182 | for (let hunt of data.rows) { 183 | let h = {} 184 | let huntData = JSON.parse(hunt.json); 185 | for (let i = 0; i < keys.length; i++) { 186 | h[keys[i]] = huntData[i]; 187 | } 188 | h.Tags = h.Tags || [] // to prevent errors when Tags is not set 189 | huntList.push(h); 190 | } 191 | huntList.forEach(hunt => { 192 | console.debug(hunt) 193 | console.debug(hunt.Tags.includes(BLAUHAUNT_TAG)) 194 | if (hunt.Tags.includes(BLAUHAUNT_TAG)) { 195 | console.debug("Blauhaunt Hunt found:") 196 | console.debug(hunt) 197 | getNotebook(hunt.HuntId); 198 | } 199 | }); 200 | } catch (error) { 201 | console.debug(error) 202 | console.debug("error in getHunts") 203 | } 204 | }) 205 | } 206 | 207 | function updateClientInfoData(clientInfoNotebook, cellID, version) { 208 | header["X-Csrf-Token"] = localStorage.getItem('csrf-token') 209 | fetch(velo_url + '/api/v1/UpdateNotebookCell', { 210 | method: 'POST', 211 | headers: header, 212 | body: JSON.stringify({ 213 | "notebook_id": clientInfoNotebook, 214 | "cell_id": cellID, 215 | "env": [{"key": "ArtifactName", "value": artifactName}], 216 | "input": "SELECT * FROM clients()\n", 217 | "type": "vql" 218 | }) 219 | }).then(response => { 220 | return response.json() 221 | }).then(data => { 222 | console.debug("Notebook Data:") 223 | console.debug(data) 224 | cellID = data.cell_id; 225 | version = data.current_version; 226 | let timestamp = data.timestamp; 227 | loadFromClientInfoCell(clientInfoNotebook, cellID, version, timestamp); 228 | }); 229 | } 230 | 231 | function getClientInfoFromVelo() { 232 | fetch(velo_url + '/api/v1/GetNotebooks?count=1000&offset=0', {headers: header}).then(response => { 233 | localStorage.setItem('csrf-token', response.headers.get("X-Csrf-Token")) 234 | return response.json() 235 | }).then(data => { 236 | let notebooks = data.items; 237 | if (!notebooks) { 238 | createClientinfoNotebook() 239 | } else { 240 | let clientInfoNotebook = "" 241 | notebooks.forEach(notebook => { 242 | let notebookID = notebook.notebook_id; 243 | notebook.cell_metadata.forEach(metadata => { 244 | let cellID = metadata.cell_id; 245 | fetch(velo_url + `/api/v1/GetNotebookCell?notebook_id=${notebookID}&cell_id=${cellID}`, {headers: header}).then(response => { 246 | return response.json() 247 | }).then(data => { 248 | let query = data.input; 249 | if (query.trim().toLowerCase() === 'select * from clients()') { 250 | let version = metadata.current_version; 251 | let timestamp = metadata.timestamp; 252 | updateClientInfoData(notebookID, cellID, version, timestamp); 253 | } 254 | }); 255 | }); 256 | }); 257 | } 258 | }); 259 | } 260 | 261 | function createClientinfoNotebook() { 262 | header["X-Csrf-Token"] = localStorage.getItem('csrf-token') 263 | fetch("/api/v1/NewNotebook", { 264 | headers: header, 265 | "referrerPolicy": "strict-origin-when-cross-origin", 266 | "body": "{\"name\":\"Blauhaunt Clientinfo\",\"description\":\"Auto created\",\"public\":true,\"artifacts\":[\"Notebooks.Default\"],\"specs\":[]}", 267 | "method": "POST", 268 | "mode": "cors", 269 | "credentials": "include" 270 | }).then(response => { 271 | return response.json().then(data => { 272 | console.debug("Notebook for client info created") 273 | console.debug(data) 274 | let clientInfoNotebook = data.notebook_id; 275 | let cellID = data.cell_metadata[0].cell_id; 276 | let version = data.cell_metadata[0].current_version; 277 | fetch("/api/v1/UpdateNotebookCell", { 278 | headers: header, 279 | "body": `{"notebook_id":"${clientInfoNotebook}","cell_id":"${cellID}","type":"vql","currently_editing":false,"input":"select * from clients()"}`, 280 | "method": "POST", 281 | "mode": "cors", 282 | "credentials": "include" 283 | }).then(response => { 284 | return response.json().then(data => { 285 | console.debug("Notebook Data:") 286 | console.debug(data) 287 | cellID = data.cell_id; 288 | version = data.current_version; 289 | let timestamp = data.timestamp; 290 | loadFromClientInfoCell(clientInfoNotebook, cellID, version, timestamp); 291 | }); 292 | }); 293 | }) 294 | }); 295 | } 296 | 297 | function loadFromClientInfoCell(notebookID, cellID, version, timestamp, startRow = 0, toRow = 1000) { 298 | fetch(velo_url + `/api/v1/GetTable?notebook_id=${notebookID}&client_id=&cell_id=${cellID}-${version}&table_id=1&TableOptions=%7B%7D&Version=${timestamp}&start_row=${startRow}&rows=${toRow}&sort_direction=false`, 299 | {headers: header} 300 | ).then(response => { 301 | return response.json() 302 | }).then(data => { 303 | console.debug("Client Data:") 304 | console.debug(data) 305 | let clientIDs = [] 306 | let keys = data.columns; 307 | let clientRows = [] 308 | data.rows.forEach(row => { 309 | row = JSON.parse(row.json); 310 | let entry = {} 311 | for (i = 0; i < row.length; i++) { 312 | entry[keys[i]] = row[i]; 313 | } 314 | clientRows.push(JSON.stringify(entry)); 315 | console.debug(entry) 316 | clientIDs.push(entry["client_id"]); 317 | }); 318 | // show loading spinner 319 | loadClientInfo(clientRows.join("\n")) 320 | caseData.clientIDs = clientIDs; 321 | // if there are more rows, load them 322 | if (data.total_rows > toRow) { 323 | loadFromClientInfoCell(notebookID, cellID, version, timestamp, startRow + toRow, toRow + 1000); 324 | } 325 | }); 326 | 327 | } 328 | 329 | 330 | function getFromMonitoringArtifact() { 331 | let notebookIDStart = "N.E." + monitoringArtifact 332 | console.debug("checking for monitoring artifact data...") 333 | // iterate over notebooks to find the one with the monitoring artifact 334 | // check if caseData has clientMonitoringLatestUpdate set 335 | if (caseData.clientMonitoringLatestUpdate === undefined) { 336 | caseData.clientMonitoringLatestUpdate = {} 337 | } 338 | if (caseData.clientIDs) { 339 | caseData.clientIDs.forEach(clientID => { 340 | console.debug("checking monitoring artifact for clientID: " + clientID) 341 | let latestUpdate = caseData.clientMonitoringLatestUpdate[clientID] || 0; 342 | fetch(velo_url + `/api/v1/GetTable?client_id=${clientID}&artifact=${monitoringArtifact}&type=CLIENT_EVENT&start_time=${latestUpdate}&end_time=9999999999&rows=10000`, { 343 | headers: header 344 | }).then(response => { 345 | return response.json() 346 | }).then(data => { 347 | console.debug("monitoring data for clientID: ") 348 | console.debug(data) 349 | if (data.rows === undefined) { 350 | return; 351 | } 352 | let keys = data.columns; 353 | let rows = data.rows; 354 | let serverTimeIndex = data.columns.indexOf("_ts"); 355 | let monitoringData = [] 356 | let maxUpdatedTime = 0; 357 | rows.forEach(row => { 358 | row = JSON.parse(row.json); 359 | console.debug(`row time: ${row[serverTimeIndex]}, lastUpdatedTime: ${latestUpdate}`) 360 | if (row[serverTimeIndex] > latestUpdate) { 361 | if (row[serverTimeIndex] > maxUpdatedTime) { 362 | console.debug("updating maxUpdatedTime to" + row[serverTimeIndex]) 363 | maxUpdatedTime = row[serverTimeIndex]; 364 | } 365 | let entry = {} 366 | keys.forEach((key, index) => { 367 | entry[key] = row[index]; 368 | }); 369 | if (entry) { 370 | console.debug(entry) 371 | monitoringData.push(JSON.stringify(entry)); 372 | } 373 | } 374 | }); 375 | caseData.clientMonitoringLatestUpdate[clientID] = maxUpdatedTime; 376 | if (monitoringData.length > 0) { 377 | console.debug("monitoring data for clientID: " + clientID + " is being processed with " + monitoringData.length + " entries") 378 | processJSONUpload(monitoringData.join("\n")).then(() => { 379 | console.debug("monitoring data processed"); 380 | storeDataToIndexDB(header["Grpc-Metadata-Orgid"]); 381 | }); 382 | } 383 | }); 384 | }); 385 | } 386 | } 387 | 388 | function changeBtn(replaceBtn, text, ordID) { 389 | let newBtn = document.createElement("button"); 390 | // get child btn from replaceBtn and copy the classes to the new btn 391 | newBtn.className = replaceBtn.children[0].className; 392 | replaceBtn.innerHTML = "" 393 | newBtn.innerText = text; 394 | newBtn.addEventListener("click", evt => { 395 | evt.preventDefault() 396 | getClientInfoFromVelo(); 397 | getHunts(ordID); 398 | }); 399 | replaceBtn.appendChild(newBtn) 400 | } 401 | 402 | function loadDataFromDB(orgID) { 403 | // check if casedata with orgID is already in indexedDB 404 | retrieveDataFromIndexDB(orgID); 405 | } 406 | 407 | function syncFromMonitoringArtifact() { 408 | return setInterval(getFromMonitoringArtifact, 60000); 409 | } 410 | 411 | function stopMonitoringAync(id) { 412 | clearInterval(id); 413 | } 414 | 415 | function createSyncBtn() { 416 | let syncBtn = document.createElement("input"); 417 | /* 418 |