├── LICENSE.md ├── README.md ├── agscript.sh ├── images ├── beacon_exec.png ├── config.png ├── cs_connect.png ├── cs_pivots.png ├── cs_status.png └── custom_proxy_example.png ├── proxychains_redshell.conf ├── redshell.py └── requirements.txt /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2020 Verizon 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RedShell 2 | An interactive command prompt for red teaming and pentesting. Automatically pushes commands through SOCKS4/5 proxies via proxychains. Optional Cobalt Strike integration pulls beacon SOCKS4/5 proxies from the team server. Automatically logs activities to a local CSV file and a Cobalt Strike team server (if configured). 3 | 4 | Note that because RedShell uses proxychains under the hood, only TCP traffic is proxied. 5 | 6 | # Installation 7 | RedShell runs on Python > 3.8. 8 | 9 | Install dependencies: 10 | ``` 11 | pip3 install -r requirements.txt 12 | ``` 13 | Install proxychains-ng (https://github.com/rofl0r/proxychains-ng): 14 | ``` 15 | apt install proxychains4 16 | ``` 17 | RedShell is no longer dependent on Cobalt Strike. However, if you're using Cobalt Strike integration, the CS client must be installed on the same host as RedShell. Also make the agscript wrapper executable: 18 | ``` 19 | chmod +x agscript.sh 20 | ``` 21 | 22 | # Usage 23 | Start RedShell: 24 | ``` 25 | $python3 redshell.py 26 | 27 | ____ _______ __ ____ 28 | / __ \___ ____/ / ___// /_ ___ / / / 29 | / /_/ / _ \/ __ /\__ \/ __ \/ _ \/ / / 30 | / _, _/ __/ /_/ /___/ / / / / __/ / / 31 | /_/ |_|\___/\__,_//____/_/ /_/\___/_/_/ 32 | 33 | 34 | Logging to: /home/user/.redshell/redshell_2022_08_22_13_00_13.csv 35 | 36 | redshell > 37 | 38 | ``` 39 | 40 | Display help: 41 | ``` 42 | redshell > help 43 | 44 | Documented commands (use 'help -v' for verbose/'help ' for details): 45 | =========================================================================== 46 | beacon_exec cs_connect cs_status help pwd socks 47 | cd cs_disconnect cs_use_pivot history quit 48 | config cs_load_config exec log set 49 | context cs_pivots exit proxy_exec shell 50 | 51 | ``` 52 | 53 | Set options: 54 | ``` 55 | redshell> set option VALUE 56 | ``` 57 | 58 | ## Logging 59 | RedShell automatically logs activities via the `beacon_exec`, `proxy_exec`, `exec`, or `log` commands. Logging is automatically initialized on startup, and log files are written to: `~/.redshell`. 60 | 61 | To log to Cobalt Strike, connect to a team server, select a pivot, and use the `beacon_exec` command. 62 | 63 | ## Proxies 64 | RedShell uses proxychains-ng and a custom proxychains configuration file. Configuration file modifications and command proxying are handled on-the-fly. 65 | 66 | ### Cobalt Strike 67 | To proxy through a Cobalt Strike, connect to a team server, select a pivot, and use the `beacon_exec` command. Refer to the Cobalt Strike section for details. 68 | 69 | ### Custom Proxies 70 | Custom socks version 4 or 5 proxies can be set with the `socks` command. 71 | ``` 72 | redshell > socks -h 73 | usage: socks [-h] [-u SOCKS5_USER] [-p SOCKS5_PASS] {socks4,socks5} ip_address socks_port 74 | 75 | Use a custom socks4/5 server 76 | 77 | positional arguments: 78 | {socks4,socks5} 79 | ip_address 80 | socks_port 81 | 82 | options: 83 | -h, --help show this help message and exit 84 | -u SOCKS5_USER 85 | -p SOCKS5_PASS 86 | ``` 87 | 88 | ## SOCKS Proxy Verification 89 | RedShell automatically verifies connections and authentication (where applicable) to SOCKS proxies upon selection (either using the `socks` or `cs_use_pivot` commands). This can be disabled with the following command: `set check_socks false` 90 | 91 | ## Context 92 | RedShell's context is a key aspect of activity logging. Context allows you to set the perspective (in activity logs) of the source host executing activities in a target network. The following context attributes can included in activity logs: IP Address, DNS Name, NetBIOS Name, User Name, and Process ID. Only IP Address is required. 93 | 94 | Notes on context: 95 | - Context is cleared when you set a new socks port 96 | - Context is cleared when you connect/disconnect from a CS team server 97 | 98 | ### Context - Custom Proxies 99 | After you set a socks proxy with the `socks` command, add context details with the `context` command. 100 | ``` 101 | RedShell> context -h 102 | usage: context [-h] [-d DNSNAME] [-n NETBIOSNAME] [-u USERNAME] [-p PID] ip_address 103 | 104 | Set a custom context (Source IP/DNS/NetBIOS/User/PID) for logging 105 | 106 | positional arguments: 107 | ip_address Source IP Address 108 | 109 | optional arguments: 110 | -h, --help show this help message and exit 111 | -d DNSNAME, --dnsname DNSNAME 112 | DNS Name 113 | -n NETBIOSNAME, --netbiosname NETBIOSNAME 114 | NetBIOS Name 115 | -u USERNAME, --username USERNAME 116 | User Name 117 | -p PID, --pid PID Process ID 118 | ``` 119 | 120 | ### Context - Cobalt Strike 121 | If you are using a pivot on a team server, context values are automatically set based on the beacon. 122 | 123 | ### Command Prompt 124 | The command prompt is automatically updated with context variables (user@host). 125 | 126 | ## Execute and Log 127 | The following RedShell commands are captured in activity logs: 128 | - `beacon_exec` - Execute a command through beacon socks proxy and simultaneously log it to the teamserver. 129 | - `proxy_exec` - Execute a command through custom socks proxy and simultaneously log it to the local file. 130 | - `exec` - Execute a command and log it to the local file. 131 | - `log` - Add a manual log entry to the local file. 132 | 133 | ## Custom Proxy Example 134 | ![alt text](./images/custom_proxy_example.png "Custom Proxy Example") 135 | 136 | ## Cobalt Strike 137 | ### Connecting to Cobalt Strike 138 | 139 | Set Cobalt Strike connection options: 140 | ``` 141 | redshell > set cs_host 127.0.0.1 142 | redshell > set cs_port 50050 143 | redshell > set cs_user somedude 144 | ``` 145 | 146 | Connect to team server (you will be prompted for the team server password): 147 | ``` 148 | redshell > cs_connect 149 | ``` 150 | Example: 151 | 152 | ![alt text](./images/cs_connect.png "CS Connect") 153 | 154 | Or load from a config file. Note: team server passwords are not read from config files. RedShell will prompt for the teamserver password and then automatically connect. 155 | ``` 156 | $ cat config.txt 157 | cs_host=127.0.0.1 158 | cs_port=12345 159 | cs_user=somedude 160 | cs_directory=/path/to/cobaltstrike/install 161 | ``` 162 | ``` 163 | redshell > cs_load_config config.txt 164 | ``` 165 | 166 | Show available proxy pivots: 167 | ``` 168 | redshell > cs_pivots 169 | ``` 170 | Example: 171 | 172 | ![alt text](./images/cs_pivots.png "CS Pivots") 173 | 174 | Select a proxy pivot (note: this can only be set after a connection to the team server has been established): 175 | ``` 176 | redshell > cs_use_pivot 2 177 | SOCKS5 pivot requires authentication. 178 | 179 | Enter SOCKS5 user: username 180 | Enter SOCKS5 password: 181 | ``` 182 | Check Cobalt Strike status: 183 | ``` 184 | redshell > cs_status 185 | ``` 186 | Example: 187 | 188 | ![alt text](./images/cs_status.png "CS Status") 189 | 190 | Execute commands through the beacon socks proxy. These can be run in the context of the current user or via sudo. Specifying 'proxychains' in the command is optional. Commands are forced through proxychains. MITRE ATT&CK Tactic IDs are optional. 191 | ``` 192 | redshell > beacon_exec -h 193 | usage: beacon_exec [-h] [-t TTP] ... 194 | 195 | Execute a command through beacon socks proxy and simultaneously log it to the teamserver. 196 | 197 | positional arguments: 198 | command Command to execute through the proxy and log. 199 | 200 | optional arguments: 201 | -h, --help show this help message and exit 202 | -t TTP, --ttp TTP MITRE ATT&CK Tactic IDs. Comma delimited to specify multiple. 203 | 204 | example: 205 | beacon_exec -t T1550.002,T1003.002 cme smb 192.168.1.1 --local-auth -u Administrator -H C713B1D611657D0687A568122193F230 --sam 206 | ``` 207 | Example: 208 | 209 | ![alt text](./images/beacon_exec.png "Beacon Exec") 210 | 211 | Note on the Redshell and CS install directory options - the script needs to know where it lives, as well as Cobalt Strike. 212 | If stuff blows up, be sure to set the directories accordingly: 213 | ``` 214 | redshell > set redshell_directory /opt/redshell 215 | redshell > set cs_directory /opt/cobaltstrike 216 | ``` 217 | 218 | ## General 219 | Note on passwords used in *exec commands: special characters in passwords may be interpreted as shell meta characters, which could cause commands to fail. To get around this, set the password option and then invoke with '$password'. Example: 220 | ``` 221 | redshell > set password Test12345 222 | password - was: '' 223 | now: 'Test12345' 224 | redshell > beacon_exec cme smb 192.168.1.14 --local-auth -u administrator -p $password --shares 225 | ``` 226 | 227 | RedShell includes commands for navigating the file system: 228 | ``` 229 | redshell > cd /opt/redshell/ 230 | redshell > pwd 231 | /opt/redshell 232 | ``` 233 | 234 | Additional commands can be run via the shell command or via the '!' shortcut: 235 | ``` 236 | redshell > shell date 237 | Mon 29 Jul 2019 05:33:02 PM MDT 238 | redshell > !date 239 | Mon 29 Jul 2019 05:33:03 PM MDT 240 | ``` 241 | 242 | Commands are tracked and accessible via the history command: 243 | ``` 244 | redshell > history 245 | 1 load_config config.txt 246 | 2 status 247 | 3 help 248 | ``` 249 | 250 | RedShell also includes tab-completion and clearing the terminal window via ctrl + l. 251 | 252 | ## CSV Log Format 253 | ``` 254 | Datetime,IP Address,DNS Name,NetBIOS Name,User,PID,Activity,TTPs 255 | 2021/09/21 14:22:32 +0000,192.168.56.106,,WINDEV,USER,7312,[PROXY] cme smb 192.168.56.105, 256 | ``` 257 | 258 | Notes: 259 | - Required fields: Datetime, IP Address, Activity 260 | - Optional fields: DNS Name, NetBIOS Name, User, PID, TTPs 261 | - Datetime format: "%Y/%m/%d %H:%M:%S %z" (UTC) 262 | 263 | ## Maintainers 264 | 265 | - [exfiltrata](https://github.com/exfiltrata) 266 | 267 | ## License 268 | 269 | This project is licensed under the terms of the Apache 2.0 open source license. Please refer to [LICENSE](LICENSE.md) for the full terms. -------------------------------------------------------------------------------- /agscript.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Copyright Verizon. 4 | # Licensed under the terms of the Apache 2.0 license. See LICENSE file in project root for terms. 5 | 6 | cd $1 7 | ./agscript $2 $3 $4 $5 8 | -------------------------------------------------------------------------------- /images/beacon_exec.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Verizon/redshell/cee80ad1a14455f0273123456e3ce3b2c36a38a0/images/beacon_exec.png -------------------------------------------------------------------------------- /images/config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Verizon/redshell/cee80ad1a14455f0273123456e3ce3b2c36a38a0/images/config.png -------------------------------------------------------------------------------- /images/cs_connect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Verizon/redshell/cee80ad1a14455f0273123456e3ce3b2c36a38a0/images/cs_connect.png -------------------------------------------------------------------------------- /images/cs_pivots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Verizon/redshell/cee80ad1a14455f0273123456e3ce3b2c36a38a0/images/cs_pivots.png -------------------------------------------------------------------------------- /images/cs_status.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Verizon/redshell/cee80ad1a14455f0273123456e3ce3b2c36a38a0/images/cs_status.png -------------------------------------------------------------------------------- /images/custom_proxy_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Verizon/redshell/cee80ad1a14455f0273123456e3ce3b2c36a38a0/images/custom_proxy_example.png -------------------------------------------------------------------------------- /proxychains_redshell.conf: -------------------------------------------------------------------------------- 1 | # proxychains.conf VER 4.x 2 | # 3 | # HTTP, SOCKS4a, SOCKS5 tunneling proxifier with DNS. 4 | 5 | 6 | # Strict - Each connection will be done via chained proxies 7 | # all proxies chained in the order as they appear in the list 8 | # all proxies must be online to play in chain 9 | # otherwise EINTR is returned to the app 10 | strict_chain 11 | 12 | 13 | # Quiet mode (no output from library) 14 | quiet_mode 15 | 16 | 17 | # Proxy DNS requests - no leak for DNS data 18 | proxy_dns 19 | 20 | 21 | # Some timeouts in milliseconds 22 | tcp_read_time_out 15000 23 | tcp_connect_time_out 8000 24 | 25 | [ProxyList] 26 | socks4 127.0.0.1 12345 -------------------------------------------------------------------------------- /redshell.py: -------------------------------------------------------------------------------- 1 | # Copyright Verizon. 2 | # Licensed under the terms of the Apache 2.0 license. See LICENSE file in project root for terms. 3 | 4 | #!/bin/env python3 5 | 6 | import argparse 7 | import csv 8 | import fileinput 9 | import functools 10 | import getpass 11 | import os 12 | import re 13 | import shlex 14 | import shutil 15 | import socket 16 | import struct 17 | import sys 18 | import textwrap 19 | from datetime import datetime, timezone 20 | 21 | import pexpect 22 | from cmd2 import Cmd, Settable, ansi, with_argparser 23 | from rich import box 24 | from rich.console import Console 25 | from rich.prompt import Prompt 26 | from rich.table import Table 27 | 28 | # define colors 29 | green = functools.partial(ansi.style, fg=ansi.RgbFg(65, 255, 0)) 30 | red = functools.partial(ansi.style, fg=ansi.RgbFg(239, 41, 41)) 31 | 32 | 33 | def xstr(s): 34 | """return empty string if input is none/false""" 35 | 36 | return '' if not s else s 37 | 38 | 39 | class Logger(): 40 | 41 | logs = [] 42 | logfile_csv = None 43 | csv_writer = None 44 | 45 | def __init__(self, command, ip=None, dns_name=None, netbios_name=None, user_name=None, pid=None, ttps=None): 46 | self.timestamp = datetime.now(timezone.utc) 47 | self.command = command 48 | self.ip = ip 49 | self.dns_name = dns_name 50 | self.netbios_name = netbios_name 51 | self.user_name = user_name 52 | self.pid = pid 53 | self.ttps = ttps 54 | self.write_log_entry() 55 | Logger.logs.append(self) 56 | 57 | 58 | @classmethod 59 | def open_logfile(cls, basefilename): 60 | 61 | logfile_csv = f"{basefilename}.csv" 62 | print(f"Logging to: {logfile_csv}\n") 63 | cls.logfile_csv = open(logfile_csv, 'w', newline='') 64 | 65 | fieldnames = ['Datetime', 'IP Address', 'DNS Name', 'NetBIOS Name', 'User', 'PID', 'Activity', 'TTPs'] 66 | 67 | cls.csv_writer = csv.DictWriter(cls.logfile_csv, fieldnames=fieldnames) 68 | cls.csv_writer.writeheader() 69 | cls.logfile_csv.flush() 70 | 71 | def asdict(self): 72 | 73 | return {'Datetime': self.timestamp.strftime("%Y/%m/%d %H:%M:%S %z"), 74 | 'IP Address': xstr(self.ip), 75 | 'DNS Name': xstr(self.dns_name), 76 | 'NetBIOS Name': xstr(self.netbios_name), 77 | 'User': xstr(self.user_name), 78 | 'PID': xstr(self.pid), 79 | 'Activity': self.command, 80 | 'TTPs': self.ttps} 81 | 82 | def write_log_entry(self): 83 | 84 | Logger.csv_writer.writerow(self.asdict()) 85 | Logger.logfile_csv.flush() 86 | 87 | @classmethod 88 | def close_logfile(cls): 89 | 90 | cls.logfile_csv.close() 91 | 92 | 93 | class CSProxyPivots(): 94 | 95 | instances = {} 96 | by_hash = {} 97 | count = 0 98 | 99 | def __init__(self, socks_type, socks_port, bid, beacon_pid, beacon_user, beacon_computer, beacon_ip, beacon_alive, beacon_last, socks5_auth=None): 100 | self.socks_type = socks_type 101 | self.socks_port = socks_port 102 | self.socks5_auth = socks5_auth 103 | self.socks5_user = None 104 | self.socks5_pass = None 105 | self.bid = bid 106 | self.beacon_pid = beacon_pid 107 | self.beacon_user = beacon_user 108 | self.beacon_computer = beacon_computer 109 | self.beacon_ip = beacon_ip 110 | self.beacon_alive = beacon_alive 111 | self.beacon_last = beacon_last 112 | self.beacon_hash = hash(f"{bid}-{beacon_pid}-{socks_port}") 113 | 114 | CSProxyPivots.count += 1 115 | 116 | self.id = CSProxyPivots.count 117 | CSProxyPivots.instances[self.id] = self 118 | CSProxyPivots.by_hash[self.beacon_hash] = self 119 | 120 | @classmethod 121 | def get_pivots(cls): 122 | 123 | data = [] 124 | 125 | data.append(['ID', 'Alive', 'Socks Type', 'Socks Port', 'Socks5 Auth', 'Beacon PID', 'Beacon User', 'Beacon Computer', 'Beacon Last']) 126 | 127 | for pivot in CSProxyPivots.instances.values(): 128 | data.append([str(pivot.id), str(pivot.beacon_alive), pivot.socks_type, pivot.socks_port, pivot.socks5_auth, pivot.beacon_pid, pivot.beacon_user, pivot.beacon_computer, pivot.beacon_last]) 129 | 130 | return data 131 | 132 | 133 | class RedShell(Cmd): 134 | 135 | prompt = 'redshell > ' 136 | 137 | def __init__(self): 138 | super().__init__() 139 | 140 | # remove built-in commands 141 | try: 142 | del Cmd.do_alias 143 | del Cmd.do_edit 144 | del Cmd.do_macro 145 | del Cmd.do_run_pyscript 146 | del Cmd.do_run_script 147 | del Cmd.do_shortcuts 148 | del Cmd.do_py 149 | except AttributeError: 150 | pass 151 | 152 | # remove built-in settings 153 | for key in ['allow_style', 'always_show_hint', 'editor', 'echo', 'feedback_to_output', 'quiet', 'timing', 'max_completion_items']: 154 | try: 155 | self.remove_settable(key) 156 | except: 157 | pass 158 | 159 | self.display_intro() 160 | 161 | # check/create redshell user dir 162 | home_dir = os.path.expanduser("~") 163 | self.redshell_user_directory = f"{home_dir}/.redshell/" 164 | 165 | if not os.path.exists(self.redshell_user_directory): 166 | os.makedirs(self.redshell_user_directory) 167 | 168 | # set cobalt strike directory, if exists 169 | if os.path.exists('/opt/cobaltstrike'): 170 | self.cs_directory = '/opt/cobaltstrike' 171 | else: 172 | self.cs_directory = '' 173 | 174 | # set config variables 175 | self.redshell_directory = os.getcwd() 176 | self.proxychains_config = f"{self.redshell_directory}/proxychains_redshell.conf" 177 | self.cs_host = '' 178 | self.cs_port = '' 179 | self.cs_user = '' 180 | self.cs_pass = '' 181 | self.cs_process = None 182 | self.cs_beacon_pid = '' 183 | self.cs_beacon_id = '' 184 | self.cs_beacon_user = '' 185 | self.cs_beacon_computer = '' 186 | self.cs_beacon_ip = '' 187 | self.context_ip = '' 188 | self.context_dns_name = '' 189 | self.context_netbios_name = '' 190 | self.context_user_name = '' 191 | self.context_pid = '' 192 | self.socks_host = '' 193 | self.socks_port = '' 194 | self.socks_type = '' 195 | self.socks5_auth = '' 196 | self.socks5_user = '' 197 | self.socks5_pass = '' 198 | self.socks_port_connected = False 199 | self.password = '' 200 | self.check_socks = True 201 | 202 | # initialze user settable options 203 | self.add_settable(Settable('redshell_directory', str, 'redshell install directory', self, completer=Cmd.path_complete, onchange_cb=self._onchange_redshell_directory)) 204 | self.add_settable(Settable('proxychains_config', str, 'proxychains config file', self, completer=Cmd.path_complete)) 205 | self.add_settable(Settable('cs_directory', str, 'Cobalt Strike install directory', self, completer=Cmd.path_complete)) 206 | self.add_settable(Settable('cs_host', str, 'Cobalt Strike team server host', self)) 207 | self.add_settable(Settable('cs_port', str, 'Cobalt Strike team server port', self)) 208 | self.add_settable(Settable('cs_user', str, 'Cobalt Strike user', self, onchange_cb=self._onchange_cs_user)) 209 | self.add_settable(Settable('password', str, 'Password for beacon_exec commands. Invoke with $password.', self)) 210 | self.add_settable(Settable('check_socks', bool, 'Validate connections/authentication to SOCKS servers', self)) 211 | 212 | # start logger 213 | now = datetime.now() 214 | timestamp = now.strftime("%Y_%m_%d_%H_%M_%S") 215 | basefilename = f"{self.redshell_user_directory}redshell_{timestamp}" 216 | Logger.open_logfile(basefilename) 217 | 218 | def _set_prompt(self): 219 | 220 | if self.socks_port_connected == True: 221 | color = green 222 | else: 223 | color = red 224 | 225 | # set prompt with context vars 226 | if self.context_user_name and self.context_netbios_name: 227 | self.prompt = f"redshell ({color(f'{self.context_user_name}@{self.context_netbios_name}')}) > " 228 | 229 | elif self.context_user_name and self.context_dns_name: 230 | self.prompt = f"redshell ({color(f'{self.context_user_name}@{self.context_dns_name}')}) > " 231 | 232 | elif self.context_dns_name: 233 | self.prompt = f"redshell ({color(f'@{self.context_dns_name}')}) > " 234 | 235 | elif self.context_ip: 236 | self.prompt = f"redshell ({color(f'@{self.context_ip}')}) > " 237 | 238 | def postcmd(self, stop, line): 239 | 240 | self._set_prompt() 241 | return stop 242 | 243 | 244 | def display_intro(self): 245 | 246 | intro = """ 247 | ____ _______ __ ____ 248 | / __ \___ ____/ / ___// /_ ___ / / / 249 | / /_/ / _ \/ __ /\__ \/ __ \/ _ \/ / / 250 | / _, _/ __/ /_/ /___/ / / / / __/ / / 251 | /_/ |_|\___/\__,_//____/_/ /_/\___/_/_/ 252 | 253 | """ 254 | self.poutput(green(intro)) 255 | 256 | 257 | def _onchange_redshell_directory(self, param_name, old, new): 258 | 259 | self.proxychains_config = f"{self.redshell_directory}/proxychains_redshell.conf" 260 | 261 | # append '_redshell' to CS username 262 | def _onchange_cs_user(self, param_name, old, new): 263 | 264 | self.cs_user += '_redshell' 265 | 266 | def print_table(self, data, header=False): 267 | """print all tables in console output""" 268 | 269 | if header: 270 | table = Table(show_lines=True, show_header=header) 271 | 272 | else: 273 | table = Table(show_lines=True, show_header=header, box=box.SQUARE) 274 | 275 | column_count = range(0, len(data[0])) 276 | 277 | for i in column_count: 278 | 279 | if header: 280 | table.add_column(data[0][i]) 281 | 282 | else: 283 | table.add_column() 284 | 285 | for row in data: 286 | 287 | if header and data.index(row) == 0: 288 | continue 289 | 290 | table.add_row(*row) 291 | 292 | console = Console() 293 | console.print(table) 294 | 295 | def update_proxychains_conf(self, socks_type, ip, socks_port, socks5_auth=None, socks5_user=None, socks5_pass=None): 296 | 297 | for line in fileinput.input(self.proxychains_config, inplace=True): 298 | if line.startswith('socks'): 299 | 300 | if socks_type == 'socks5' and socks5_auth: 301 | print(f"{socks_type} {ip} {socks_port} {socks5_user} {socks5_pass}", end="\n") 302 | 303 | else: 304 | print(f"{socks_type} {ip} {socks_port}", end="\n") 305 | 306 | else: 307 | print(line, end = '') 308 | 309 | def clear_context(self, clear_socks=False, clear_cs=False): 310 | 311 | # clear existing connection 312 | self.cs_beacon_id = '' 313 | self.cs_beacon_pid = '' 314 | self.cs_beacon_user = '' 315 | self.cs_beacon_computer = '' 316 | self.cs_beacon_ip = '' 317 | self.context_ip = '' 318 | self.context_dns_name = '' 319 | self.context_netbios_name = '' 320 | self.context_user_name = '' 321 | self.context_pid = '' 322 | 323 | # clear socks port if user is applying a new one 324 | if clear_socks: 325 | self.socks_host = '' 326 | self.socks_port = '' 327 | self.socks_type = '' 328 | self.socks5_auth = '' 329 | self.socks5_user = '' 330 | self.socks5_pass = '' 331 | self.socks_port_connected = False 332 | 333 | # if connected to cs team server, kill connection 334 | if clear_cs: 335 | # close the agscript process 336 | if self.cs_process: 337 | self.cs_process.close() 338 | self.cs_process = None 339 | 340 | argparser = argparse.ArgumentParser() 341 | argparser.add_argument('-d', '--dnsname', type=str, help="DNS Name") 342 | argparser.add_argument('-n', '--netbiosname', type=str, help="NetBIOS Name") 343 | argparser.add_argument('-u', '--username', type=str, help="User Name") 344 | argparser.add_argument('-p', '--pid', type=str, help="Process ID") 345 | argparser.add_argument(type=str, dest="ip_address", help="Source IP Address") 346 | @with_argparser(argparser) 347 | def do_context(self, args): 348 | """Set a custom context (Source IP/DNS/NetBIOS/User/PID) for logging""" 349 | 350 | if self.context_ip: 351 | self.poutput("Context changed!") 352 | self.pwarning("WARNING: If moving to a new socks port, be sure to update your socks connection accordingly.") 353 | else: 354 | self.poutput("New context applied!") 355 | 356 | # if connected to cs team server, kill connection and socks. else clear context values only 357 | if self.cs_process: 358 | self.clear_context(clear_socks=True, clear_cs=True) 359 | else: 360 | self.clear_context() 361 | 362 | self.context_ip = args.ip_address 363 | 364 | if args.dnsname: 365 | self.context_dns_name = args.dnsname 366 | if args.netbiosname: 367 | self.context_netbios_name = args.netbiosname 368 | if args.username: 369 | self.context_user_name = args.username 370 | if args.pid: 371 | self.context_pid = args.pid 372 | 373 | argparser = argparse.ArgumentParser() 374 | argparser.add_argument(type=str, dest="socks_type", choices=['socks4', 'socks5']) 375 | argparser.add_argument(type=str, dest="ip_address") 376 | argparser.add_argument(type=str, dest="socks_port") 377 | argparser.add_argument('-u', type=str, dest="socks5_user") 378 | argparser.add_argument('-p', type=str, dest="socks5_pass") 379 | @with_argparser(argparser) 380 | def do_socks(self, args): 381 | """Use a custom socks4/5 server""" 382 | 383 | # clear any existing context, socks port, and cobalt strike connections 384 | self.clear_context(clear_socks=True, clear_cs=True) 385 | 386 | self.socks_type = args.socks_type 387 | self.socks_host = args.ip_address 388 | self.socks_port = args.socks_port 389 | self.socks_port_connected = True 390 | 391 | if args.socks_type == 'socks5': 392 | 393 | if (args.socks5_user and not args.socks5_pass) or (args.socks5_pass and not args.socks5_user): 394 | 395 | if args.socks_user: 396 | self.perror("ERROR: SOCKS5 user set but missing password!") 397 | else: 398 | self.perror("ERROR: SOCKS5 password set but missing user!") 399 | return 400 | 401 | elif args.socks5_user and args.socks5_pass: 402 | self.socks5_auth = 'UserAndPwd' 403 | self.socks5_user = args.socks5_user 404 | self.socks5_pass = args.socks5_pass 405 | 406 | else: 407 | self.socks5_auth = '' 408 | self.socks5_user = '' 409 | self.socks5_pass = '' 410 | 411 | if self.check_socks: 412 | 413 | if self.validate_socks(self.socks_type, self.socks_host, self.socks_port, self.socks5_auth, self.socks5_user, self.socks5_pass): 414 | 415 | self.update_proxychains_conf(self.socks_type, self.socks_host, self.socks_port, self.socks5_auth, self.socks5_user, self.socks5_pass) 416 | self.socks_port_connected = True 417 | 418 | else: 419 | self.clear_context(clear_socks=True) 420 | return 421 | 422 | else: 423 | self.update_proxychains_conf(self.socks_type, self.cs_host, self.socks_port, self.socks5_auth, self.socks5_user, self.socks5_pass) 424 | self.socks_port_connected = True 425 | 426 | self.poutput("Socks port updated.") 427 | self.pwarning("WARNING: Be sure to update your context accordingly with the 'context' command.") 428 | 429 | def do_cs_connect(self, args): 430 | """Connect to Cobalt Strike team server""" 431 | 432 | self.clear_context(clear_socks=True) 433 | 434 | # check config directories before attempting connection 435 | if not os.path.exists(f"{self.redshell_directory}/agscript.sh"): 436 | self.perror("Error: redshell install directory not found! Set the directory with this command: 'set redshell_directory'") 437 | return 438 | 439 | if not os.path.exists(f"{self.cs_directory}/agscript"): 440 | self.perror("Error: Cobalt Strike install directory not found! Set the directory with this command: 'set cs_directory'") 441 | return 442 | 443 | # check permissions on agscript.sh 444 | if not shutil.which(f"{self.redshell_directory}/agscript.sh"): 445 | self.perror("Error: agscript.sh does not appear to be executable! Fix it with this command: 'chmod +x agscript.sh'") 446 | return 447 | 448 | # prompt user for team server password 449 | self.cs_pass = getpass.getpass("Enter Cobalt Strike password: ") 450 | 451 | # spawn agscript process 452 | self.cs_process = pexpect.spawn(f"{self.redshell_directory}/agscript.sh {self.cs_directory} {self.cs_host} {self.cs_port} {self.cs_user} {self.cs_pass}") 453 | 454 | # check if process is alive 455 | if not self.cs_process.isalive(): 456 | self.perror("Error connecting to CS team server! Check config and try again.") 457 | return 458 | 459 | # look for the aggressor prompt 460 | try: 461 | self.cs_process.expect('.*aggressor.*> ') 462 | except: 463 | self.perror("Error connecting to CS team server! Check config and try again.") 464 | return 465 | 466 | self.poutput("Connecting...") 467 | 468 | # upon successful connection, display status 469 | self.do_cs_status('') 470 | 471 | def do_cs_disconnect(self, args): 472 | """Disconnect from CS team server""" 473 | 474 | self.clear_context(clear_socks=True, clear_cs=True) 475 | 476 | 477 | def do_cs_status(self, args): 478 | """Display CS team server and beacon socks port connection status""" 479 | 480 | if self.cs_process and self.cs_process.isalive(): 481 | cs_server_status = f"[#41FF00]Connected via {self.cs_user}@{self.cs_host}:{self.cs_port}[/]" 482 | 483 | else: 484 | cs_server_status = "[#EF2929]Disconnected[/]" 485 | 486 | if self.cs_process and self.cs_process.isalive() and self.socks_port_connected: 487 | socks_port_status = f"[#41FF00]Connected via {self.socks_type} port {self.socks_port} @ beacon PID {self.cs_beacon_pid}[/]" 488 | 489 | else: 490 | socks_port_status = "[#EF2929]Disconnected[/]" 491 | 492 | data = [ 493 | ["[i]CS Team Server Status[/]", cs_server_status], 494 | ["[i]Socks Port Status[/]", socks_port_status] 495 | ] 496 | 497 | self.print_table(data) 498 | 499 | argparser = argparse.ArgumentParser() 500 | argparser.add_argument('-s', action='store_true', dest='show_secrets', help="Show secrets") 501 | @with_argparser(argparser) 502 | def do_config(self, args): 503 | """Display current config""" 504 | 505 | data = [ 506 | ["[i]Redshell Install Directory[/]", self.redshell_directory], 507 | ["[i]Proxychains Config[/]", self.proxychains_config], 508 | ["[i]Log File[/]", Logger.logfile_csv.name], 509 | ["[i]CS Install Directory[/]", self.cs_directory], 510 | ] 511 | 512 | if self.cs_host: 513 | data.append(["[i]CS Team Server[/]", self.cs_host]) 514 | data.append(["[i]CS Team Server Port[/]", self.cs_port]) 515 | data.append(["[i]CS User[/]", self.cs_user]) 516 | 517 | if self.socks_port: 518 | data.append(["[i]Socks Connection", f"{self.socks_type}://{self.socks_host}:{self.socks_port}"]) 519 | 520 | if self.socks5_auth: 521 | data.append(["[i]Socks5 User", self.socks5_user]) 522 | data.append(["[i]Socks5 Password", self.socks5_pass if args.show_secrets else '*' * len(self.socks5_pass)]) 523 | 524 | else: 525 | data.append(["[i]Socks Connection", '']) 526 | 527 | context = '' 528 | if self.context_ip: 529 | context += f"[i]IP:[/] {self.context_ip}" 530 | if self.context_dns_name: 531 | context += f" [i]DNS:[/] {self.context_dns_name}" 532 | if self.context_netbios_name: 533 | context += f" [i]NetBIOS:[/] {self.context_netbios_name}" 534 | if self.context_user_name: 535 | context += f" [i]User:[/] {self.context_user_name}" 536 | if self.context_pid: 537 | context += f" [i]PID:[/] {self.context_pid}" 538 | 539 | data.append(["[i]Context[/]", context]) 540 | 541 | if self.password: 542 | data.append(["[i]Password[/]", self.password if args.show_secrets else '*' * len(self.password)]) 543 | 544 | self.print_table(data) 545 | 546 | argparser = argparse.ArgumentParser() 547 | argparser.add_argument(type=str, dest="file_name", completer=Cmd.path_complete) 548 | @with_argparser(argparser) 549 | def do_cs_load_config(self, args): 550 | """Load Cobalt Strike team server config (host, port, and user) from file""" 551 | 552 | self.clear_context(clear_socks=True) 553 | 554 | try: 555 | with open(args.file_name, 'r') as cf: 556 | for line in cf.readlines(): 557 | 558 | cs_host = re.search('cs_host=(.*)', line) 559 | if cs_host: 560 | self.cs_host = cs_host.group(1) 561 | 562 | cs_port = re.search('cs_port=(.*)', line) 563 | if cs_port: 564 | self.cs_port = cs_port.group(1) 565 | 566 | cs_directory = re.search('cs_directory=(.*)', line) 567 | if cs_directory: 568 | self.cs_directory = cs_directory.group(1).strip(' ') 569 | 570 | cs_user = re.search('cs_user=(.*)', line) 571 | if cs_user: 572 | self.cs_user = cs_user.group(1) 573 | self.cs_user += '_redshell' 574 | 575 | self.poutput("Config applied:") 576 | self.do_config('') 577 | self.do_cs_connect('') 578 | 579 | except FileNotFoundError: 580 | self.perror("Error: config file not found!") 581 | 582 | 583 | def do_cs_pivots(self, args): 584 | """Show Cobalt Strike proxy pivots available on the team server""" 585 | 586 | # check for active connection to the team server 587 | if not self.cs_process or not self.cs_process.isalive(): 588 | self.perror("Error: not connected to CS team server. Connect first and then select a pivot.") 589 | self.clear_context(clear_socks=True) 590 | return 591 | 592 | # ask agscript for pivots 593 | self.cs_process.sendline('x pivots()') 594 | self.cs_process.expect('.*aggressor.*> ') 595 | 596 | if self.cs_process.after: 597 | 598 | # copy instance containers and clear them to reset 599 | pivot_instances = CSProxyPivots.instances.copy() 600 | pivot_instances_by_hash = CSProxyPivots.by_hash.copy() 601 | CSProxyPivots.instances.clear() 602 | CSProxyPivots.by_hash.clear() 603 | CSProxyPivots.count = 0 604 | 605 | # parse through results 606 | for result in re.findall('%\([^()]*\)', self.cs_process.after.decode()): 607 | 608 | pivot_socks_type = None 609 | pivot_socks_port = None 610 | pivot_socks5_auth = None 611 | pivot_bid = None 612 | pivot_pid = None 613 | pivot_user = None 614 | pivot_computer = None 615 | pivot_alive = None 616 | pivot_last = None 617 | 618 | # get socks type 619 | result_socks_type = re.search("type => '(SOCKS[5|4a]).*?'", result) 620 | if result_socks_type: 621 | pivot_socks_type = result_socks_type.group(1).lower() 622 | 623 | # get socks5 auth 624 | result_socks_info = re.search("socks_info => '(.*?)'", result) 625 | if result_socks_info: 626 | socks_info = result_socks_info.group(1).lower() 627 | if 'userandpwd' in socks_info and not 'noauth' in socks_info: 628 | pivot_socks5_auth = 'userandpwd' 629 | 630 | # get socks port 631 | result_port = re.search("port => '([0-9]+)'", result) 632 | if result_port: 633 | pivot_socks_port = result_port.group(1) 634 | 635 | # get beacon ID 636 | result_bid = re.search("bid => '([0-9]+)'", result) 637 | if result_bid: 638 | pivot_bid = result_bid.group(1) 639 | 640 | if pivot_bid: 641 | 642 | # get full beacon info for beacon ID 643 | self.cs_process.sendline(f"x beacon_info({pivot_bid})") 644 | self.cs_process.expect('.*aggressor.*> ') 645 | 646 | if self.cs_process.after: 647 | 648 | beacon_info = self.cs_process.after.decode() 649 | 650 | # check if beacon is alive or dead 651 | result_alive = re.search("alive => 'true'", beacon_info) 652 | if result_alive: 653 | pivot_alive = True 654 | 655 | # get beacon user 656 | result_user = re.search("user => '(.*?)'", beacon_info) 657 | if result_user: 658 | pivot_user = result_user.group(1) 659 | 660 | # get beacon computer 661 | result_computer = re.search("computer => '(.*?)'", beacon_info) 662 | if result_computer: 663 | pivot_computer = result_computer.group(1) 664 | 665 | # get beacon ip 666 | result_ip = re.search("internal => '(.*?)'", beacon_info) 667 | if result_ip: 668 | pivot_ip = result_ip.group(1) 669 | 670 | # get beacon pid 671 | result_pid = re.search("pid => '([0-9]+)'", beacon_info) 672 | if result_pid: 673 | pivot_pid = result_pid.group(1) 674 | 675 | # get beacon last 676 | result_last = re.search("lastf => '(.*?)'", beacon_info) 677 | if result_last: 678 | pivot_last = result_last.group(1) 679 | 680 | # intialize ProxyPivot instance if we have all the necessary details 681 | if pivot_socks_type and pivot_socks_port and pivot_bid and pivot_pid and pivot_user and pivot_computer and pivot_alive and pivot_last: 682 | 683 | # look for existing pivot instance 684 | pivot_hash = hash(f"{pivot_bid}-{pivot_pid}-{pivot_socks_port}") 685 | cs_pivot = pivot_instances_by_hash.get(pivot_hash) 686 | 687 | # if we have an existing pivot, just update alive and last values 688 | if cs_pivot: 689 | cs_pivot.beacon_alive = pivot_alive 690 | cs_pivot.beacon_last = pivot_last 691 | 692 | CSProxyPivots.count += 1 693 | 694 | cs_pivot.id = CSProxyPivots.count 695 | 696 | # add existing instance back into class containers 697 | CSProxyPivots.instances[cs_pivot.id] = cs_pivot 698 | CSProxyPivots.by_hash[pivot_hash] = cs_pivot 699 | 700 | # none found, make a new instance 701 | else: 702 | CSProxyPivots(pivot_socks_type, pivot_socks_port, pivot_bid, pivot_pid, pivot_user, pivot_computer, pivot_ip, pivot_alive, pivot_last, pivot_socks5_auth) 703 | 704 | # display ProxyPivot table 705 | if CSProxyPivots.instances.items(): 706 | 707 | self.print_table(CSProxyPivots.get_pivots(), header=True) 708 | 709 | else: 710 | self.pwarning("No proxy pivots found!") 711 | 712 | 713 | def do_cs_use_pivot(self, arg_pivot_id): 714 | """Set RedShell to use Cobalt Strike pivot ID""" 715 | 716 | self.clear_context(clear_socks=True) 717 | 718 | # check for active connection to the team server 719 | if not self.cs_process or not self.cs_process.isalive(): 720 | self.perror("Error: not connected to CS team server. Connect first and then select a pivot.") 721 | return 722 | 723 | if not CSProxyPivots.instances: 724 | self.perror("No pivots found! Run 'cs_pivots' to query them on the team server") 725 | return 726 | 727 | # convert arg to int 728 | try: 729 | pivot_id = int(arg_pivot_id) 730 | except ValueError: 731 | self.perror('Invalid pivot ID, must be int!') 732 | return 733 | 734 | # get pivot instance by specified ID 735 | proxy_pivot = CSProxyPivots.instances.get(pivot_id) 736 | 737 | if proxy_pivot: 738 | 739 | if proxy_pivot.beacon_alive: 740 | 741 | # set config vars from selected ProxyPiot instance 742 | self.cs_beacon_id = proxy_pivot.bid 743 | self.cs_beacon_pid = proxy_pivot.beacon_pid 744 | self.cs_beacon_user = proxy_pivot.beacon_user.replace(' *', '') 745 | self.cs_beacon_computer = proxy_pivot.beacon_computer 746 | self.cs_beacon_ip = proxy_pivot.beacon_ip 747 | self.context_pid = proxy_pivot.beacon_pid 748 | self.context_user_name = proxy_pivot.beacon_user.replace(' *', '') 749 | self.context_netbios_name = proxy_pivot.beacon_computer 750 | self.context_ip = proxy_pivot.beacon_ip 751 | self.socks_host = self.cs_host 752 | self.socks_port = proxy_pivot.socks_port 753 | self.socks_type = proxy_pivot.socks_type 754 | self.socks5_auth = proxy_pivot.socks5_auth 755 | 756 | # collect socks5 creds 757 | if self.socks_type == 'socks5' and self.socks5_auth: 758 | 759 | if not proxy_pivot.socks5_user and not proxy_pivot.socks5_pass: 760 | 761 | self.poutput("SOCKS5 pivot requires authentication.\n") 762 | 763 | proxy_pivot.socks5_user = self.read_input("Enter SOCKS5 user: ") 764 | self.socks5_user = proxy_pivot.socks5_user 765 | 766 | proxy_pivot.socks5_pass = getpass.getpass("Enter SOCKS5 password: ") 767 | self.socks5_pass = proxy_pivot.socks5_pass 768 | 769 | else: 770 | self.socks5_user = proxy_pivot.socks5_user 771 | self.socks5_pass = proxy_pivot.socks5_pass 772 | 773 | if self.check_socks: 774 | 775 | if self.validate_socks(self.socks_type, self.socks_host, self.socks_port, self.socks5_auth, self.socks5_user, self.socks5_pass, proxy_pivot): 776 | 777 | self.update_proxychains_conf(self.socks_type, self.cs_host, self.socks_port, self.socks5_auth, self.socks5_user, self.socks5_pass) 778 | self.socks_port_connected = True 779 | 780 | else: 781 | self.clear_context(clear_socks=True) 782 | return 783 | 784 | else: 785 | self.update_proxychains_conf(self.socks_type, self.cs_host, self.socks_port, self.socks5_auth, self.socks5_user, self.socks5_pass) 786 | self.socks_port_connected = True 787 | 788 | self.do_cs_status('') 789 | return 790 | 791 | else: 792 | self.pwarning('Specified pivot ID is not alive!') 793 | return 794 | 795 | else: 796 | self.perror('Invalid pivot ID!') 797 | return 798 | 799 | def validate_socks(self, socks_type, ip, port, socks5_auth=None, socks5_user=None, socks5_pass=None, cs_proxy_pivot=None): 800 | """checks connectivity and authentication to SOCKS servers""" 801 | 802 | # initialize socket 803 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 804 | 805 | # connect to the IP and port 806 | try: 807 | s.connect((ip, int(port))) 808 | except ConnectionRefusedError: 809 | self.perror("Error connecting to SOCKS proxy: Connection refused!") 810 | 811 | # if it's socks4 then we're all done 812 | if socks_type == 'socks4': 813 | s.close() 814 | return True 815 | 816 | # test socks5 with no auth 817 | elif socks_type == 'socks5' and not socks5_auth: 818 | 819 | # 0x05 = SOCKS5, 0x01 = client supports one auth type, 0x00 = no auth 820 | handshake = struct.pack('BBB', 0x05, 0x01, 0x00) 821 | s.sendall(handshake) 822 | 823 | try: 824 | data = s.recv(2) 825 | version, auth = struct.unpack('BB', data) 826 | except: 827 | self.perror("Error connecting to SOCKS proxy!") 828 | s.close() 829 | return False 830 | 831 | if version == 5 and auth == 0: 832 | s.close() 833 | return True 834 | 835 | # test socks5 with user/pass auth 836 | elif socks_type == 'socks5' and socks5_auth: 837 | 838 | # 0x05 = SOCKS5, 0x01 = client supports one auth type, 0x01 = user/pass auth 839 | handshake = struct.pack('BBB', 0x05, 0x01, 0x02) 840 | s.sendall(handshake) 841 | 842 | try: 843 | data = s.recv(2) 844 | version, auth = struct.unpack('BB', data) 845 | except: 846 | self.perror("Error connecting to SOCKS proxy!") 847 | s.close() 848 | return False 849 | 850 | if version == 5 and auth == 2: 851 | 852 | auth = b"\x01" + struct.pack("B", len(socks5_user)) + socks5_user.encode() + struct.pack("B", len(socks5_pass)) + socks5_pass.encode() 853 | s.sendall(auth) 854 | 855 | try: 856 | data = s.recv(2) 857 | version, status = struct.unpack('BB', data) 858 | except: 859 | self.perror("Error connecting to SOCKS5 proxy!") 860 | s.close() 861 | return False 862 | 863 | if status == 0: 864 | s.close() 865 | return True 866 | 867 | else: 868 | self.perror("Error authenticating to SOCKS5 proxy!") 869 | s.close() 870 | 871 | # reset creds on the pivot instance since auth failed 872 | if cs_proxy_pivot: 873 | cs_proxy_pivot.socks5_user = None 874 | cs_proxy_pivot.socks5_pass = None 875 | 876 | return False 877 | 878 | else: 879 | self.perror("Error connecting to SOCKS5 proxy!") 880 | s.close() 881 | return False 882 | 883 | def do_cd(self, args): 884 | """Change directory""" 885 | 886 | os.chdir(args) 887 | 888 | # configure auto complete on the cd command 889 | complete_cd = Cmd.path_complete 890 | 891 | def do_pwd(self, args): 892 | """Print working directory""" 893 | 894 | self.poutput(os.getcwd()) 895 | 896 | def do_exit(self, args): 897 | """Exit RedShell""" 898 | 899 | Logger.close_logfile() 900 | 901 | return True 902 | 903 | def validate_ttps(self, ttps): 904 | 905 | ttps_valid = [] 906 | 907 | ttps_check = ttps.split(',') 908 | 909 | for ttp in ttps_check: 910 | if re.match('^(T[0-9]{4})(\.[0-9]{3})?$', ttp): 911 | ttps_valid.append(ttp) 912 | else: 913 | self.pwarning(f"Invalid TTP specified: {ttp}. Not including in log.") 914 | 915 | validated_ttps = ', '.join(ttps_valid) 916 | 917 | return validated_ttps 918 | 919 | argparser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter) 920 | argparser.description = "Execute a command through beacon socks proxy and simultaneously log it to the teamserver." 921 | argparser.epilog = textwrap.dedent(''' 922 | 923 | example: 924 | beacon_exec -t T1550.002,T1003.002 cme smb 192.168.1.1 --local-auth -u Administrator -H C713B1D611657D0687A568122193F230 --sam 925 | ''') 926 | 927 | argparser.add_argument('-t', '--ttp', type=str, help="MITRE ATT&CK Tactic IDs. Comma delimited to specify multiple.") 928 | argparser.add_argument('command', nargs=argparse.REMAINDER, help="Command to execute through the beacon proxy and log.", completer=Cmd.shell_cmd_complete) 929 | @with_argparser(argparser) 930 | def do_beacon_exec(self, args): 931 | 932 | # check if agscript process is alive 933 | if not self.cs_process or not self.cs_process.isalive(): 934 | self.perror("Error: not connected to CS team server. Connect first and then select a pivot.") 935 | return 936 | 937 | # check if socks port is connected 938 | elif not self.socks_port_connected: 939 | self.perror("Error: socks port not connected!") 940 | return 941 | 942 | else: 943 | # make a copy of the user-specified command 944 | command_list = args.command 945 | 946 | # add proxychains to the command if user didn't include it 947 | if 'proxychains' not in command_list: 948 | id = 0 949 | if 'sudo' in command_list: 950 | id = 1 951 | for item in ['proxychains', '-f', self.proxychains_config]: 952 | command_list.insert(id, item) 953 | id += 1 954 | 955 | # convert command list into a string 956 | command = shlex.join(command_list) 957 | 958 | if '$password' in command and not self.password: 959 | self.perror("Error: $password invoked, but password is not set. Add it with command: set password ") 960 | return 961 | 962 | command = re.sub("\$password", self.password, command) 963 | 964 | # only log the command (minus sudo and proxychains) 965 | cs_log_command = re.sub("proxychains.*?conf |sudo ", '', command) 966 | cs_log_command = re.sub("\\\\", "\\\\\\\\", cs_log_command) 967 | cs_log_command = re.sub("\$", "\$", cs_log_command) # escape $ char 968 | cs_log_command = cs_log_command.replace('"', '\\"') # escape " char 969 | cs_log_command = f"[PROXY] {cs_log_command}" # append [PROXY] to logged command 970 | 971 | log_command = re.sub("proxychains.*?conf |sudo ", '', command) 972 | log_command = f"[PROXY] {log_command}" # append [PROXY] to logged command 973 | 974 | ttps = '' 975 | if args.ttp: 976 | ttps = self.validate_ttps(args.ttp) 977 | 978 | if ttps: 979 | # log command with TTPs to team server 980 | self.cs_process.sendline(f'x btask({self.cs_beacon_id}, "{cs_log_command}", "{ttps}")') 981 | self.cs_process.expect('.*aggressor.*> ') 982 | 983 | else: 984 | # log command without TTPs to team server 985 | self.cs_process.sendline(f'x btask({self.cs_beacon_id}, "{cs_log_command}")') 986 | self.cs_process.expect('.*aggressor.*> ') 987 | 988 | Logger(log_command, ip=self.cs_beacon_ip, netbios_name=self.cs_beacon_computer, user_name=self.cs_beacon_user, pid=self.cs_beacon_pid, ttps=ttps.replace(' ', '')) 989 | 990 | # run the command 991 | self.do_shell(command) 992 | 993 | argparser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter) 994 | argparser.description = "Execute a command through custom socks proxy and simultaneously log it to the local file." 995 | argparser.epilog = textwrap.dedent(''' 996 | 997 | example: 998 | proxy_exec -t T1550.002,T1003.002 cme smb 192.168.1.1 --local-auth -u Administrator -H C713B1D611657D0687A568122193F230 --sam 999 | ''') 1000 | 1001 | argparser.add_argument('-t', '--ttp', type=str, help="MITRE ATT&CK Tactic IDs. Comma delimited to specify multiple.") 1002 | argparser.add_argument('command', nargs=argparse.REMAINDER, help="Command to execute through the proxy and log.", completer=Cmd.shell_cmd_complete) 1003 | @with_argparser(argparser) 1004 | def do_proxy_exec(self, args): 1005 | 1006 | # check if socks port is connected 1007 | if not self.socks_port_connected: 1008 | self.perror("Error: socks port not connected!") 1009 | return 1010 | 1011 | # make a copy of the user-specified command 1012 | command_list = args.command 1013 | 1014 | # add proxychains to the command if user didn't include it 1015 | if 'proxychains' not in command_list: 1016 | id = 0 1017 | if 'sudo' in command_list: 1018 | id = 1 1019 | for item in ['proxychains', '-f', self.proxychains_config]: 1020 | command_list.insert(id, item) 1021 | id += 1 1022 | 1023 | # convert command list into a string 1024 | command = shlex.join(command_list) 1025 | 1026 | if '$password' in command and not self.password: 1027 | self.perror("Error: $password invoked, but password is not set. Add it with command: set password ") 1028 | return 1029 | 1030 | command = re.sub("\$password", self.password, command) 1031 | 1032 | # only log the command (minus sudo and proxychains) 1033 | log_command = re.sub("proxychains.*?conf |sudo ", '', command) 1034 | 1035 | # append [PROXY] to logged command 1036 | log_command = f"[PROXY] {log_command}" 1037 | 1038 | ttps = '' 1039 | if args.ttp: 1040 | ttps = self.validate_ttps(args.ttp) 1041 | 1042 | Logger(log_command, ip=self.context_ip, dns_name=self.context_dns_name, netbios_name=self.context_netbios_name, user_name=self.context_user_name, pid=self.context_pid, ttps=ttps.replace(' ', '')) 1043 | 1044 | # run the command 1045 | self.do_shell(command) 1046 | 1047 | argparser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter) 1048 | argparser.description = "Execute a command and log it to the local file." 1049 | argparser.epilog = textwrap.dedent(''' 1050 | 1051 | example: 1052 | exec -t T1550.002,T1003.002 cme smb 192.168.1.1 --local-auth -u Administrator -H C713B1D611657D0687A568122193F230 --sam 1053 | ''') 1054 | 1055 | argparser.add_argument('-t', '--ttp', type=str, help="MITRE ATT&CK Tactic IDs. Comma delimited to specify multiple.") 1056 | argparser.add_argument('command', nargs=argparse.REMAINDER, help="Command to execute and log.", completer=Cmd.shell_cmd_complete) 1057 | @with_argparser(argparser) 1058 | def do_exec(self, args): 1059 | 1060 | # make a copy of the user-specified command 1061 | command_list = args.command 1062 | 1063 | # convert command list into a string 1064 | command = shlex.join(command_list) 1065 | 1066 | if '$password' in command and not self.password: 1067 | self.perror("Error: $password invoked, but password is not set. Add it with command: set password ") 1068 | return 1069 | 1070 | command = re.sub("\$password", self.password, command) 1071 | 1072 | # only log the command (minus sudo) 1073 | log_command = re.sub("sudo ", '', command) 1074 | 1075 | ttps = '' 1076 | if args.ttp: 1077 | ttps = self.validate_ttps(args.ttp) 1078 | 1079 | Logger(log_command, ip=self.context_ip, dns_name=self.context_dns_name, netbios_name=self.context_netbios_name, user_name=self.context_user_name, pid=self.context_pid, ttps=ttps.replace(' ', '')) 1080 | 1081 | # run the command 1082 | self.do_shell(command) 1083 | 1084 | argparser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter) 1085 | argparser.description = "Add a manual log entry to the local file." 1086 | argparser.epilog = textwrap.dedent(''' 1087 | 1088 | example: 1089 | log -t T1608.001 Uploaded malware to LOTS site 1090 | ''') 1091 | 1092 | argparser.add_argument('-t', '--ttp', type=str, help="MITRE ATT&CK Tactic IDs. Comma delimited to specify multiple.") 1093 | argparser.add_argument('log_entry', nargs=argparse.REMAINDER, help="Entry to log.") 1094 | @with_argparser(argparser) 1095 | def do_log(self, args): 1096 | 1097 | # make a copy of the user-specified log entry 1098 | log_list = args.log_entry 1099 | 1100 | # convert command list into a string 1101 | log_entry = ' '.join(log_list) 1102 | 1103 | ttps = '' 1104 | if args.ttp: 1105 | ttps = self.validate_ttps(args.ttp) 1106 | 1107 | Logger(log_entry, ip=self.context_ip, dns_name=self.context_dns_name, netbios_name=self.context_netbios_name, user_name=self.context_user_name, pid=self.context_pid, ttps=ttps.replace(' ', '')) 1108 | 1109 | 1110 | if __name__ == '__main__': 1111 | 1112 | app = RedShell() 1113 | sys.exit(app.cmdloop()) 1114 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | cmd2>=2.0.0 2 | pexpect 3 | rich --------------------------------------------------------------------------------