├── .github └── workflows │ └── main.yml ├── LICENSE.md ├── README.md ├── beacon_utils.py ├── comm.py ├── extra └── communication_poc.py ├── parse_beacon_config.py ├── requirements.txt ├── samples ├── 10fd211ba97ddf12aecb1e7931d92c3ba37421c362cb1490e0203c1bd88ec141.zip ├── 13e954be0b0c022c392c956e9a800201a75dab7e288230b835bcdd4a9d68253d.zip ├── 320a5f715aa5724c21013fc14bfe0a10893ce9723ebc25d9ae9f06f5517795d4.zip ├── 4d1d732125e4d1a3ba0571e0cd892cf8e0dce854387ee405f75df4dcfb0f616b.zip ├── 5cd19717831e5259d535783be33f86ad7e77f8df25cd8f342da4f4f33327d989.zip └── 7773169ca4ea81203a550dfebe53f091a8c57a3a5b12386e51c5a05194fef3ff.zip ├── setup.py └── test_parse_beacon_config.py /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | # Controls when the action will run. 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the master branch 8 | push: 9 | branches: [ master ] 10 | pull_request: 11 | branches: [ master ] 12 | 13 | # Allows you to run this workflow manually from the Actions tab 14 | workflow_dispatch: 15 | 16 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 17 | jobs: 18 | # This workflow contains a single job called "build" 19 | build: 20 | # The type of runner that the job will run on 21 | runs-on: ubuntu-latest 22 | 23 | # Steps represent a sequence of tasks that will be executed as part of the job 24 | steps: 25 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 26 | - uses: actions/checkout@v2 27 | - uses: actions/setup-python@v2 28 | with: 29 | python-version: '3.8' 30 | 31 | - name: Install dependencies and run test 32 | run: | 33 | pip install -r requirements.txt 34 | python -m unittest 35 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Attribution-NonCommercial-ShareAlike 4.0 International 2 | 3 | ======================================================================= 4 | 5 | Creative Commons Corporation ("Creative Commons") is not a law firm and 6 | does not provide legal services or legal advice. Distribution of 7 | Creative Commons public licenses does not create a lawyer-client or 8 | other relationship. Creative Commons makes its licenses and related 9 | information available on an "as-is" basis. Creative Commons gives no 10 | warranties regarding its licenses, any material licensed under their 11 | terms and conditions, or any related information. Creative Commons 12 | disclaims all liability for damages resulting from their use to the 13 | fullest extent possible. 14 | 15 | Using Creative Commons Public Licenses 16 | 17 | Creative Commons public licenses provide a standard set of terms and 18 | conditions that creators and other rights holders may use to share 19 | original works of authorship and other material subject to copyright 20 | and certain other rights specified in the public license below. The 21 | following considerations are for informational purposes only, are not 22 | exhaustive, and do not form part of our licenses. 23 | 24 | Considerations for licensors: Our public licenses are 25 | intended for use by those authorized to give the public 26 | permission to use material in ways otherwise restricted by 27 | copyright and certain other rights. Our licenses are 28 | irrevocable. Licensors should read and understand the terms 29 | and conditions of the license they choose before applying it. 30 | Licensors should also secure all rights necessary before 31 | applying our licenses so that the public can reuse the 32 | material as expected. Licensors should clearly mark any 33 | material not subject to the license. This includes other CC- 34 | licensed material, or material used under an exception or 35 | limitation to copyright. More considerations for licensors: 36 | wiki.creativecommons.org/Considerations_for_licensors 37 | 38 | Considerations for the public: By using one of our public 39 | licenses, a licensor grants the public permission to use the 40 | licensed material under specified terms and conditions. If 41 | the licensor's permission is not necessary for any reason--for 42 | example, because of any applicable exception or limitation to 43 | copyright--then that use is not regulated by the license. Our 44 | licenses grant only permissions under copyright and certain 45 | other rights that a licensor has authority to grant. Use of 46 | the licensed material may still be restricted for other 47 | reasons, including because others have copyright or other 48 | rights in the material. A licensor may make special requests, 49 | such as asking that all changes be marked or described. 50 | Although not required by our licenses, you are encouraged to 51 | respect those requests where reasonable. More_considerations 52 | for the public: 53 | wiki.creativecommons.org/Considerations_for_licensees 54 | 55 | ======================================================================= 56 | 57 | Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International 58 | Public License 59 | 60 | By exercising the Licensed Rights (defined below), You accept and agree 61 | to be bound by the terms and conditions of this Creative Commons 62 | Attribution-NonCommercial-ShareAlike 4.0 International Public License 63 | ("Public License"). To the extent this Public License may be 64 | interpreted as a contract, You are granted the Licensed Rights in 65 | consideration of Your acceptance of these terms and conditions, and the 66 | Licensor grants You such rights in consideration of benefits the 67 | Licensor receives from making the Licensed Material available under 68 | these terms and conditions. 69 | 70 | 71 | Section 1 -- Definitions. 72 | 73 | a. Adapted Material means material subject to Copyright and Similar 74 | Rights that is derived from or based upon the Licensed Material 75 | and in which the Licensed Material is translated, altered, 76 | arranged, transformed, or otherwise modified in a manner requiring 77 | permission under the Copyright and Similar Rights held by the 78 | Licensor. For purposes of this Public License, where the Licensed 79 | Material is a musical work, performance, or sound recording, 80 | Adapted Material is always produced where the Licensed Material is 81 | synched in timed relation with a moving image. 82 | 83 | b. Adapter's License means the license You apply to Your Copyright 84 | and Similar Rights in Your contributions to Adapted Material in 85 | accordance with the terms and conditions of this Public License. 86 | 87 | c. BY-NC-SA Compatible License means a license listed at 88 | creativecommons.org/compatiblelicenses, approved by Creative 89 | Commons as essentially the equivalent of this Public License. 90 | 91 | d. Copyright and Similar Rights means copyright and/or similar rights 92 | closely related to copyright including, without limitation, 93 | performance, broadcast, sound recording, and Sui Generis Database 94 | Rights, without regard to how the rights are labeled or 95 | categorized. For purposes of this Public License, the rights 96 | specified in Section 2(b)(1)-(2) are not Copyright and Similar 97 | Rights. 98 | 99 | e. Effective Technological Measures means those measures that, in the 100 | absence of proper authority, may not be circumvented under laws 101 | fulfilling obligations under Article 11 of the WIPO Copyright 102 | Treaty adopted on December 20, 1996, and/or similar international 103 | agreements. 104 | 105 | f. Exceptions and Limitations means fair use, fair dealing, and/or 106 | any other exception or limitation to Copyright and Similar Rights 107 | that applies to Your use of the Licensed Material. 108 | 109 | g. License Elements means the license attributes listed in the name 110 | of a Creative Commons Public License. The License Elements of this 111 | Public License are Attribution, NonCommercial, and ShareAlike. 112 | 113 | h. Licensed Material means the artistic or literary work, database, 114 | or other material to which the Licensor applied this Public 115 | License. 116 | 117 | i. Licensed Rights means the rights granted to You subject to the 118 | terms and conditions of this Public License, which are limited to 119 | all Copyright and Similar Rights that apply to Your use of the 120 | Licensed Material and that the Licensor has authority to license. 121 | 122 | j. Licensor means the individual(s) or entity(ies) granting rights 123 | under this Public License. 124 | 125 | k. NonCommercial means not primarily intended for or directed towards 126 | commercial advantage or monetary compensation. For purposes of 127 | this Public License, the exchange of the Licensed Material for 128 | other material subject to Copyright and Similar Rights by digital 129 | file-sharing or similar means is NonCommercial provided there is 130 | no payment of monetary compensation in connection with the 131 | exchange. 132 | 133 | l. Share means to provide material to the public by any means or 134 | process that requires permission under the Licensed Rights, such 135 | as reproduction, public display, public performance, distribution, 136 | dissemination, communication, or importation, and to make material 137 | available to the public including in ways that members of the 138 | public may access the material from a place and at a time 139 | individually chosen by them. 140 | 141 | m. Sui Generis Database Rights means rights other than copyright 142 | resulting from Directive 96/9/EC of the European Parliament and of 143 | the Council of 11 March 1996 on the legal protection of databases, 144 | as amended and/or succeeded, as well as other essentially 145 | equivalent rights anywhere in the world. 146 | 147 | n. You means the individual or entity exercising the Licensed Rights 148 | under this Public License. Your has a corresponding meaning. 149 | 150 | 151 | Section 2 -- Scope. 152 | 153 | a. License grant. 154 | 155 | 1. Subject to the terms and conditions of this Public License, 156 | the Licensor hereby grants You a worldwide, royalty-free, 157 | non-sublicensable, non-exclusive, irrevocable license to 158 | exercise the Licensed Rights in the Licensed Material to: 159 | 160 | a. reproduce and Share the Licensed Material, in whole or 161 | in part, for NonCommercial purposes only; and 162 | 163 | b. produce, reproduce, and Share Adapted Material for 164 | NonCommercial purposes only. 165 | 166 | 2. Exceptions and Limitations. For the avoidance of doubt, where 167 | Exceptions and Limitations apply to Your use, this Public 168 | License does not apply, and You do not need to comply with 169 | its terms and conditions. 170 | 171 | 3. Term. The term of this Public License is specified in Section 172 | 6(a). 173 | 174 | 4. Media and formats; technical modifications allowed. The 175 | Licensor authorizes You to exercise the Licensed Rights in 176 | all media and formats whether now known or hereafter created, 177 | and to make technical modifications necessary to do so. The 178 | Licensor waives and/or agrees not to assert any right or 179 | authority to forbid You from making technical modifications 180 | necessary to exercise the Licensed Rights, including 181 | technical modifications necessary to circumvent Effective 182 | Technological Measures. For purposes of this Public License, 183 | simply making modifications authorized by this Section 2(a) 184 | (4) never produces Adapted Material. 185 | 186 | 5. Downstream recipients. 187 | 188 | a. Offer from the Licensor -- Licensed Material. Every 189 | recipient of the Licensed Material automatically 190 | receives an offer from the Licensor to exercise the 191 | Licensed Rights under the terms and conditions of this 192 | Public License. 193 | 194 | b. Additional offer from the Licensor -- Adapted Material. 195 | Every recipient of Adapted Material from You 196 | automatically receives an offer from the Licensor to 197 | exercise the Licensed Rights in the Adapted Material 198 | under the conditions of the Adapter's License You apply. 199 | 200 | c. No downstream restrictions. You may not offer or impose 201 | any additional or different terms or conditions on, or 202 | apply any Effective Technological Measures to, the 203 | Licensed Material if doing so restricts exercise of the 204 | Licensed Rights by any recipient of the Licensed 205 | Material. 206 | 207 | 6. No endorsement. Nothing in this Public License constitutes or 208 | may be construed as permission to assert or imply that You 209 | are, or that Your use of the Licensed Material is, connected 210 | with, or sponsored, endorsed, or granted official status by, 211 | the Licensor or others designated to receive attribution as 212 | provided in Section 3(a)(1)(A)(i). 213 | 214 | b. Other rights. 215 | 216 | 1. Moral rights, such as the right of integrity, are not 217 | licensed under this Public License, nor are publicity, 218 | privacy, and/or other similar personality rights; however, to 219 | the extent possible, the Licensor waives and/or agrees not to 220 | assert any such rights held by the Licensor to the limited 221 | extent necessary to allow You to exercise the Licensed 222 | Rights, but not otherwise. 223 | 224 | 2. Patent and trademark rights are not licensed under this 225 | Public License. 226 | 227 | 3. To the extent possible, the Licensor waives any right to 228 | collect royalties from You for the exercise of the Licensed 229 | Rights, whether directly or through a collecting society 230 | under any voluntary or waivable statutory or compulsory 231 | licensing scheme. In all other cases the Licensor expressly 232 | reserves any right to collect such royalties, including when 233 | the Licensed Material is used other than for NonCommercial 234 | purposes. 235 | 236 | 237 | Section 3 -- License Conditions. 238 | 239 | Your exercise of the Licensed Rights is expressly made subject to the 240 | following conditions. 241 | 242 | a. Attribution. 243 | 244 | 1. If You Share the Licensed Material (including in modified 245 | form), You must: 246 | 247 | a. retain the following if it is supplied by the Licensor 248 | with the Licensed Material: 249 | 250 | i. identification of the creator(s) of the Licensed 251 | Material and any others designated to receive 252 | attribution, in any reasonable manner requested by 253 | the Licensor (including by pseudonym if 254 | designated); 255 | 256 | ii. a copyright notice; 257 | 258 | iii. a notice that refers to this Public License; 259 | 260 | iv. a notice that refers to the disclaimer of 261 | warranties; 262 | 263 | v. a URI or hyperlink to the Licensed Material to the 264 | extent reasonably practicable; 265 | 266 | b. indicate if You modified the Licensed Material and 267 | retain an indication of any previous modifications; and 268 | 269 | c. indicate the Licensed Material is licensed under this 270 | Public License, and include the text of, or the URI or 271 | hyperlink to, this Public License. 272 | 273 | 2. You may satisfy the conditions in Section 3(a)(1) in any 274 | reasonable manner based on the medium, means, and context in 275 | which You Share the Licensed Material. For example, it may be 276 | reasonable to satisfy the conditions by providing a URI or 277 | hyperlink to a resource that includes the required 278 | information. 279 | 3. If requested by the Licensor, You must remove any of the 280 | information required by Section 3(a)(1)(A) to the extent 281 | reasonably practicable. 282 | 283 | b. ShareAlike. 284 | 285 | In addition to the conditions in Section 3(a), if You Share 286 | Adapted Material You produce, the following conditions also apply. 287 | 288 | 1. The Adapter's License You apply must be a Creative Commons 289 | license with the same License Elements, this version or 290 | later, or a BY-NC-SA Compatible License. 291 | 292 | 2. You must include the text of, or the URI or hyperlink to, the 293 | Adapter's License You apply. You may satisfy this condition 294 | in any reasonable manner based on the medium, means, and 295 | context in which You Share Adapted Material. 296 | 297 | 3. You may not offer or impose any additional or different terms 298 | or conditions on, or apply any Effective Technological 299 | Measures to, Adapted Material that restrict exercise of the 300 | rights granted under the Adapter's License You apply. 301 | 302 | 303 | Section 4 -- Sui Generis Database Rights. 304 | 305 | Where the Licensed Rights include Sui Generis Database Rights that 306 | apply to Your use of the Licensed Material: 307 | 308 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right 309 | to extract, reuse, reproduce, and Share all or a substantial 310 | portion of the contents of the database for NonCommercial purposes 311 | only; 312 | 313 | b. if You include all or a substantial portion of the database 314 | contents in a database in which You have Sui Generis Database 315 | Rights, then the database in which You have Sui Generis Database 316 | Rights (but not its individual contents) is Adapted Material, 317 | including for purposes of Section 3(b); and 318 | 319 | c. You must comply with the conditions in Section 3(a) if You Share 320 | all or a substantial portion of the contents of the database. 321 | 322 | For the avoidance of doubt, this Section 4 supplements and does not 323 | replace Your obligations under this Public License where the Licensed 324 | Rights include other Copyright and Similar Rights. 325 | 326 | 327 | Section 5 -- Disclaimer of Warranties and Limitation of Liability. 328 | 329 | a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE 330 | EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS 331 | AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF 332 | ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, 333 | IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, 334 | WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR 335 | PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, 336 | ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT 337 | KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT 338 | ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. 339 | 340 | b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE 341 | TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, 342 | NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, 343 | INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, 344 | COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR 345 | USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN 346 | ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR 347 | DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR 348 | IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. 349 | 350 | c. The disclaimer of warranties and limitation of liability provided 351 | above shall be interpreted in a manner that, to the extent 352 | possible, most closely approximates an absolute disclaimer and 353 | waiver of all liability. 354 | 355 | 356 | Section 6 -- Term and Termination. 357 | 358 | a. This Public License applies for the term of the Copyright and 359 | Similar Rights licensed here. However, if You fail to comply with 360 | this Public License, then Your rights under this Public License 361 | terminate automatically. 362 | 363 | b. Where Your right to use the Licensed Material has terminated under 364 | Section 6(a), it reinstates: 365 | 366 | 1. automatically as of the date the violation is cured, provided 367 | it is cured within 30 days of Your discovery of the 368 | violation; or 369 | 370 | 2. upon express reinstatement by the Licensor. 371 | 372 | For the avoidance of doubt, this Section 6(b) does not affect any 373 | right the Licensor may have to seek remedies for Your violations 374 | of this Public License. 375 | 376 | c. For the avoidance of doubt, the Licensor may also offer the 377 | Licensed Material under separate terms or conditions or stop 378 | distributing the Licensed Material at any time; however, doing so 379 | will not terminate this Public License. 380 | 381 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public 382 | License. 383 | 384 | 385 | Section 7 -- Other Terms and Conditions. 386 | 387 | a. The Licensor shall not be bound by any additional or different 388 | terms or conditions communicated by You unless expressly agreed. 389 | 390 | b. Any arrangements, understandings, or agreements regarding the 391 | Licensed Material not stated herein are separate from and 392 | independent of the terms and conditions of this Public License. 393 | 394 | 395 | Section 8 -- Interpretation. 396 | 397 | a. For the avoidance of doubt, this Public License does not, and 398 | shall not be interpreted to, reduce, limit, restrict, or impose 399 | conditions on any use of the Licensed Material that could lawfully 400 | be made without permission under this Public License. 401 | 402 | b. To the extent possible, if any provision of this Public License is 403 | deemed unenforceable, it shall be automatically reformed to the 404 | minimum extent necessary to make it enforceable. If the provision 405 | cannot be reformed, it shall be severed from this Public License 406 | without affecting the enforceability of the remaining terms and 407 | conditions. 408 | 409 | c. No term or condition of this Public License will be waived and no 410 | failure to comply consented to unless expressly agreed to by the 411 | Licensor. 412 | 413 | d. Nothing in this Public License constitutes or may be interpreted 414 | as a limitation upon, or waiver of, any privileges and immunities 415 | that apply to the Licensor or You, including from the legal 416 | processes of any jurisdiction or authority. 417 | 418 | ======================================================================= 419 | 420 | Creative Commons is not a party to its public 421 | licenses. Notwithstanding, Creative Commons may elect to apply one of 422 | its public licenses to material it publishes and in those instances 423 | will be considered the “Licensor.” The text of the Creative Commons 424 | public licenses is dedicated to the public domain under the CC0 Public 425 | Domain Dedication. Except for the limited purpose of indicating that 426 | material is shared under a Creative Commons public license or as 427 | otherwise permitted by the Creative Commons policies published at 428 | creativecommons.org/policies, Creative Commons does not authorize the 429 | use of the trademark "Creative Commons" or any other trademark or logo 430 | of Creative Commons without its prior written consent including, 431 | without limitation, in connection with any unauthorized modifications 432 | to any of its public licenses or any other arrangements, 433 | understandings, or agreements concerning use of licensed material. For 434 | the avoidance of doubt, this paragraph does not form part of the 435 | public licenses. 436 | 437 | Creative Commons may be contacted at creativecommons.org. 438 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CobaltStrikeParser 2 | Python parser for CobaltStrike Beacon's configuration 3 | 4 | ## Description 5 | Use `parse_beacon_config.py` for stageless beacons, memory dumps or C2 urls with metasploit compatibility mode (default true). 6 | Many stageless beacons are PEs where the beacon code itself is stored in the `.data` section and xored with 4-byte key. 7 | The script tries to find the xor key and data heuristically, decrypt the data and parse the configuration from it. 8 | 9 | This is designed so it can be used as a library too. 10 | 11 | The repo now also includes a small commuincation module (comm.py) that can help with communcating to a C2 server as a beacon. 12 | 13 | ## Usage 14 | ``` 15 | usage: parse_beacon_config.py [-h] [--json] [--quiet] [--version VERSION] beacon 16 | 17 | Parses CobaltStrike Beacon's configuration from PE, memory dump or URL. 18 | 19 | positional arguments: 20 | beacon This can be a file path or a url (if started with http/s) 21 | 22 | optional arguments: 23 | -h, --help show this help message and exit 24 | --json Print as json 25 | --quiet Do not print missing or empty settings 26 | --version VERSION Try as specific cobalt version (3 or 4). If not specified, tries both. 27 | ``` 28 | 29 | ## Extra 30 | To use the communication poc copy it to the main folder and run it from there. 31 | For installing the M2Crypto library (a requirement for the poc) on Windows, it's easiest with installers found online, and not through pip. -------------------------------------------------------------------------------- /beacon_utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | ''' 3 | By Gal Kristal from SentinelOne (gkristal.w@gmail.com) @gal_kristal 4 | Refs: 5 | https://github.com/RomanEmelyanov/CobaltStrikeForensic/blob/master/L8_get_beacon.py 6 | https://github.com/nccgroup/pybeacon 7 | ''' 8 | 9 | import requests, struct, urllib3 10 | import argparse 11 | from urllib.parse import urljoin 12 | import socket 13 | import json 14 | from base64 import b64encode 15 | from struct import unpack, unpack_from 16 | 17 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 18 | EMPTY_UA_HEADERS = {"User-Agent":""} 19 | URL_PATHS = {'x86':'ab2g', 'x64':'ab2h'} 20 | 21 | class Base64Encoder(json.JSONEncoder): 22 | def default(self, o): 23 | if isinstance(o, bytes): 24 | return b64encode(o).decode() 25 | return json.JSONEncoder.default(self, o) 26 | 27 | 28 | def _cli_print(msg, end='\n'): 29 | if __name__ == '__main__': 30 | print(msg, end=end) 31 | 32 | 33 | def read_dword_be(fh): 34 | data = fh.read(4) 35 | if not data or len(data) != 4: 36 | return None 37 | return unpack(">I",data)[0] 38 | 39 | 40 | def get_beacon_data(url, arch): 41 | full_url = urljoin(url, URL_PATHS[arch]) 42 | try: 43 | resp = requests.get(full_url, timeout=30, headers=EMPTY_UA_HEADERS, verify=False) 44 | except requests.exceptions.RequestException as e: 45 | _cli_print('[-] Connection error: ', e) 46 | return 47 | 48 | if resp.status_code != 200: 49 | _cli_print('[-] Failed with HTTP status code: ', resp.status_code) 50 | return 51 | 52 | buf = resp.content 53 | 54 | # Check if it's a Trial beacon, therefore not xor encoded (not tested) 55 | eicar_offset = buf.find(b'EICAR-STANDARD-ANTIVIRUS-TEST-FILE') 56 | if eicar_offset != -1: 57 | return buf 58 | return decrypt_beacon(buf) 59 | 60 | 61 | def decrypt_beacon(buf): 62 | offset = buf.find(b'\xff\xff\xff') 63 | if offset == -1: 64 | _cli_print('[-] Unexpected buffer received') 65 | return 66 | offset += 3 67 | key = struct.unpack_from('I', len(data)) + data 66 | return pubkey.public_encrypt(packed_data, M2Crypto.RSA.pkcs1_padding) 67 | 68 | 69 | def pack(self): 70 | data = self.aes_source_bytes + struct.pack('>hhIIHBH', self.charset, self.charset, self.bid, self.pid, self.port, self.is64, self.ver) + self.junk 71 | data += struct.pack('4s', self.ip) 72 | data += b'\x00' * (51 - len(data)) 73 | data += '\t'.join([self.comp, self.user]).encode() 74 | return self.rsa_encrypt(data) 75 | 76 | 77 | TERMINATION_STEPS = ['header', 'parameter', 'print'] 78 | TSTEPS = {1: "append", 2: "prepend", 3: "base64", 4: "print", 5: "parameter", 6: "header", 7: "build", 8: "netbios", 9: "const_parameter", 10: "const_header", 11: "netbiosu", 12: "uri_append", 13: "base64url", 14: "strrep", 15: "mask", 16: "const_host_header"} 79 | 80 | # Could probably just be b'\x00'*4 + data 81 | def mask(arg, data): 82 | key = os.urandom(4) 83 | data = data.encode('latin-1') 84 | return key.decode('latin-1') + ''.join(chr(c ^ key[i%4]) for i, c in enumerate(data)) 85 | 86 | def demask(arg, data): 87 | key = data[:4].encode('latin-1') 88 | data = data.encode('latin-1') 89 | return ''.join(chr(c ^ key[i%4]) for i, c in enumerate(data[4:])) 90 | 91 | def netbios_decode(name, case): 92 | i = iter(name.upper()) 93 | try: 94 | return ''.join([chr(((ord(c)-ord(case))<<4)+((ord(next(i))-ord(case))&0xF)) for c in i]) 95 | except: 96 | return '' 97 | 98 | func_dict_encode = {"append": lambda arg, data: data + arg, 99 | "prepend": lambda arg, data: arg + data, 100 | "base64": lambda arg, data: base64.b64encode(data), 101 | "netbios": lambda arg, data: ''.join([chr((ord(c)>>4) + ord('a')) + chr((ord(c)&0xF) + ord('a')) for c in data]), 102 | "netbiosu": lambda arg, data: ''.join([chr((ord(c)>>4) + ord('A')) + chr((ord(c)&0xF) + ord('A')) for c in data]), 103 | "base64": lambda arg, data: base64.b64encode(data.encode('latin-1')).decode('latin-1'), 104 | "base64url": lambda arg, data: base64.urlsafe_b64encode(data.encode('latin-1')).decode('latin-1').strip('='), 105 | "mask": mask, 106 | } 107 | 108 | func_dict_decode = {"append": lambda arg, data: data[:-len(arg)], 109 | "prepend": lambda arg, data: data[len(arg):], 110 | "base64": lambda arg, data: base64.b64decode(data), 111 | "netbios": lambda arg, data: netbios_decode(data, 'a'), 112 | "netbiosu": lambda arg, data: netbios_decode(data, 'A'), 113 | "base64": lambda arg, data: base64.b64decode(data.encode('latin-1')).decode('latin-1'), 114 | "base64url": lambda arg, data: base64.urlsafe_b64decode(data.encode('latin-1')).decode('latin-1').strip('='), 115 | "mask": demask, 116 | } 117 | 118 | 119 | class Transform(object): 120 | def __init__(self, trans_dict): 121 | """An helper class to tranform data according to cobalt's malleable profile 122 | 123 | Args: 124 | trans_dict (dict): A dictionary that came from packedSetting data. It's in the form of: 125 | {'ConstHeaders':[], 'ConstParams': [], 'Metadata': [], 'SessionId': [], 'Output': []} 126 | """ 127 | self.trans_dict = trans_dict 128 | 129 | def encode(self, metadata, output, sessionId): 130 | """ 131 | 132 | Args: 133 | metadata (str): The metadata of a Beacon, usually given from Metadata.pack() 134 | output (str): If this is for a Beacon's response, then this is the response's data 135 | sessionId (str): the Beacon's ID 136 | 137 | Returns: 138 | (str, dict, dict): This is to be used in an HTTP request. The tuple is (request_body, request_headers, request_params) 139 | """ 140 | params = {} 141 | headers = {} 142 | body = '' 143 | for step in self.trans_dict['Metadata']: 144 | action = step.split(' ')[0].lower() 145 | arg = step.lstrip(action).strip().strip('"') 146 | if action in TERMINATION_STEPS: 147 | if action == "header": 148 | headers[arg] = metadata 149 | elif action == "parameter": 150 | params[arg] = metadata 151 | elif action == "print": 152 | body = metadata 153 | else: 154 | metadata = func_dict_encode[action](arg, metadata) 155 | 156 | for step in self.trans_dict['Output']: 157 | action = step.split(' ')[0].lower() 158 | arg = step.lstrip(action).strip().strip('"') 159 | if action in TERMINATION_STEPS: 160 | if action == "header": 161 | headers[arg] = output 162 | elif action == "parameter": 163 | params[arg] = output 164 | elif action == "print": 165 | body = output 166 | else: 167 | output = func_dict_encode[action](arg, output) 168 | 169 | for step in self.trans_dict['SessionId']: 170 | action = step.split(' ')[0].lower() 171 | arg = step.lstrip(action).strip().strip('"') 172 | if action in TERMINATION_STEPS: 173 | if action == "header": 174 | headers[arg] = sessionId 175 | elif action == "parameter": 176 | params[arg] = sessionId 177 | elif action == "print": 178 | body = sessionId 179 | else: 180 | sessionId = func_dict_encode[action](arg, sessionId) 181 | 182 | for step in self.trans_dict['ConstHeaders']: 183 | offset = step.find(': ') 184 | header, value = step[:offset], step[offset+2:] 185 | headers[header] = value 186 | 187 | for step in self.trans_dict['ConstParams']: 188 | offset = step.find('=') 189 | param, value = step[:offset], step[offset+1:] 190 | params[param] = value 191 | 192 | return body, headers, params 193 | 194 | def decode(self, body, headers, params): 195 | """ 196 | Parses beacon's communication data from an HTTP request 197 | Args: 198 | body (str): The body of an HTTP request 199 | headers (dict): Headers dict from the HTTP request 200 | params (dict): Params dict from the HTTP request 201 | 202 | Returns: 203 | (str, str, str): The tuple is (metadata, output, sessionId) 204 | """ 205 | metadata = '' 206 | output = '' 207 | sessionId = '' 208 | for step in self.trans_dict['Metadata'][::-1]: 209 | action = step.split(' ')[0].lower() 210 | arg = step.lstrip(action).strip().strip('"') 211 | if action in TERMINATION_STEPS: 212 | if action == "header": 213 | metadata = headers[arg] 214 | elif action == "parameter": 215 | metadata = params[arg] 216 | elif action == "print": 217 | metadata = body 218 | else: 219 | metadata = func_dict_decode[action](arg, metadata) 220 | 221 | for step in self.trans_dict['Output'][::-1]: 222 | action = step.split(' ')[0].lower() 223 | arg = step.lstrip(action).strip().strip('"') 224 | if action in TERMINATION_STEPS: 225 | if action == "header": 226 | output = headers[arg] 227 | elif action == "parameter": 228 | output = params[arg] 229 | elif action == "print": 230 | output = body 231 | else: 232 | output = func_dict_decode[action](arg, output) 233 | 234 | for step in self.trans_dict['SessionId'][::-1]: 235 | action = step.split(' ')[0].lower() 236 | arg = step.lstrip(action).strip().strip('"') 237 | if action in TERMINATION_STEPS: 238 | if action == "header": 239 | sessionId = headers[arg] 240 | elif action == "parameter": 241 | sessionId = params[arg] 242 | elif action == "print": 243 | sessionId = body 244 | else: 245 | sessionId = func_dict_decode[action](arg, sessionId) 246 | 247 | return metadata, output, sessionId 248 | 249 | 250 | 251 | 252 | 253 | 254 | -------------------------------------------------------------------------------- /extra/communication_poc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | ''' 3 | By Gal Kristal from SentinelOne (gkristal.w@gmail.com) @gal_kristal 4 | Refs: 5 | https://github.com/RomanEmelyanov/CobaltStrikeForensic/blob/master/L8_get_beacon.py 6 | https://github.com/nccgroup/pybeacon 7 | ''' 8 | 9 | import requests, struct, sys, os, urllib3 10 | import argparse 11 | from parse_beacon_config import cobaltstrikeConfig 12 | from urllib.parse import urljoin 13 | from io import BytesIO 14 | from Crypto.Cipher import AES 15 | import hmac 16 | import urllib 17 | import socket 18 | from comm import * 19 | 20 | HASH_ALGO = hashlib.sha256 21 | SIG_SIZE = HASH_ALGO().digest_size 22 | CS_FIXED_IV = b"abcdefghijklmnop" 23 | 24 | EMPTY_UA_HEADERS = {"User-Agent":""} 25 | URL_PATHS = {'x86':'ab2g', 'x64':'ab2h'} 26 | 27 | def get_beacon_data(url, arch): 28 | full_url = urljoin(url, URL_PATHS[arch]) 29 | try: 30 | resp = requests.get(full_url, timeout=30, headers=EMPTY_UA_HEADERS, verify=False) 31 | except requests.exceptions.RequestException as e: 32 | print('[-] Connection error: ', e) 33 | return 34 | 35 | if resp.status_code != 200: 36 | print('[-] Failed with HTTP status code: ', resp.status_code) 37 | return 38 | 39 | buf = resp.content 40 | 41 | # Check if it's a Trial beacon, therefore not xor encoded (not tested) 42 | eicar_offset = buf.find(b'EICAR-STANDARD-ANTIVIRUS-TEST-FILE') 43 | if eicar_offset != -1: 44 | return cobaltstrikeConfig(BytesIO(buf)).parse_config() 45 | 46 | offset = buf.find(b'\xff\xff\xff') 47 | if offset == -1: 48 | print('[-] Unexpected buffer received') 49 | return 50 | offset += 3 51 | key = struct.unpack_from('II', 1, len(random_data)) + random_data 95 | pad_size = AES.block_size - len(data) % AES.block_size 96 | data = data + pad_size * b'\x00' 97 | 98 | # encrypt the task data and wrap with hmac sig and encrypted data length 99 | cipher = AES.new(m.aes_key, AES.MODE_CBC, CS_FIXED_IV) 100 | enc_data = cipher.encrypt(data) 101 | sig = hmac.new(m.hmac_key, enc_data, HASH_ALGO).digest()[0:16] 102 | enc_data += sig 103 | enc_data = struct.pack('>I', len(enc_data)) + enc_data 104 | 105 | # task data is POSTed so we need to take the transformation steps of http-post.client 106 | t = Transform(conf['HttpPost_Metadata']) 107 | body, headers, params = t.encode(m.pack().decode('latin-1'), enc_data.decode('latin-1'), str(m.bid)) 108 | 109 | print('[+] Sending task data') 110 | 111 | try: 112 | req = requests.request('POST', urljoin('http://'+conf['C2Server'].split(',')[0], conf['HttpPostUri'].split(',')[0]), params=params, data=body, headers=dict(**headers, **{'User-Agent':''}), timeout=5) 113 | except Exception as e: 114 | print('[-] Got excpetion from server while sending task: %s' % e) 115 | 116 | 117 | 118 | if __name__ == '__main__': 119 | parser = argparse.ArgumentParser(description="Parse CobaltStrike Beacon's configuration from C2 url and registers a beacon with it") 120 | parser.add_argument("url", help="Cobalt C2 server (e.g. http://1.1.1.1)") 121 | args = parser.parse_args() 122 | 123 | x86_beacon_conf = get_beacon_data(args.url, 'x86') 124 | x64_beacon_conf = get_beacon_data(args.url, 'x64') 125 | if not x86_beacon_conf and not x64_beacon_conf: 126 | print("[-] Failed finding any beacon configuration") 127 | exit(1) 128 | 129 | print("[+] Got beacon configuration successfully") 130 | conf = x86_beacon_conf or x64_beacon_conf 131 | register_beacon(conf) -------------------------------------------------------------------------------- /parse_beacon_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | ''' 3 | Parses CobaltStrike Beacon's configuration from PE file or memory dump. 4 | By Gal Kristal from SentinelOne (gkristal.w@gmail.com) @gal_kristal 5 | 6 | Inspired by https://github.com/JPCERTCC/aa-tools/blob/master/cobaltstrikescan.py 7 | 8 | TODO: 9 | 1. Parse headers modifiers 10 | 2. Dynamic size parsing 11 | ''' 12 | 13 | from beacon_utils import * 14 | from struct import unpack, unpack_from 15 | from socket import inet_ntoa 16 | from collections import OrderedDict 17 | from netstruct import unpack as netunpack 18 | import argparse 19 | import io 20 | import re 21 | import pefile 22 | import os 23 | import hashlib 24 | from io import BytesIO 25 | 26 | THRESHOLD = 1100 27 | COLUMN_WIDTH = 35 28 | SUPPORTED_VERSIONS = (3, 4) 29 | SILENT_CONFIGS = ['PublicKey', 'ProcInject_Stub', 'smbFrameHeader', 'tcpFrameHeader', 'SpawnTo'] 30 | 31 | def _cli_print(msg, end='\n'): 32 | if __name__ == '__main__': 33 | print(msg, end=end) 34 | 35 | class confConsts: 36 | MAX_SETTINGS = 64 37 | TYPE_NONE = 0 38 | TYPE_SHORT = 1 39 | TYPE_INT = 2 40 | TYPE_STR = 3 41 | 42 | START_PATTERNS = { 43 | 3: b'\x69\x68\x69\x68\x69\x6b..\x69\x6b\x69\x68\x69\x6b..\x69\x6a', 44 | 4: b'\x2e\x2f\x2e\x2f\x2e...\x2e\x2c\x2e\x2f' 45 | } 46 | START_PATTERN_DECODED = b'\x00\x01\x00\x01\x00...\x00\x02\x00\x01\x00' 47 | CONFIG_SIZE = 4096 48 | XORBYTES = { 49 | 3: 0x69, 50 | 4: 0x2e 51 | } 52 | 53 | class packedSetting: 54 | 55 | def __init__(self, pos, datatype, length=0, isBlob=False, isHeaders=False, isIpAddress=False, isBool=False, isDate=False, boolFalseValue=0, isProcInjectTransform=False, isMalleableStream=False, hashBlob=False, enum=None, mask=None): 56 | self.pos = pos 57 | self.datatype = datatype 58 | self.is_blob = isBlob 59 | self.is_headers = isHeaders 60 | self.is_ipaddress = isIpAddress 61 | self.is_bool = isBool 62 | self.is_date = isDate 63 | self.is_malleable_stream = isMalleableStream 64 | self.bool_false_value = boolFalseValue 65 | self.is_transform = isProcInjectTransform 66 | self.hashBlob = hashBlob 67 | self.enum = enum 68 | self.mask = mask 69 | self.transform_get = None 70 | self.transform_post = None 71 | if datatype == confConsts.TYPE_STR and length == 0: 72 | raise(Exception("if datatype is TYPE_STR then length must not be 0")) 73 | 74 | self.length = length 75 | if datatype == confConsts.TYPE_SHORT: 76 | self.length = 2 77 | elif datatype == confConsts.TYPE_INT: 78 | self.length = 4 79 | 80 | 81 | def binary_repr(self): 82 | """ 83 | Param number - Type - Length - Value 84 | """ 85 | self_repr = bytearray(6) 86 | self_repr[1] = self.pos 87 | self_repr[3] = self.datatype 88 | self_repr[4:6] = self.length.to_bytes(2, 'big') 89 | return self_repr 90 | 91 | def parse_transformdata(self, data): 92 | ''' 93 | Args: 94 | data (bytes): Raw communication transforam data 95 | 96 | Returns: 97 | dict: Dict of transform commands that should be convenient for communication forging 98 | 99 | ''' 100 | dio = io.BytesIO(data) 101 | trans = {'ConstHeaders':[], 'ConstParams': [], 'Metadata': [], 'SessionId': [], 'Output': []} 102 | current_category = 'Constants' 103 | 104 | # TODO: replace all magic numbers here with enum 105 | while True: 106 | tstep = read_dword_be(dio) 107 | if tstep == 7: 108 | name = read_dword_be(dio) 109 | if self.pos == 12: # GET 110 | current_category = 'Metadata' 111 | else: # POST 112 | current_category = 'SessionId' if name == 0 else 'Output' 113 | elif tstep in (1, 2, 5, 6): 114 | length = read_dword_be(dio) 115 | step_data = dio.read(length).decode('latin-1') 116 | trans[current_category].append(BeaconSettings.TSTEPS[tstep] + ' "' + step_data + '"') 117 | elif tstep in (10, 16, 9): 118 | length = read_dword_be(dio) 119 | step_data = dio.read(length).decode('latin-1') 120 | if tstep == 9: 121 | trans['ConstParams'].append(step_data) 122 | else: 123 | trans['ConstHeaders'].append(step_data) 124 | elif tstep in (3, 4, 13, 8, 11, 12, 15): 125 | trans[current_category].append(BeaconSettings.TSTEPS[tstep]) 126 | else: 127 | break 128 | 129 | if self.pos == 12: 130 | self.transform_get = trans 131 | else: 132 | self.transform_post = trans 133 | 134 | return trans 135 | 136 | 137 | def pretty_repr(self, full_config_data): 138 | data_offset = full_config_data.find(self.binary_repr()) 139 | if data_offset < 0 and self.datatype == confConsts.TYPE_STR: 140 | self.length = 16 141 | while self.length < 2048: 142 | data_offset = full_config_data.find(self.binary_repr()) 143 | if data_offset > 0: 144 | break 145 | self.length *= 2 146 | 147 | if data_offset < 0: 148 | return 'Not Found' 149 | 150 | repr_len = len(self.binary_repr()) 151 | conf_data = full_config_data[data_offset + repr_len : data_offset + repr_len + self.length] 152 | if self.datatype == confConsts.TYPE_SHORT: 153 | conf_data = unpack('>H', conf_data)[0] 154 | if self.is_bool: 155 | ret = 'False' if conf_data == self.bool_false_value else 'True' 156 | return ret 157 | elif self.enum: 158 | return self.enum[conf_data] 159 | elif self.mask: 160 | ret_arr = [] 161 | for k,v in self.mask.items(): 162 | if k == 0 and k == conf_data: 163 | ret_arr.append(v) 164 | if k & conf_data: 165 | ret_arr.append(v) 166 | return ret_arr 167 | else: 168 | return conf_data 169 | 170 | elif self.datatype == confConsts.TYPE_INT: 171 | if self.is_ipaddress: 172 | return inet_ntoa(conf_data) 173 | 174 | else: 175 | conf_data = unpack('>i', conf_data)[0] 176 | if self.is_date and conf_data != 0: 177 | fulldate = str(conf_data) 178 | return "%s-%s-%s" % (fulldate[0:4], fulldate[4:6], fulldate[6:]) 179 | 180 | return conf_data 181 | 182 | if self.is_blob: 183 | if self.enum != None: 184 | ret_arr = [] 185 | i = 0 186 | while i < len(conf_data): 187 | v = conf_data[i] 188 | if v == 0: 189 | return ret_arr 190 | v = self.enum[v] 191 | if v: 192 | ret_arr.append(v) 193 | i+=1 194 | 195 | # Only EXECUTE_TYPE for now 196 | else: 197 | # Skipping unknown short value in the start 198 | string1 = netunpack(b'I$', conf_data[i+3:])[0].decode() 199 | string2 = netunpack(b'I$', conf_data[i+3+4+len(string1):])[0].decode() 200 | ret_arr.append("%s:%s" % (string1.strip('\x00'),string2.strip('\x00'))) 201 | i += len(string1) + len(string2) + 11 202 | 203 | 204 | if self.is_transform: 205 | if conf_data == bytes(len(conf_data)): 206 | return 'Empty' 207 | 208 | ret_arr = [] 209 | prepend_length = unpack('>I', conf_data[0:4])[0] 210 | prepend = conf_data[4 : 4+prepend_length] 211 | append_length_offset = prepend_length + 4 212 | append_length = unpack('>I', conf_data[append_length_offset : append_length_offset+4])[0] 213 | append = conf_data[append_length_offset+4 : append_length_offset+4+append_length] 214 | ret_arr.append(prepend) 215 | ret_arr.append(append if append_length < 256 and append != bytes(append_length) else 'Empty') 216 | return ret_arr 217 | 218 | if self.is_malleable_stream: 219 | prog = [] 220 | fh = io.BytesIO(conf_data) 221 | while True: 222 | op = read_dword_be(fh) 223 | if not op: 224 | break 225 | if op == 1: 226 | l = read_dword_be(fh) 227 | prog.append("Remove %d bytes from the end" % l) 228 | elif op == 2: 229 | l = read_dword_be(fh) 230 | prog.append("Remove %d bytes from the beginning" % l) 231 | elif op == 3: 232 | prog.append("Base64 decode") 233 | elif op == 8: 234 | prog.append("NetBIOS decode 'a'") 235 | elif op == 11: 236 | prog.append("NetBIOS decode 'A'") 237 | elif op == 13: 238 | prog.append("Base64 URL-safe decode") 239 | elif op == 15: 240 | prog.append("XOR mask w/ random key") 241 | 242 | conf_data = prog 243 | if self.hashBlob: 244 | conf_data = hashlib.md5(conf_data).hexdigest() 245 | 246 | return conf_data 247 | 248 | if self.is_headers: 249 | return self.parse_transformdata(conf_data) 250 | 251 | conf_data = conf_data.strip(b'\x00').decode('latin-1') 252 | return conf_data 253 | 254 | 255 | class BeaconSettings: 256 | 257 | BEACON_TYPE = {0x0: "HTTP", 0x1: "Hybrid HTTP DNS", 0x2: "SMB", 0x4: "TCP", 0x8: "HTTPS", 0x10: "Bind TCP"} 258 | ACCESS_TYPE = {0x0: "Use proxy server (manual)", 0x1: "Use direct connection", 0x2: "Use IE settings", 0x4: "Use proxy server (credentials)"} 259 | EXECUTE_TYPE = {0x1: "CreateThread", 0x2: "SetThreadContext", 0x3: "CreateRemoteThread", 0x4: "RtlCreateUserThread", 0x5: "NtQueueApcThread", 0x6: None, 0x7: None, 0x8: "NtQueueApcThread-s"} 260 | ALLOCATION_FUNCTIONS = {0: "VirtualAllocEx", 1: "NtMapViewOfSection"} 261 | TSTEPS = {1: "append", 2: "prepend", 3: "base64", 4: "print", 5: "parameter", 6: "header", 7: "build", 8: "netbios", 9: "const_parameter", 10: "const_header", 11: "netbiosu", 12: "uri_append", 13: "base64url", 14: "strrep", 15: "mask", 16: "const_host_header"} 262 | ROTATE_STRATEGY = ["round-robin", "random", "failover", "failover-5x", "failover-50x", "failover-100x", "failover-1m", "failover-5m", "failover-15m", "failover-30m", "failover-1h", "failover-3h", "failover-6h", "failover-12h", "failover-1d", "rotate-1m", "rotate-5m", "rotate-15m", "rotate-30m", "rotate-1h", "rotate-3h", "rotate-6h", "rotate-12h", "rotate-1d" ] 263 | 264 | def __init__(self, version): 265 | if version not in SUPPORTED_VERSIONS: 266 | _cli_print("Error: Only supports version 3 and 4, not %d" % version) 267 | return 268 | self.version = version 269 | self.settings = OrderedDict() 270 | self.init() 271 | 272 | def init(self): 273 | self.settings['BeaconType'] = packedSetting(1, confConsts.TYPE_SHORT, mask=self.BEACON_TYPE) 274 | self.settings['Port'] = packedSetting(2, confConsts.TYPE_SHORT) 275 | self.settings['SleepTime'] = packedSetting(3, confConsts.TYPE_INT) 276 | self.settings['MaxGetSize'] = packedSetting(4, confConsts.TYPE_INT) 277 | self.settings['Jitter'] = packedSetting(5, confConsts.TYPE_SHORT) 278 | self.settings['MaxDNS'] = packedSetting(6, confConsts.TYPE_SHORT) 279 | # Silenced config 280 | self.settings['PublicKey'] = packedSetting(7, confConsts.TYPE_STR, 256, isBlob=True) 281 | self.settings['PublicKey_MD5'] = packedSetting(7, confConsts.TYPE_STR, 256, isBlob=True, hashBlob=True) 282 | self.settings['C2Server'] = packedSetting(8, confConsts.TYPE_STR, 256) 283 | self.settings['UserAgent'] = packedSetting(9, confConsts.TYPE_STR, 128) 284 | # TODO: Concat with C2Server? 285 | self.settings['HttpPostUri'] = packedSetting(10, confConsts.TYPE_STR, 64) 286 | 287 | # This is how the server transforms its communication to the beacon 288 | # ref: https://www.cobaltstrike.com/help-malleable-c2 | https://usualsuspect.re/article/cobalt-strikes-malleable-c2-under-the-hood 289 | # TODO: Switch to isHeaders parser logic 290 | self.settings['Malleable_C2_Instructions'] = packedSetting(11, confConsts.TYPE_STR, 256, isBlob=True,isMalleableStream=True) 291 | # This is the way the beacon transforms its communication to the server 292 | # TODO: Change name to HttpGet_Client and HttpPost_Client 293 | self.settings['HttpGet_Metadata'] = packedSetting(12, confConsts.TYPE_STR, 256, isHeaders=True) 294 | self.settings['HttpPost_Metadata'] = packedSetting(13, confConsts.TYPE_STR, 256, isHeaders=True) 295 | 296 | self.settings['SpawnTo'] = packedSetting(14, confConsts.TYPE_STR, 16, isBlob=True) 297 | self.settings['PipeName'] = packedSetting(15, confConsts.TYPE_STR, 128) 298 | # Options 16-18 are deprecated in 3.4 299 | self.settings['DNS_Idle'] = packedSetting(19, confConsts.TYPE_INT, isIpAddress=True) 300 | self.settings['DNS_Sleep'] = packedSetting(20, confConsts.TYPE_INT) 301 | # Options 21-25 are for SSHAgent 302 | self.settings['SSH_Host'] = packedSetting(21, confConsts.TYPE_STR, 256) 303 | self.settings['SSH_Port'] = packedSetting(22, confConsts.TYPE_SHORT) 304 | self.settings['SSH_Username'] = packedSetting(23, confConsts.TYPE_STR, 128) 305 | self.settings['SSH_Password_Plaintext'] = packedSetting(24, confConsts.TYPE_STR, 128) 306 | self.settings['SSH_Password_Pubkey'] = packedSetting(25, confConsts.TYPE_STR, 6144) 307 | self.settings['SSH_Banner'] = packedSetting(54, confConsts.TYPE_STR, 128) 308 | 309 | self.settings['HttpGet_Verb'] = packedSetting(26, confConsts.TYPE_STR, 16) 310 | self.settings['HttpPost_Verb'] = packedSetting(27, confConsts.TYPE_STR, 16) 311 | self.settings['HttpPostChunk'] = packedSetting(28, confConsts.TYPE_INT) 312 | self.settings['Spawnto_x86'] = packedSetting(29, confConsts.TYPE_STR, 64) 313 | self.settings['Spawnto_x64'] = packedSetting(30, confConsts.TYPE_STR, 64) 314 | # Whether the beacon encrypts his communication, should be always on (1) in beacon 4 315 | self.settings['CryptoScheme'] = packedSetting(31, confConsts.TYPE_SHORT) 316 | self.settings['Proxy_Config'] = packedSetting(32, confConsts.TYPE_STR, 128) 317 | self.settings['Proxy_User'] = packedSetting(33, confConsts.TYPE_STR, 64) 318 | self.settings['Proxy_Password'] = packedSetting(34, confConsts.TYPE_STR, 64) 319 | self.settings['Proxy_Behavior'] = packedSetting(35, confConsts.TYPE_SHORT, enum=self.ACCESS_TYPE) 320 | # Option 36 is deprecated in beacon < 4.5 321 | self.settings['Watermark_Hash'] = packedSetting(36, confConsts.TYPE_STR, 32) 322 | self.settings['Watermark'] = packedSetting(37, confConsts.TYPE_INT) 323 | self.settings['bStageCleanup'] = packedSetting(38, confConsts.TYPE_SHORT, isBool=True) 324 | self.settings['bCFGCaution'] = packedSetting(39, confConsts.TYPE_SHORT, isBool=True) 325 | self.settings['KillDate'] = packedSetting(40, confConsts.TYPE_INT, isDate=True) 326 | # Inner parameter, does not seem interesting so silencing 327 | #self.settings['textSectionEnd (0 if !sleep_mask)'] = packedSetting(41, confConsts.TYPE_INT) 328 | 329 | #TODO: dynamic size parsing 330 | #self.settings['ObfuscateSectionsInfo'] = packedSetting(42, confConsts.TYPE_STR, %d, isBlob=True) 331 | self.settings['bProcInject_StartRWX'] = packedSetting(43, confConsts.TYPE_SHORT, isBool=True, boolFalseValue=4) 332 | self.settings['bProcInject_UseRWX'] = packedSetting(44, confConsts.TYPE_SHORT, isBool=True, boolFalseValue=32) 333 | self.settings['bProcInject_MinAllocSize'] = packedSetting(45, confConsts.TYPE_INT) 334 | self.settings['ProcInject_PrependAppend_x86'] = packedSetting(46, confConsts.TYPE_STR, 256, isBlob=True, isProcInjectTransform=True) 335 | self.settings['ProcInject_PrependAppend_x64'] = packedSetting(47, confConsts.TYPE_STR, 256, isBlob=True, isProcInjectTransform=True) 336 | self.settings['ProcInject_Execute'] = packedSetting(51, confConsts.TYPE_STR, 128, isBlob=True, enum=self.EXECUTE_TYPE) 337 | # If True then allocation is using NtMapViewOfSection 338 | self.settings['ProcInject_AllocationMethod'] = packedSetting(52, confConsts.TYPE_SHORT, enum=self.ALLOCATION_FUNCTIONS) 339 | 340 | # Unknown data, silenced for now 341 | self.settings['ProcInject_Stub'] = packedSetting(53, confConsts.TYPE_STR, 16, isBlob=True) 342 | self.settings['bUsesCookies'] = packedSetting(50, confConsts.TYPE_SHORT, isBool=True) 343 | self.settings['HostHeader'] = packedSetting(54, confConsts.TYPE_STR, 128) 344 | 345 | # Silenced as I've yet to test it on a sample with those options 346 | self.settings['smbFrameHeader'] = packedSetting(57, confConsts.TYPE_STR, 128, isBlob=True) 347 | self.settings['tcpFrameHeader'] = packedSetting(58, confConsts.TYPE_STR, 128, isBlob=True) 348 | self.settings['headersToRemove'] = packedSetting(59, confConsts.TYPE_STR, 64) 349 | 350 | # DNS Beacon 351 | self.settings['DNS_Beaconing'] = packedSetting(60, confConsts.TYPE_STR, 33) 352 | self.settings['DNS_get_TypeA'] = packedSetting(61, confConsts.TYPE_STR, 33) 353 | self.settings['DNS_get_TypeAAAA'] = packedSetting(62, confConsts.TYPE_STR, 33) 354 | self.settings['DNS_get_TypeTXT'] = packedSetting(63, confConsts.TYPE_STR, 33) 355 | self.settings['DNS_put_metadata'] = packedSetting(64, confConsts.TYPE_STR, 33) 356 | self.settings['DNS_put_output'] = packedSetting(65, confConsts.TYPE_STR, 33) 357 | self.settings['DNS_resolver'] = packedSetting(66, confConsts.TYPE_STR, 15) 358 | self.settings['DNS_strategy'] = packedSetting(67, confConsts.TYPE_SHORT, enum=self.ROTATE_STRATEGY) 359 | self.settings['DNS_strategy_rotate_seconds'] = packedSetting(68, confConsts.TYPE_INT) 360 | self.settings['DNS_strategy_fail_x'] = packedSetting(69, confConsts.TYPE_INT) 361 | self.settings['DNS_strategy_fail_seconds'] = packedSetting(70, confConsts.TYPE_INT) 362 | 363 | # Retry settings (CS 4.5+ only) 364 | self.settings['Retry_Max_Attempts'] = packedSetting(71, confConsts.TYPE_INT) 365 | self.settings['Retry_Increase_Attempts'] = packedSetting(72, confConsts.TYPE_INT) 366 | self.settings['Retry_Duration'] = packedSetting(73, confConsts.TYPE_INT) 367 | 368 | 369 | class cobaltstrikeConfig: 370 | def __init__(self, f): 371 | ''' 372 | f: file path or file-like object 373 | ''' 374 | self.data = None 375 | if isinstance(f, str): 376 | with open(f, 'rb') as fobj: 377 | self.data = fobj.read() 378 | else: 379 | self.data = f.read() 380 | 381 | """Parse the CobaltStrike configuration""" 382 | 383 | @staticmethod 384 | def decode_config(cfg_blob, version): 385 | return bytes([cfg_offset ^ confConsts.XORBYTES[version] for cfg_offset in cfg_blob]) 386 | 387 | def _parse_config(self, version, quiet=False, as_json=False): 388 | ''' 389 | Parses beacon's configuration from beacon PE or memory dump. 390 | Returns json of config is found; else it returns None. 391 | 392 | :int version: Try a specific version (3 or 4), or leave None to try both of them 393 | :bool quiet: Whether to print missing or empty settings 394 | :bool as_json: Whether to dump as json 395 | ''' 396 | re_start_match = re.search(confConsts.START_PATTERNS[version], self.data) 397 | re_start_decoded_match = re.search(confConsts.START_PATTERN_DECODED, self.data) 398 | 399 | if not re_start_match and not re_start_decoded_match: 400 | return None 401 | encoded_config_offset = re_start_match.start() if re_start_match else -1 402 | decoded_config_offset = re_start_decoded_match.start() if re_start_decoded_match else -1 403 | 404 | if encoded_config_offset >= 0: 405 | full_config_data = cobaltstrikeConfig.decode_config(self.data[encoded_config_offset : encoded_config_offset + confConsts.CONFIG_SIZE], version=version) 406 | else: 407 | full_config_data = self.data[decoded_config_offset : decoded_config_offset + confConsts.CONFIG_SIZE] 408 | 409 | parsed_config = {} 410 | settings = BeaconSettings(version).settings.items() 411 | for conf_name, packed_conf in settings: 412 | parsed_setting = packed_conf.pretty_repr(full_config_data) 413 | 414 | parsed_config[conf_name] = parsed_setting 415 | if as_json: 416 | continue 417 | 418 | if conf_name in SILENT_CONFIGS: 419 | continue 420 | 421 | if parsed_setting == 'Not Found' and quiet: 422 | continue 423 | 424 | conf_type = type(parsed_setting) 425 | if conf_type in (str, int, bytes): 426 | if quiet and conf_type == str and parsed_setting.strip() == '': 427 | continue 428 | _cli_print("{: <{width}} - {val}".format(conf_name, width=COLUMN_WIDTH-3, val=parsed_setting)) 429 | 430 | elif parsed_setting == []: 431 | if quiet: 432 | continue 433 | _cli_print("{: <{width}} - {val}".format(conf_name, width=COLUMN_WIDTH-3, val='Empty')) 434 | 435 | elif conf_type == dict: # the beautifulest code 436 | conf_data = [] 437 | for k in parsed_setting.keys(): 438 | if parsed_setting[k]: 439 | conf_data.append(k) 440 | for v in parsed_setting[k]: 441 | conf_data.append('\t' + v) 442 | if not conf_data: 443 | continue 444 | _cli_print("{: <{width}} - {val}".format(conf_name, width=COLUMN_WIDTH-3, val=conf_data[0])) 445 | for val in conf_data[1:]: 446 | _cli_print(' ' * COLUMN_WIDTH, end='') 447 | _cli_print(val) 448 | 449 | elif conf_type == list: # list 450 | _cli_print("{: <{width}} - {val}".format(conf_name, width=COLUMN_WIDTH-3, val=parsed_setting[0])) 451 | for val in parsed_setting[1:]: 452 | _cli_print(' ' * COLUMN_WIDTH, end='') 453 | _cli_print(val) 454 | 455 | if as_json: 456 | _cli_print(json.dumps(parsed_config, cls=Base64Encoder)) 457 | 458 | return parsed_config 459 | 460 | def parse_config(self, version=None, quiet=False, as_json=False): 461 | ''' 462 | Parses beacon's configuration from beacon PE or memory dump 463 | Returns json of config is found; else it returns None. 464 | 465 | :int version: Try a specific version (3 or 4), or leave None to try both of them 466 | :bool quiet: Whether to print missing or empty settings 467 | :bool as_json: Whether to dump as json 468 | ''' 469 | 470 | if not version: 471 | for ver in SUPPORTED_VERSIONS: 472 | parsed = self._parse_config(version=ver, quiet=quiet, as_json=as_json) 473 | if parsed: 474 | return parsed 475 | else: 476 | return self._parse_config(version=version, quiet=quiet, as_json=as_json) 477 | return None 478 | 479 | 480 | def parse_encrypted_config_non_pe(self, version=None, quiet=False, as_json=False): 481 | self.data = decrypt_beacon(self.data) 482 | return self.parse_config(version=version, quiet=quiet, as_json=as_json) 483 | 484 | def parse_encrypted_config(self, version=None, quiet=False, as_json=False): 485 | ''' 486 | Parses beacon's configuration from stager dll or memory dump 487 | Returns json of config is found; else it returns None. 488 | 489 | :bool quiet: Whether to print missing settings 490 | :bool as_json: Whether to dump as json 491 | ''' 492 | 493 | try: 494 | pe = pefile.PE(data=self.data) 495 | except pefile.PEFormatError: 496 | return self.parse_encrypted_config_non_pe(version=version, quiet=quiet, as_json=as_json) 497 | 498 | data_sections = [s for s in pe.sections if s.Name.find(b'.data') != -1] 499 | if not data_sections: 500 | _cli_print("Failed to find .data section") 501 | return False 502 | data = data_sections[0].get_data() 503 | 504 | offset = 0 505 | key_found = False 506 | while offset < len(data): 507 | key = data[offset:offset+4] 508 | if key != bytes(4): 509 | if data.count(key) >= THRESHOLD: 510 | key_found = True 511 | size = int.from_bytes(data[offset-4:offset], 'little') 512 | encrypted_data_offset = offset+16 - (offset % 16) 513 | break 514 | 515 | offset += 4 516 | 517 | if not key_found: 518 | return False 519 | 520 | # decrypt 521 | enc_data = data[encrypted_data_offset:encrypted_data_offset+size] 522 | dec_data = [] 523 | for i,c in enumerate(enc_data): 524 | dec_data.append(c ^ key[i % 4]) 525 | 526 | dec_data = bytes(dec_data) 527 | self.data = dec_data 528 | return self.parse_config(version=version, quiet=quiet, as_json=as_json) 529 | 530 | 531 | if __name__ == '__main__': 532 | parser = argparse.ArgumentParser(description="Parses CobaltStrike Beacon's configuration from PE, memory dump or URL.") 533 | parser.add_argument("beacon", help="This can be a file path or a url (if started with http/s)") 534 | parser.add_argument("--json", help="Print as json", action="store_true", default=False) 535 | parser.add_argument("--quiet", help="Do not print missing or empty settings", action="store_true", default=False) 536 | parser.add_argument("--version", help="Try as specific cobalt version (3 or 4). If not specified, tries both.", type=int) 537 | args = parser.parse_args() 538 | 539 | if os.path.isfile(args.beacon): 540 | if cobaltstrikeConfig(args.beacon).parse_config(version=args.version, quiet=args.quiet, as_json=args.json) or \ 541 | cobaltstrikeConfig(args.beacon).parse_encrypted_config(version=args.version, quiet=args.quiet, as_json=args.json): 542 | exit(0) 543 | 544 | elif args.beacon.lower().startswith('http'): 545 | x86_beacon_data = get_beacon_data(args.beacon, 'x86') 546 | x64_beacon_data = get_beacon_data(args.beacon, 'x64') 547 | if not x86_beacon_data and not x64_beacon_data: 548 | print("[-] Failed to find any beacon configuration") 549 | exit(1) 550 | 551 | conf_data = x86_beacon_data or x64_beacon_data 552 | if cobaltstrikeConfig(BytesIO(conf_data)).parse_config(version=args.version, quiet=args.quiet, as_json=args.json) or \ 553 | cobaltstrikeConfig(BytesIO(conf_data)).parse_encrypted_config(version=args.version, quiet=args.quiet, as_json=args.json): 554 | exit(0) 555 | 556 | else: 557 | print("[-] Target path is not an existing file or a C2 URL") 558 | exit(1) 559 | 560 | print("[-] Failed to find any beacon configuration") 561 | exit(1) 562 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | urllib3 2 | requests 3 | netstruct==1.1.2 4 | pefile==2019.4.18 5 | #M2Crypto==0.37.1 6 | #pycryptodome==3.10.1 -------------------------------------------------------------------------------- /samples/10fd211ba97ddf12aecb1e7931d92c3ba37421c362cb1490e0203c1bd88ec141.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sentinel-One/CobaltStrikeParser/2703878a74811d047efde815429b78719afed357/samples/10fd211ba97ddf12aecb1e7931d92c3ba37421c362cb1490e0203c1bd88ec141.zip -------------------------------------------------------------------------------- /samples/13e954be0b0c022c392c956e9a800201a75dab7e288230b835bcdd4a9d68253d.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sentinel-One/CobaltStrikeParser/2703878a74811d047efde815429b78719afed357/samples/13e954be0b0c022c392c956e9a800201a75dab7e288230b835bcdd4a9d68253d.zip -------------------------------------------------------------------------------- /samples/320a5f715aa5724c21013fc14bfe0a10893ce9723ebc25d9ae9f06f5517795d4.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sentinel-One/CobaltStrikeParser/2703878a74811d047efde815429b78719afed357/samples/320a5f715aa5724c21013fc14bfe0a10893ce9723ebc25d9ae9f06f5517795d4.zip -------------------------------------------------------------------------------- /samples/4d1d732125e4d1a3ba0571e0cd892cf8e0dce854387ee405f75df4dcfb0f616b.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sentinel-One/CobaltStrikeParser/2703878a74811d047efde815429b78719afed357/samples/4d1d732125e4d1a3ba0571e0cd892cf8e0dce854387ee405f75df4dcfb0f616b.zip -------------------------------------------------------------------------------- /samples/5cd19717831e5259d535783be33f86ad7e77f8df25cd8f342da4f4f33327d989.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sentinel-One/CobaltStrikeParser/2703878a74811d047efde815429b78719afed357/samples/5cd19717831e5259d535783be33f86ad7e77f8df25cd8f342da4f4f33327d989.zip -------------------------------------------------------------------------------- /samples/7773169ca4ea81203a550dfebe53f091a8c57a3a5b12386e51c5a05194fef3ff.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sentinel-One/CobaltStrikeParser/2703878a74811d047efde815429b78719afed357/samples/7773169ca4ea81203a550dfebe53f091a8c57a3a5b12386e51c5a05194fef3ff.zip -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | import sys 3 | 4 | with open('README.md') as fh: 5 | long_description = fh.read() 6 | 7 | setuptools.setup( 8 | name="CobaltStrikeParser", 9 | version="11721a49", 10 | description="Python parser for CobaltStrike Beacon's configuration", 11 | license="Attribution-NonCommercial-ShareAlike 4.0 International", 12 | long_description=long_description, 13 | url="https://github.com/Sentinel-One/CobaltStrikeParser", 14 | py_modules=["parse_beacon_config", "beacon_utils"], 15 | install_requires=["urllib3", 16 | "requests", 17 | "netstruct==1.1.2", 18 | "pefile==2019.4.18"] 19 | ) 20 | -------------------------------------------------------------------------------- /test_parse_beacon_config.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python3 2 | 3 | import io 4 | import os 5 | import unittest 6 | 7 | from parse_beacon_config import cobaltstrikeConfig 8 | 9 | from zipfile import ZipFile 10 | 11 | 12 | def decrypt_sample(zip_path): 13 | with ZipFile(zip_path) as z: 14 | for fn in z.namelist(): 15 | return io.BytesIO(z.read(fn, pwd=bytes("infected", "ascii"))) 16 | 17 | 18 | class TestBeaconParsing(unittest.TestCase): 19 | def test_non_pe_x86(self): 20 | path = os.path.join( 21 | os.path.dirname(__file__), 22 | "samples", 23 | "13e954be0b0c022c392c956e9a800201a75dab7e288230b835bcdd4a9d68253d.zip", 24 | ) 25 | f = decrypt_sample(path) 26 | parser = cobaltstrikeConfig(f) 27 | conf = parser.parse_encrypted_config() 28 | self.assertEqual(conf.get("HttpPostUri"), "/submit.php") 29 | 30 | def test_encrypted_x86_64(self): 31 | path = os.path.join( 32 | os.path.dirname(__file__), 33 | "samples", 34 | "10fd211ba97ddf12aecb1e7931d92c3ba37421c362cb1490e0203c1bd88ec141.zip", 35 | ) 36 | f = decrypt_sample(path) 37 | parser = cobaltstrikeConfig(f) 38 | conf = parser.parse_encrypted_config() 39 | self.assertEqual(conf.get("PublicKey_MD5"), "fbc7faad3bf1d91fefde4244476c4ffd") 40 | 41 | def test_encrypted_x86(self): 42 | path = os.path.join( 43 | os.path.dirname(__file__), 44 | "samples", 45 | "7773169ca4ea81203a550dfebe53f091a8c57a3a5b12386e51c5a05194fef3ff.zip", 46 | ) 47 | f = decrypt_sample(path) 48 | parser = cobaltstrikeConfig(f) 49 | conf = parser.parse_encrypted_config() 50 | self.assertEqual(conf.get("PublicKey_MD5"), "41d0f3a319ef312f6e30a370c544477b") 51 | 52 | def test_trial_beacon_x86(self): 53 | path = os.path.join( 54 | os.path.dirname(__file__), 55 | "samples", 56 | "4d1d732125e4d1a3ba0571e0cd892cf8e0dce854387ee405f75df4dcfb0f616b.zip", 57 | ) 58 | f = decrypt_sample(path) 59 | parser = cobaltstrikeConfig(f) 60 | conf = parser.parse_config() 61 | self.assertIn('header "CGGGGG"', conf.get("HttpGet_Metadata").get("Metadata")) 62 | 63 | def test_beacon_45_x86_64(self): 64 | path = os.path.join( 65 | os.path.dirname(__file__), 66 | "samples", 67 | "320a5f715aa5724c21013fc14bfe0a10893ce9723ebc25d9ae9f06f5517795d4.zip", 68 | ) 69 | f = decrypt_sample(path) 70 | parser = cobaltstrikeConfig(f) 71 | conf = parser.parse_config() 72 | self.assertEqual(conf.get("Watermark_Hash"), "xi1knfb/QiftN2EAhdtcyw==") 73 | self.assertEqual(conf.get("Retry_Max_Attempts"), 0) 74 | self.assertEqual(conf.get("Retry_Increase_Attempts"), 0) 75 | self.assertEqual(conf.get("Retry_Duration"), 0) 76 | 77 | def test_csv4_startbytes(self): 78 | path = os.path.join( 79 | os.path.dirname(__file__), 80 | "samples", 81 | "5cd19717831e5259d535783be33f86ad7e77f8df25cd8f342da4f4f33327d989.zip", 82 | ) 83 | f = decrypt_sample(path) 84 | parser = cobaltstrikeConfig(f) 85 | conf = parser.parse_config() 86 | self.assertNotEqual(conf, None) 87 | 88 | 89 | 90 | if __name__ == "__main__": 91 | unittest.main() 92 | --------------------------------------------------------------------------------