├── static └── .gitkeep ├── .gitignore ├── VERSION.txt ├── data ├── payloads │ ├── scanner.sh │ └── msf_extract.rb ├── payload │ └── 90ef8eaa-01b7-4e98-9070-105eca3bac39.yml └── abilities │ ├── reconnaissance │ └── 567eaaba-94cc-4a27-83f8-768e5638f4e1.yml │ └── resource-development │ └── bed8f28e-c0ed-463e-9e31-d5607e5473df.yml ├── README.md ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── question.md │ ├── bug_report.md │ └── feature_request.md └── pull_request_template.md ├── hook.py ├── stores └── accessStore.js ├── app └── access_api.py ├── gui └── views │ └── access.vue └── templates └── access.html /static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea/* 3 | **/__pycache__ 4 | -------------------------------------------------------------------------------- /VERSION.txt: -------------------------------------------------------------------------------- 1 | 2.9.0-d1394c1508a1c89fc5b59651fce7abec 2 | -------------------------------------------------------------------------------- /data/payloads/scanner.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo '[+] Starting basic NMAP scan' 4 | nmap -Pn $1 5 | echo '[+] Complete with module' -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MITRE Caldera Plugin: Access 2 | 3 | Plugin supplying Caldera with red-team tools and functions, as applied to initial access. 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | contact_links: 2 | - name: Documentation 3 | url: https://caldera.readthedocs.io/en/latest/ 4 | about: Your question may be answered in the documentation 5 | -------------------------------------------------------------------------------- /data/payload/90ef8eaa-01b7-4e98-9070-105eca3bac39.yml: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | 4 | id: 90ef8eaa-01b7-4e98-9070-105eca3bac39 5 | name: Access Payloads 6 | standard_payloads: 7 | msf_extract.py: 8 | description: GET METASPLOIT EXPLOITS 9 | id: 169b74a7-e31b-4dd6-b6ff-721f3a615cc7 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U00002753 Question" 3 | about: Support questions 4 | title: '' 5 | labels: question 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | 13 | 18 | -------------------------------------------------------------------------------- /data/abilities/reconnaissance/567eaaba-94cc-4a27-83f8-768e5638f4e1.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - id: 567eaaba-94cc-4a27-83f8-768e5638f4e1 3 | name: NMAP scan 4 | description: Scan an external host for open ports and services 5 | tactic: reconnaissance 6 | technique: 7 | name: Active Scanning 8 | attack_id: T1595 9 | platforms: 10 | darwin,linux: 11 | sh: 12 | command: | 13 | ./scanner.sh #{target.ip} 14 | timeout: 300 15 | payloads: 16 | - scanner.sh 17 | -------------------------------------------------------------------------------- /data/abilities/resource-development/bed8f28e-c0ed-463e-9e31-d5607e5473df.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - id: bed8f28e-c0ed-463e-9e31-d5607e5473df 3 | name: Load Metasploit Abilities 4 | description: Load Metasploit Abilities 5 | tactic: resource-development 6 | technique: 7 | name: "Obtain Capabilities: Exploits" 8 | attack_id: T1588.005 9 | platforms: 10 | darwin,linux: 11 | sh: 12 | command: | 13 | msfconsole -r msf_extract.rb #{app.contact.http} #{app.api_key.red} 14 | timeout: 1000 15 | payloads: 16 | - msf_extract.rb 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41E Bug report" 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: wbooth 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Desktop (please complete the following information):** 24 | - OS: [e.g. Mac, Windows, Kali] 25 | - Browser [e.g. chrome, safari] 26 | - Version [e.g. 2.8.0] 27 | 28 | **Additional context** 29 | Add any other context about the problem here. 30 | -------------------------------------------------------------------------------- /hook.py: -------------------------------------------------------------------------------- 1 | from app.utility.base_world import BaseWorld 2 | from plugins.access.app.access_api import AccessApi 3 | 4 | name = 'Access' 5 | description = 'A toolkit containing initial access throwing modules' 6 | address = '/plugin/access/gui' 7 | access = BaseWorld.Access.RED 8 | 9 | 10 | async def enable(services): 11 | access_api = AccessApi(services=services) 12 | app = services.get('app_svc').application 13 | app.router.add_static('/access', 'plugins/access/static', append_version=True) 14 | app.router.add_route('GET', '/plugin/access/gui', access_api.landing) 15 | app.router.add_route('POST', '/plugin/access/exploit', access_api.exploit) 16 | app.router.add_route('POST', '/plugin/access/abilities', access_api.abilities) 17 | app.router.add_route('POST', '/plugin/access/executor', access_api.executor) 18 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | (insert summary) 4 | 5 | ## Type of change 6 | 7 | Please delete options that are not relevant. 8 | 9 | - [ ] Bug fix (non-breaking change which fixes an issue) 10 | - [ ] New feature (non-breaking change which adds functionality) 11 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 12 | - [ ] This change requires a documentation update 13 | 14 | ## How Has This Been Tested? 15 | 16 | Please describe the tests that you ran to verify your changes. 17 | 18 | 19 | ## Checklist: 20 | 21 | - [ ] My code follows the style guidelines of this project 22 | - [ ] I have performed a self-review of my own code 23 | - [ ] I have made corresponding changes to the documentation 24 | - [ ] I have added tests that prove my fix is effective or that my feature works 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F680 New Feature Request" 3 | about: Propose a new feature 4 | title: '' 5 | labels: feature 6 | assignees: 'wbooth' 7 | 8 | --- 9 | 10 | **What problem are you trying to solve? Please describe.** 11 | > Eg. I'm always frustrated when [...] 12 | 13 | 14 | **The ideal solution: What should the feature should do?** 15 | > a clear and concise description 16 | 17 | 18 | **What category of feature is this?** 19 | 20 | - [ ] UI/UX 21 | - [ ] API 22 | - [ ] Other 23 | 24 | **If you have code or pseudo-code please provide:** 25 | 26 | 27 | ```python 28 | 29 | ``` 30 | 31 | - [ ] Willing to submit a pull request to implement this feature? 32 | 33 | **Additional context** 34 | Add any other context or screenshots about the feature request here. 35 | 36 | Thank you for your contribution! 37 | -------------------------------------------------------------------------------- /stores/accessStore.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | 3 | export const useAccessStore = defineStore("accessStore", { 4 | state: () => { 5 | return {}; 6 | }, 7 | getters: { 8 | enabledPlugins: (state) => (state.mainConfig ? state.mainConfig.plugins : []) 9 | }, 10 | actions: { 11 | async restRequest(requestType, data, callback = (r) => { 12 | console.log('Fetch Success', r); 13 | }, endpoint = '/api/rest') { 14 | const requestData = requestType === 'GET' ? 15 | {method: requestType, headers: {'Content-Type': 'application/json'}} : 16 | {method: requestType, headers: {'Content-Type': 'application/json'}, body: JSON.stringify(data)} 17 | 18 | fetch(endpoint, requestData) 19 | .then((response) => { 20 | if (!response.ok) { 21 | throw (response.statusText); 22 | } 23 | return response.text(); 24 | }) 25 | .then((text) => { 26 | try { 27 | callback(JSON.parse(text)); 28 | } catch { 29 | callback(text); 30 | } 31 | }) 32 | .catch((error) => console.error(error)); 33 | }, 34 | 35 | async apiV2(requestType, endpoint, body = null, jsonRequest = true) { 36 | let requestBody = { method: requestType }; 37 | if (jsonRequest) { 38 | requestBody.headers = { 'Content-Type': 'application/json' }; 39 | if (body) { 40 | requestBody.body = JSON.stringify(body); 41 | } 42 | } else { 43 | if (body) { 44 | requestBody.body = body; 45 | } 46 | } 47 | 48 | return new Promise((resolve, reject) => { 49 | fetch(endpoint, requestBody) 50 | .then((response) => { 51 | if (!response.ok) { 52 | reject(response.statusText); 53 | } 54 | return response.text(); 55 | }) 56 | .then((text) => { 57 | try { 58 | resolve(JSON.parse(text)); 59 | } catch { 60 | resolve(text); 61 | } 62 | }); 63 | }); 64 | } 65 | }, 66 | }); 67 | -------------------------------------------------------------------------------- /app/access_api.py: -------------------------------------------------------------------------------- 1 | import copy 2 | 3 | from aiohttp import web 4 | from aiohttp_jinja2 import template 5 | 6 | from app.objects.secondclass.c_fact import Fact 7 | from app.service.auth_svc import for_all_public_methods, check_authorization 8 | 9 | 10 | @for_all_public_methods(check_authorization) 11 | class AccessApi: 12 | 13 | def __init__(self, services): 14 | self.data_svc = services.get('data_svc') 15 | self.rest_svc = services.get('rest_svc') 16 | self.auth_svc = services.get('auth_svc') 17 | 18 | @template('access.html') 19 | async def landing(self, request): 20 | search = dict(access=tuple(await self.auth_svc.get_permissions(request))) 21 | abilities = await self.data_svc.locate('abilities', match=search) 22 | tactics = sorted(list(set(a.tactic.lower() for a in abilities))) 23 | obfuscators = [o.display for o in await self.data_svc.locate('obfuscators')] 24 | return dict(agents=[a.display for a in await self.data_svc.locate('agents', match=search)], 25 | abilities=[a.display for a in abilities], tactics=tactics, obfuscators=obfuscators) 26 | 27 | async def exploit(self, request): 28 | data = await request.json() 29 | converted_facts = [Fact(trait=f['trait'], value=f['value']) for f in data.get('facts', [])] 30 | await self.rest_svc.task_agent_with_ability(data['paw'], data['ability_id'], data['obfuscator'], converted_facts) 31 | return web.json_response('complete') 32 | 33 | async def abilities(self, request): 34 | data = await request.json() 35 | agent_search = dict(access=tuple(await self.auth_svc.get_permissions(request)), paw=data['paw']) 36 | agent = (await self.data_svc.locate('agents', match=agent_search))[0] 37 | ability_search = dict(access=tuple(await self.auth_svc.get_permissions(request))) 38 | abilities = await self.data_svc.locate('abilities', match=ability_search) 39 | capable_abilities = await agent.capabilities(list(abilities)) 40 | return web.json_response([a.display for a in capable_abilities]) 41 | 42 | async def executor(self, request): 43 | data = await request.json() 44 | agent_search = dict(access=tuple(await self.auth_svc.get_permissions(request)), paw=data['paw']) 45 | agent = (await self.data_svc.locate('agents', match=agent_search))[0] 46 | ability_search = dict(access=tuple(await self.auth_svc.get_permissions(request)), ability_id=data['ability_id']) 47 | ability = (await self.data_svc.locate('abilities', match=ability_search))[0] 48 | executor = await agent.get_preferred_executor(ability) 49 | if not executor: 50 | return web.json_response(dict(error='Executor not found for ability')) 51 | trimmed_ability = copy.deepcopy(ability) 52 | trimmed_ability.remove_all_executors() 53 | trimmed_ability.add_executor(executor) 54 | return web.json_response(trimmed_ability.display) 55 | -------------------------------------------------------------------------------- /data/payloads/msf_extract.rb: -------------------------------------------------------------------------------- 1 | 2 | 3 | require 'json' 4 | require 'net/http' 5 | require 'securerandom' 6 | require 'uri' 7 | 8 | def get_exploit_info(exploit_name) 9 | exploit = self.framework.modules.create(exploit_name) 10 | description = get_exploit_description(exploit) 11 | privileged = exploit.privileged ? "Elevated" : "" 12 | platforms = exploit.target_platform.platforms.map {|plat| plat.realname } 13 | 14 | params = [] 15 | for name, option in exploit.options 16 | if option.required && option.default.nil? 17 | params.push(name) 18 | end 19 | end 20 | 21 | data = { 22 | "name" => exploit.name, 23 | "description" => description, 24 | "platform" => platforms, 25 | "privilege" => exploit.privileged ? "Elevated" : "", 26 | "module" => exploit.realname, 27 | "params" => params 28 | } 29 | end 30 | 31 | def get_exploit_description(exploit) 32 | description = Rex::Text.compress(exploit.description) 33 | description << "\n\n" 34 | 35 | if (exploit.options.has_options?) 36 | description << "Basic options:\n\n" 37 | description << ::Msf::Serializer::ReadableText.dump_options(exploit, " ") 38 | end 39 | 40 | adv_opts = ::Msf::Serializer::ReadableText.dump_advanced_options(exploit, " ") 41 | description << "\nAdvanced options:\n\n" 42 | description << "#{adv_opts}\n" if (adv_opts and adv_opts.length > 0) 43 | 44 | description << ::Msf::Serializer::ReadableText.dump_references(exploit, " ") 45 | 46 | return description 47 | end 48 | 49 | def convert_to_ability(exploit) 50 | command = 'msfconsole -x "use ' + exploit['module'] + '; ' 51 | for param in exploit['params'] 52 | command += 'set ' + param + ' ' + '#{msf.' + param + '}; ' 53 | end 54 | command += 'run"' 55 | 56 | os = get_os 57 | executor = (os == "windows" ? "cmd" : "sh") 58 | 59 | ability = { 60 | "ability_id" => SecureRandom.uuid, 61 | "tactic" => "metasploit", 62 | "technique_name" => "metasploit", 63 | "technique_id" => "T1349", 64 | "name" => exploit['name'], 65 | "description" => exploit['description'], 66 | "privilege" => exploit['privilege'], 67 | "executors" => [{ 68 | "command" => command, 69 | "timeout" => 60, 70 | "platform" => os 71 | }] 72 | } 73 | return ability 74 | end 75 | 76 | def get_os 77 | if (/cygwin|mswin|mingw|bccwin|wince|emx/ =~ RUBY_PLATFORM) != nil 78 | return "windows" 79 | elsif (/darwin/ =~ RUBY_PLATFORM) != nil 80 | return "darwin" 81 | else 82 | return "linux" 83 | end 84 | end 85 | 86 | def save_ability(ability, c2_uri, c2_key) 87 | uri = URI.parse(c2_uri + '/api/v2/abilities') 88 | header = { 89 | 'Content-Type' => 'text/json', 90 | 'KEY' => c2_key 91 | } 92 | http = Net::HTTP.new(uri.host, uri.port) 93 | request = Net::HTTP::Post.new(uri, header) 94 | request.body = ability.to_json 95 | response = http.request(request) 96 | end 97 | 98 | c2_uri = ARGV.shift || 'http://0.0.0.0:8888' 99 | c2_key = ARGV.shift || 'ADMIN123' 100 | 101 | for exploit_name in self.framework.exploits.keys 102 | exploit = get_exploit_info(exploit_name) 103 | ability = convert_to_ability(exploit) 104 | response = save_ability(ability, c2_uri, c2_key) 105 | end 106 | 107 | exit(true) 108 | 109 | 110 | 111 | quit 112 | -------------------------------------------------------------------------------- /gui/views/access.vue: -------------------------------------------------------------------------------- 1 | 202 | 203 | 204 | div(ref="headerAccess") 205 | h2 Access 206 | p Here you can task any agent with any ability from the database - outside the scope of an operation. This is especially useful for conducting initial access attacks. To do this, deploy an agent locally and task it with either pre-ATT&CK or initial access tactics, pointed at any target. You can even deploy an agent remotely and use it as a proxy to conduct your initial access attacks. To the right, you'll see every ability directly tasked to an agent. 207 | hr 208 | // AGENT SELECTION 209 | 210 | div.mb-6 211 | form 212 | #select-agent.field.has-addons 213 | label.label.mr-5 Select an agent 214 | .control.is-expanded 215 | .select.is-small.is-fullwidth 216 | select(v-on:change="selectAgent()" v-model="selectedAgentPaw") 217 | option(value="" disabled selected) Select an agent 218 | template(v-for="agent of agents" :key="agent.paw") 219 | option(v-bind:value="agent.paw" v-text="`${agent.host} - ${agent.paw}`") 220 | 221 | div.has-text-centered.content(v-show="!selectedAgentPaw") 222 | p Select an agent to get started 223 | 224 | div.mb-3(v-show="selectedAgentPaw") 225 | .is-flex(v-show="selectedAgentPaw") 226 | button.button.is-primary.is-small.mr-6(@click="showRunModal = true") 227 | span.icon 228 | i.fas.fa-running 229 | span Run an Ability 230 | span.mr-6 231 | strong.mr-4 Agent Platform 232 | span(v-text="selectedAgent.platform") 233 | span 234 | strong.mr-4 Compatible Abilities 235 | span(v-text="filteredAbilities.length") 236 | p.has-text-centered.content(v-show="!links.length") No links to show 237 | 238 | div(v-show="selectedAgentPaw && links.length") 239 | table.table.is-striped.is-fullwidth 240 | thead 241 | tr 242 | th order 243 | th name 244 | th tactic 245 | th status 246 | th 247 | tbody 248 | tr.pointer(v-for="(link, index) in links", :key="link.unique") 249 | td(v-text="index + 1") 250 | td(v-text="link.ability.name") 251 | td(v-text="link.ability.tactic") 252 | td(v-text="getLinkStatus(link)" v-bind:class="{ 'has-text-danger': getLinkStatus(link) === 'failed', 'has-text-success': getLinkStatus(link) === 'success' }") 253 | td 254 | button.button.is-small.is-primary(@click="showOutput(link)") Output 255 | 256 | // MODALS 257 | 258 | div.modal(v-bind:class="{ 'is-active': showOutputModal }") 259 | .modal-background(@click="showOutputModal = false") 260 | .modal-card.wide 261 | header.modal-card-head 262 | p.modal-card-title Link Output 263 | section.modal-card-body 264 | label.label Command 265 | pre(v-text="outputCommand") 266 | label.label Standard Output 267 | pre(v-text="outputResult.stdout || '[ no output to show ]'") 268 | footer.modal-card-foot 269 | nav.level 270 | .level-left 271 | .level-right 272 | .level-item 273 | button.button.is-small(@click="showOutputModal = false") Close 274 | 275 | div.modal(v-bind:class="{ 'is-active': showRunModal }") 276 | .modal-background(@click="showRunModal = false") 277 | .modal-card 278 | header.modal-card-head 279 | p.modal-card-title Run an Ability 280 | section.modal-card-body 281 | p.has-text-centered Select an Ability 282 | form 283 | .field.is-horizontal 284 | .field-label.is-small 285 | label.label 286 | span.icon.is-small 287 | i.fas.fa-search 288 | .field-body 289 | .field 290 | .control 291 | input.input.is-small(v-model="searchQuery" placeholder="Search for an ability..." v-on:keyup="searchForAbility()") 292 | .search-results 293 | template(v-for="result in searchResults", :key="result.ability_id") 294 | p(@click="selectAbility(result.ability_id)" v-text="result.name") 295 | form 296 | .field.is-horizontal 297 | .field-label.is-small 298 | label.label Tactic 299 | .field-body 300 | .field 301 | .control 302 | div.select.is-small.is-fullwidth 303 | select(v-model="selectedTactic" v-on:change="selectedAbilityId = ''") 304 | option(disabled selected value="") Choose a tactic 305 | option(v-for="tactic of [...new Set(filteredAbilities.map((e) => e.tactic))]", :key="tactic" :value="tactic") {{ tactic }} 306 | .field.is-horizontal 307 | .field-label.is-small 308 | label.label Technique 309 | .field-body 310 | .field 311 | .control 312 | div.select.is-small.is-fullwidth 313 | select(v-model="selectedTechnique" v-bind:disabled="!selectedTactic" v-on:change="selectedAbilityId = ''") 314 | option(disabled selected value="") Choose a technique 315 | template(:key="exploit.technique_id" v-for="exploit of [...new Set(filteredAbilities.filter((e) => selectedTactic === e.tactic).map((e) => e.technique_id))].map((t) => filteredAbilities.find((e) => e.technique_id === t))") 316 | option(v-bind:value="exploit.technique_id" v-text="exploit.technique_id") 317 | .field.is-horizontal 318 | .field-label.is-small 319 | label.label Ability 320 | .field-body 321 | .field 322 | .control 323 | div.select.is-small.is-fullwidth 324 | select(v-model="selectedAbilityId", v-bind:disabled="!selectedTechnique" v-on:change="selectAbility(selectedAbilityId)") 325 | option(disabled selected value="") Choose an ability 326 | template(v-for="ability of filteredAbilities.filter((e) => selectedTechnique === e.technique_id)", :key="ability.ability_id") 327 | option(v-bind:value="ability.ability_id" v-text="ability.name") 328 | template(v-if="selectedAbilityId") 329 | .content 330 | hr 331 | h3 {{ selectedAbility.name }} 332 | p {{ selectedAbility.description }} 333 | 334 | form.mb-4 335 | .field.is-horizontal 336 | .field-label.is-small 337 | label.label Obfuscator 338 | 339 | .field-body 340 | .field.is-narrow 341 | .control.is-expanded 342 | .select.is-fullwidth.is-small 343 | select(v-model="selectedObfuscator") 344 | template(v-for="obf in obfuscators" :key="obf.name") 345 | option(v-bind:value="obf.name" v-text="obf.name") 346 | 347 | template(v-for="fact in facts" :key="fact.trait") 348 | .field.is-horizontal 349 | .field-label.is-small 350 | label.label {{ fact.trait }} 351 | 352 | .field-body 353 | .field 354 | .control 355 | input.input.is-small.is-fullwidth(v-model="fact.value" placeholder="Enter a value...") 356 | 357 | button.button.is-small.is-primary.is-fullwidth(@click="execute()") Execute 358 | 359 | footer.modal-card-foot 360 | nav.level 361 | .level-left 362 | .level-right 363 | .level-item 364 | button.button.is-small(@click="showRunModal = false") Close 365 | 366 | 367 | 368 | 389 | -------------------------------------------------------------------------------- /templates/access.html: -------------------------------------------------------------------------------- 1 |
8 | Here you can task any agent with any ability from the database - outside the scope of an operation. 9 | This is especially useful for conducting initial access attacks. To do this, deploy an agent locally 10 | and task it with either pre-ATT&CK or initial access tactics, pointed at any target. You can even deploy 11 | an agent remotely and use it as a proxy to conduct your initial access attacks. To the right, you'll 12 | see every ability directly tasked to an agent. 13 |
14 |Select an agent to get started
39 |No links to show
51 || order | 58 |name | 59 |tactic | 60 |status | 61 |62 | |
|---|---|---|---|---|
| 68 | | 69 | | 70 | | 71 | | 72 | 73 | | 74 |
Link Output
87 |Run an Ability
114 |Select an Ability
117 | 136 | 195 | 196 |