├── .gitignore ├── LICENSE ├── README.md ├── poisonapple ├── __init__.py ├── auxiliary │ ├── login-items-add.sh │ ├── login-items-rm.sh │ ├── poisonapple.plist │ └── poisonapple.sh ├── cli.py ├── techniques.py └── util.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.egg-info/ 3 | dist/ 4 | build/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Cyborg Security 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PoisonApple 2 | 3 | 4 | 5 | This is a command-line tool to perform various persistence mechanism techniques on macOS. This tool was designed to be used by threat hunters for cyber threat emulation purposes. 6 | 7 | ## Install 8 | 9 | Do it up: 10 | ``` 11 | $ pip3 install poisonapple --user 12 | ``` 13 | 14 | Note: PoisonApple was written & tested using Python 3.9, it should work using Python 3.6+ 15 | 16 | ## Important Notes! 17 | 18 | * PoisonApple will make modifications to your macOS system, it's advised to only use PoisonApple on a virtual machine. Although any persistence mechanism technique added using this tool can also be easily removed (-r), **please use with caution**! 19 | * Be advised: This tool will likely cause common AV / EDR / other macOS security products to generate alerts. 20 | * To understand how any of these techniques work in-depth please see [The Art of Mac Malware, Volume 1: Analysis - Chapter 0x2: Persistence](https://taomm.org/PDFs/vol1/CH%200x02%20Persistence.pdf) by Patrick Wardle of Objective-See. It's a fantastic resource. 21 | 22 | ## Usage 23 | 24 | See PoisonApple switch options (--help): 25 | ``` 26 | $ poisonapple --help 27 | usage: poisonapple [-h] [-l] [-t TECHNIQUE] [-n NAME] [-c COMMAND] [-r] 28 | 29 | Command-line tool to perform various persistence mechanism techniques on macOS. 30 | 31 | optional arguments: 32 | -h, --help show this help message and exit 33 | -l, --list list available persistence mechanism techniques 34 | -t TECHNIQUE, --technique TECHNIQUE 35 | persistence mechanism technique to use 36 | -n NAME, --name NAME name for the file or label used for persistence 37 | -c COMMAND, --command COMMAND 38 | command(s) to execute for persistence 39 | -r, --remove remove persistence mechanism 40 | ``` 41 | 42 | List of available techniques: 43 | ``` 44 | $ poisonapple --list 45 | , _______ __ 46 | .-.:|.-. | _ .-----|__|-----.-----.-----. 47 | .' '. |. | | | | |__ --| | | | | 48 | '-."~". .-' |. ____|_____|__|_____|_____|__|__| 49 | } ` } { |: | _______ __ 50 | } } } { |::.| | _ .-----.-----| |-----. 51 | } ` } { `---' |. | | | | | | | -__| 52 | .-'"~" '-. |. _ | __| __|__|_____| 53 | '. .' |: | |__| |__| 54 | '-_.._-' |::.|:. | 55 | `--- ---' v0.2.3 56 | 57 | +--------------------+ 58 | | AtJob | 59 | +--------------------+ 60 | | Bashrc | 61 | +--------------------+ 62 | | Cron | 63 | +--------------------+ 64 | | CronRoot | 65 | +--------------------+ 66 | | Emond | 67 | +--------------------+ 68 | | Iterm2 | 69 | +--------------------+ 70 | | LaunchAgent | 71 | +--------------------+ 72 | | LaunchAgentUser | 73 | +--------------------+ 74 | | LaunchDaemon | 75 | +--------------------+ 76 | | LoginHook | 77 | +--------------------+ 78 | | LoginHookUser | 79 | +--------------------+ 80 | | LoginItem | 81 | +--------------------+ 82 | | LogoutHook | 83 | +--------------------+ 84 | | LogoutHookUser | 85 | +--------------------+ 86 | | Periodic | 87 | +--------------------+ 88 | | Reopen | 89 | +--------------------+ 90 | | Zshrc | 91 | +--------------------+ 92 | ``` 93 | 94 | Apply a persistence mechanism: 95 | ``` 96 | $ poisonapple -t LaunchAgentUser -n testing 97 | , _______ __ 98 | .-.:|.-. | _ .-----|__|-----.-----.-----. 99 | .' '. |. | | | | |__ --| | | | | 100 | '-."~". .-' |. ____|_____|__|_____|_____|__|__| 101 | } ` } { |: | _______ __ 102 | } } } { |::.| | _ .-----.-----| |-----. 103 | } ` } { `---' |. | | | | | | | -__| 104 | .-'"~" '-. |. _ | __| __|__|_____| 105 | '. .' |: | |__| |__| 106 | '-_.._-' |::.|:. | 107 | `--- ---' v0.2.3 108 | 109 | [+] Success! The persistence mechanism action was successful: LaunchAgentUser 110 | ``` 111 | 112 | If no command is specified (-c) a default trigger command will be used which writes to a file on the Desktop every time the persistence mechanism is triggered: 113 | ``` 114 | $ cat ~/Desktop/PoisonApple-LaunchAgentUser 115 | Triggered @ Tue Mar 23 17:46:02 CDT 2021 116 | Triggered @ Tue Mar 23 17:46:13 CDT 2021 117 | Triggered @ Tue Mar 23 17:46:23 CDT 2021 118 | Triggered @ Tue Mar 23 17:46:33 CDT 2021 119 | Triggered @ Tue Mar 23 17:46:43 CDT 2021 120 | Triggered @ Tue Mar 23 17:46:53 CDT 2021 121 | Triggered @ Tue Mar 23 17:47:03 CDT 2021 122 | Triggered @ Tue Mar 23 17:47:13 CDT 2021 123 | Triggered @ Tue Mar 23 17:48:05 CDT 2021 124 | Triggered @ Tue Mar 23 17:48:15 CDT 2021 125 | ``` 126 | 127 | Remove a persistence mechanism: 128 | ``` 129 | $ poisonapple -t LaunchAgentUser -n testing -r 130 | ... 131 | ``` 132 | 133 | Use a custom command: 134 | ``` 135 | $ poisonapple -t LaunchAgentUser -n foo -c "echo foo >> /Users/user/Desktop/foo" 136 | ... 137 | ``` -------------------------------------------------------------------------------- /poisonapple/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyborgSecurity/PoisonApple/9282e61fc37a241cda144a901a57776498a1fde3/poisonapple/__init__.py -------------------------------------------------------------------------------- /poisonapple/auxiliary/login-items-add.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # 4 | # Source: https://github.com/andrewp-as-is/mac-login-items 5 | # 6 | 7 | { set +x; } 2>/dev/null 8 | 9 | usage() { 10 | echo "usage: $(basename $0) path ..." 1>&2 11 | [[ $1 == "-h" ]] || [[ $1 == "--help" ]]; exit 12 | } 13 | 14 | [[ $1 == "-h" ]] || [[ $1 == "--help" ]] && usage "$@" 15 | 16 | hidden=false 17 | 18 | while [[ $# != 0 ]]; do 19 | path="$1" 20 | ! [ -d "$1" ] && { 21 | [ -d /Applications/"$1" ] && path=/Applications/"$1" 22 | [ -d /Applications/"$1".app ] && path=/Applications/"$1".app 23 | [ -d ~/Applications/"$1" ] && path=~/Applications/"$1" 24 | [ -d ~/Applications/"$1".app ] && path=~/Applications/"$1".app 25 | } 26 | ! [ -e "$path" ] && echo "ERROR: $path NOT EXISTS" 1>&2 && exit 1 27 | osascript </dev/null 8 | 9 | usage() { 10 | echo "usage: $(basename $0)" 1>&2 11 | [[ $1 == "-h" ]] || [[ $1 == "--help" ]]; exit 12 | } 13 | 14 | [[ $1 == "-h" ]] || [[ $1 == "--help" ]] && usage "$@" 15 | 16 | [[ $# == 0 ]] && usage 17 | 18 | while [[ $# != 0 ]]; do 19 | osascript < 2 | 3 | 4 | 5 | 6 | name 7 | poisonapple rule 8 | enabled 9 | 10 | eventTypes 11 | 12 | startup 13 | 14 | actions 15 | 16 | 17 | command 18 | {0} 19 | user 20 | root 21 | arguments 22 | 23 | Emond 24 | 25 | type 26 | RunCommand 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /poisonapple/auxiliary/poisonapple.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo "Triggered @ $(date) " >> /Users/$(stat -f "%Su" /dev/console)/Desktop/PoisonApple-$1 -------------------------------------------------------------------------------- /poisonapple/cli.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | from poisonapple.techniques import Technique 4 | from poisonapple.util import get_trigger_command, print_status 5 | 6 | BANNER = """\ 7 | , _______ __ 8 | .-.:|.-. | _ .-----|__|-----.-----.-----. 9 | .' '. |. | | | | |__ --| | | | | 10 | '-."~". .-' |. ____|_____|__|_____|_____|__|__| 11 | } ` } { |: | _______ __ 12 | } } } { |::.| | _ .-----.-----| |-----. 13 | } ` } { `---' |. | | | | | | | -__| 14 | .-'"~" '-. |. _ | __| __|__|_____| 15 | '. .' |: | |__| |__| 16 | '-_.._-' |::.|:. | 17 | `--- ---' v0.2.3 18 | """ 19 | 20 | 21 | def get_parser(): 22 | parser = argparse.ArgumentParser( 23 | description="Command-line tool to perform various persistence mechanism techniques on macOS." 24 | ) 25 | parser.add_argument( 26 | "-l", 27 | "--list", 28 | action="store_true", 29 | help="list available persistence mechanism techniques", 30 | ) 31 | parser.add_argument( 32 | "-t", 33 | "--technique", 34 | default=str(), 35 | type=str, 36 | help="persistence mechanism technique to use", 37 | ) 38 | parser.add_argument( 39 | "-n", 40 | "--name", 41 | default=str(), 42 | type=str, 43 | help="name for the file or label used for persistence", 44 | ) 45 | parser.add_argument( 46 | "-c", 47 | "--command", 48 | default=str(), 49 | type=str, 50 | help="command(s) to execute for persistence", 51 | ) 52 | parser.add_argument( 53 | "-r", 54 | "--remove", 55 | action="store_true", 56 | help="remove persistence mechanism" 57 | ) 58 | return parser 59 | 60 | 61 | def main(): 62 | parser = get_parser() 63 | args = vars(parser.parse_args()) 64 | technique_list = Technique.__subclasses__() 65 | print(BANNER) 66 | 67 | if args["list"]: 68 | seperator = f'+{"-"*20}+' 69 | for technique in technique_list: 70 | print(f"{seperator}\n| {technique.__name__:<18} |") 71 | print(seperator) 72 | return 73 | 74 | name = args["name"] 75 | remove = args["remove"] 76 | command = args["command"] 77 | technique = args["technique"] 78 | 79 | if not (name and technique): 80 | print_status("missing_option", stop=True) 81 | 82 | for technique_class in technique_list: 83 | technique_name = technique_class.__name__ 84 | if technique_name.lower() == technique.strip().lower(): 85 | if not command: 86 | command = get_trigger_command(technique_name) 87 | t = technique_class(name, command) 88 | if remove: 89 | t.remove() 90 | else: 91 | t.run() 92 | 93 | 94 | if __name__ == "__main__": 95 | main() 96 | -------------------------------------------------------------------------------- /poisonapple/techniques.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from poisonapple.util import ( 4 | print_status, 5 | get_full_path, 6 | create_cron_job, 7 | remove_line, 8 | create_app, 9 | get_plist, 10 | write_plist, 11 | plist_launch_write, 12 | plist_launch_uninstall, 13 | ) 14 | 15 | 16 | class Technique: 17 | def __init__(self, technique, name, command, root_required=False): 18 | self.technique = technique 19 | self.name = name 20 | self.command = command 21 | self.root_required = root_required 22 | self.success = False 23 | self.error_message = str() 24 | 25 | def display_result(self): 26 | if self.success: 27 | print_status("success", text=self.technique) 28 | else: 29 | print_status("failure", text=self.technique) 30 | print_status("python_error", text=self.error_message) 31 | 32 | @staticmethod 33 | def execute(func): 34 | def wrapper(self): 35 | if self.root_required and not os.geteuid() == 0: 36 | self.error_message = "Root access is required for this technique." 37 | else: 38 | try: 39 | func(self) 40 | self.success = True 41 | except Exception as e: 42 | self.error_message = str(e) 43 | self.display_result() 44 | return wrapper 45 | 46 | 47 | class AtJob(Technique): 48 | def __init__(self, name, command): 49 | super().__init__("AtJob", name, command, root_required=True) 50 | self.atrun_plist = "/System/Library/LaunchDaemons/com.apple.atrun.plist" 51 | 52 | @Technique.execute 53 | def run(self): 54 | os.system(f"launchctl unload -F {self.atrun_plist}") 55 | os.system(f"launchctl load -w {self.atrun_plist}") 56 | os.system(f"{self.command} | at +1 minute") 57 | 58 | @Technique.execute 59 | def remove(self): 60 | os.system(f"launchctl unload -F {self.atrun_plist}") 61 | 62 | 63 | class Bashrc(Technique): 64 | def __init__(self, name, command): 65 | super().__init__("Bashrc", name, command, root_required=False) 66 | self.bashrc_path = f"/Users/{os.getlogin()}/.bashrc" 67 | 68 | @Technique.execute 69 | def run(self): 70 | os.system(f'echo "{self.command} # {self.name}" >> {self.bashrc_path}') 71 | 72 | @Technique.execute 73 | def remove(self): 74 | remove_line(f"# {self.name}", self.bashrc_path) 75 | 76 | 77 | class Cron(Technique): 78 | def __init__(self, name, command): 79 | super().__init__("Cron", name, command, root_required=False) 80 | 81 | @Technique.execute 82 | def run(self): 83 | create_cron_job(os.getlogin(), self.command, self.name) 84 | 85 | @Technique.execute 86 | def remove(self): 87 | remove_line( 88 | f"# {self.name}", os.path.join("/usr/lib/cron/tabs/", os.getlogin()) 89 | ) 90 | 91 | 92 | class CronRoot(Technique): 93 | def __init__(self, name, command): 94 | super().__init__("CronRoot", name, command, root_required=True) 95 | 96 | @Technique.execute 97 | def run(self): 98 | create_cron_job("root", self.command, self.name) 99 | 100 | @Technique.execute 101 | def remove(self): 102 | remove_line(f"# {self.name}", "/usr/lib/cron/tabs/root") 103 | 104 | 105 | class Emond(Technique): 106 | def __init__(self, name, command): 107 | super().__init__("Emond", name, command, root_required=True) 108 | 109 | @Technique.execute 110 | def run(self): 111 | plist_path = get_full_path("auxiliary/poisonapple.plist") 112 | trigger_path = get_full_path("auxiliary/poisonapple.sh") 113 | with open(plist_path) as f: 114 | plist_data = f.read() 115 | with open(f"/etc/emond.d/rules/{self.name}.plist", "w") as f: 116 | f.write(plist_data.format(trigger_path)) 117 | os.system(f"touch /private/var/db/emondClients/{self.name}") 118 | 119 | @Technique.execute 120 | def remove(self): 121 | os.remove(f"/etc/emond.d/rules/{self.name}.plist") 122 | os.remove(f"/private/var/db/emondClients/{self.name}") 123 | 124 | 125 | class Iterm2(Technique): 126 | def __init__(self, name, command): 127 | super().__init__("Iterm2", name, command, root_required=False) 128 | self.iterm2_plist = ( 129 | f"/Users/{os.getlogin()}/Library/Preferences/com.googlecode.iterm2.plist" 130 | ) 131 | 132 | @Technique.execute 133 | def run(self): 134 | plist_data = get_plist(self.iterm2_plist) 135 | plist_data["New Bookmarks"][0]["Initial Text"] = f"{self.command} && clear" 136 | write_plist(self.iterm2_plist, plist_data) 137 | 138 | @Technique.execute 139 | def remove(self): 140 | plist_data = get_plist(self.iterm2_plist) 141 | plist_data["New Bookmarks"][0].pop("Initial Text") 142 | write_plist(self.iterm2_plist, plist_data) 143 | 144 | 145 | class LaunchAgent(Technique): 146 | def __init__(self, name, command): 147 | super().__init__("LaunchAgent", name, command, root_required=True) 148 | 149 | @Technique.execute 150 | def run(self): 151 | plist_launch_write(self.name, self.command, 2) 152 | 153 | @Technique.execute 154 | def remove(self): 155 | plist_launch_uninstall(self.name, 2) 156 | 157 | 158 | class LaunchAgentUser(Technique): 159 | def __init__(self, name, command): 160 | super().__init__("LaunchAgentUser", name, command, root_required=False) 161 | 162 | @Technique.execute 163 | def run(self): 164 | try: 165 | os.mkdir(os.path.join(f"/Users/{os.getlogin()}", "Library/LaunchAgents")) 166 | except FileExistsError: 167 | pass 168 | plist_launch_write(self.name, self.command, 1) 169 | 170 | @Technique.execute 171 | def remove(self): 172 | plist_launch_uninstall(self.name, 1) 173 | 174 | 175 | class LaunchDaemon(Technique): 176 | def __init__(self, name, command): 177 | super().__init__("LaunchDaemon", name, command, root_required=True) 178 | 179 | @Technique.execute 180 | def run(self): 181 | plist_launch_write(self.name, self.command, 3) 182 | 183 | @Technique.execute 184 | def remove(self): 185 | plist_launch_uninstall(self.name, 3) 186 | 187 | 188 | class LoginHook(Technique): 189 | def __init__(self, name, command): 190 | super().__init__("LoginHook", name, command, root_required=True) 191 | 192 | @Technique.execute 193 | def run(self): 194 | os.system(f'defaults write com.apple.loginwindow LoginHook "{self.command}"') 195 | 196 | @Technique.execute 197 | def remove(self): 198 | os.system("defaults delete com.apple.loginwindow LoginHook") 199 | 200 | 201 | class LoginHookUser(Technique): 202 | def __init__(self, name, command): 203 | super().__init__("LoginHookUser", name, command, root_required=False) 204 | 205 | @Technique.execute 206 | def run(self): 207 | os.system(f'defaults write com.apple.loginwindow LoginHook "{self.command}"') 208 | 209 | @Technique.execute 210 | def remove(self): 211 | os.system("defaults delete com.apple.loginwindow LoginHook") 212 | 213 | 214 | class LoginItem(Technique): 215 | def __init__(self, name, command): 216 | super().__init__("LoginItem", name, command, root_required=False) 217 | 218 | @Technique.execute 219 | def run(self): 220 | app_path = create_app(self.name, self.command, "LoginItem") 221 | login_items_add_path = get_full_path("auxiliary/login-items-add.sh") 222 | os.system(f"{login_items_add_path} {app_path}") 223 | 224 | @Technique.execute 225 | def remove(self): 226 | login_items_rm_path = get_full_path("auxiliary/login-items-rm.sh") 227 | os.system(f"{login_items_rm_path} {self.name}") 228 | 229 | 230 | class LogoutHook(Technique): 231 | def __init__(self, name, command): 232 | super().__init__("LogoutHook", name, command, root_required=True) 233 | 234 | @Technique.execute 235 | def run(self): 236 | os.system(f'defaults write com.apple.loginwindow LogoutHook "{self.command}"') 237 | 238 | @Technique.execute 239 | def remove(self): 240 | os.system("defaults delete com.apple.loginwindow LogoutHook") 241 | 242 | 243 | class LogoutHookUser(Technique): 244 | def __init__(self, name, command): 245 | super().__init__("LogoutHookUser", name, command, root_required=False) 246 | 247 | @Technique.execute 248 | def run(self): 249 | os.system(f'defaults write com.apple.loginwindow LogoutHook "{self.command}"') 250 | 251 | @Technique.execute 252 | def remove(self): 253 | os.system("defaults delete com.apple.loginwindow LogoutHook") 254 | 255 | 256 | class Periodic(Technique): 257 | def __init__(self, name, command): 258 | super().__init__("Periodic", name, command, root_required=True) 259 | 260 | @Technique.execute 261 | def run(self): 262 | periodic_path = f"/etc/periodic/daily/666.{self.name}" 263 | with open(periodic_path, "w") as f: 264 | f.write(f"#!/usr/bin/env bash\n{self.command}") 265 | os.chmod(periodic_path, 0o755) 266 | 267 | @Technique.execute 268 | def remove(self): 269 | os.remove(f"/etc/periodic/daily/666.{self.name}") 270 | 271 | 272 | class Reopen(Technique): 273 | def __init__(self, name, command): 274 | super().__init__("Reopen", name, command, root_required=False) 275 | 276 | def get_plist_paths(self): 277 | reopen_path = f"/Users/{os.getlogin()}/Library/Preferences/ByHost/" 278 | plist_paths = [ 279 | os.path.join(reopen_path, f) 280 | for f in os.listdir(reopen_path) 281 | if f.startswith("com.apple.loginwindow") 282 | ] 283 | if not plist_paths: 284 | raise Exception("Reopen plist file not found!") 285 | return plist_paths 286 | 287 | @Technique.execute 288 | def run(self): 289 | print_status( 290 | "warning", "This technique is finicky and might not work as expected, YMMV." 291 | ) 292 | app_path = create_app(self.name, self.command, "Reopen") 293 | for path in self.get_plist_paths(): 294 | plist_data = get_plist(path) 295 | plist_data["TALAppsToRelaunchAtLogin"].append( 296 | { 297 | "Hide": False, 298 | "BundleID": self.name, 299 | "Path": app_path, 300 | "BackgroundState": 2, 301 | } 302 | ) 303 | write_plist(path, plist_data) 304 | 305 | @Technique.execute 306 | def remove(self): 307 | for path in self.get_plist_paths(): 308 | plist_data = get_plist(path) 309 | for reopen_dict in plist_data["TALAppsToRelaunchAtLogin"]: 310 | if reopen_dict["BundleID"] == self.name: 311 | plist_data["TALAppsToRelaunchAtLogin"].remove(reopen_dict) 312 | write_plist(path, plist_data) 313 | 314 | 315 | class Zshrc(Technique): 316 | def __init__(self, name, command): 317 | super().__init__("Zshrc", name, command, root_required=False) 318 | self.zshrc_path = f"/Users/{os.getlogin()}/.zshrc" 319 | 320 | @Technique.execute 321 | def run(self): 322 | os.system(f'echo "{self.command} # {self.name}" >> {self.zshrc_path}') 323 | 324 | @Technique.execute 325 | def remove(self): 326 | remove_line(f"# {self.name}", self.zshrc_path) 327 | -------------------------------------------------------------------------------- /poisonapple/util.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import crayons 4 | import launchd 5 | import plistlib 6 | 7 | from crontab import CronTab 8 | 9 | STATUS_MESSAGES = { 10 | "failure": "[!] Failure! The persistence mechansim action failed", 11 | "python_error": "[-] Error! Traceback", 12 | "missing_command": "[-] Error! Need to specifiy either --command OR --popup", 13 | "missing_option": "[-] Error! Missing required option, see --help for more info...", 14 | "success": "[+] Success! The persistence mechanism action was successful", 15 | "warning": "[~] Warning", 16 | } 17 | 18 | STATUS_COLORS = { 19 | "[+]": crayons.green, 20 | "[-]": crayons.red, 21 | "[!]": crayons.magenta, 22 | "[~]": crayons.yellow, 23 | } 24 | 25 | 26 | def print_status(name, text=str(), stop=False): 27 | message = STATUS_MESSAGES[name] 28 | if text: 29 | message += f": {text}" 30 | for log_type, color in STATUS_COLORS.items(): 31 | if message.startswith(log_type): 32 | print(color(message)) 33 | if stop: 34 | sys.exit(1) 35 | 36 | 37 | def get_full_path(relative_path): 38 | return os.path.join(os.path.abspath(os.path.dirname(__file__)), relative_path) 39 | 40 | 41 | def get_plist(file_path): 42 | with open(file_path, "rb") as f: 43 | return plistlib.load(f) 44 | 45 | 46 | def write_plist(file_path, data): 47 | with open(file_path, "wb") as f: 48 | plistlib.dump(data, f) 49 | 50 | 51 | def plist_launch_write(label, program_arguments, scope): 52 | plist = dict( 53 | Label=label, 54 | ProgramArguments=program_arguments.split(), 55 | RunAtLoad=True, 56 | KeepAlive=True, 57 | ) 58 | job = launchd.LaunchdJob(label) 59 | fname = launchd.plist.write(label, plist, scope) 60 | launchd.load(fname) 61 | 62 | 63 | def plist_launch_uninstall(label, scope): 64 | fname = launchd.plist.discover_filename(label, scope) 65 | if not fname: 66 | raise Exception(f"{label}.plist not found.") 67 | launchd.unload(fname) 68 | os.unlink(fname) 69 | 70 | 71 | def get_trigger_command(technique_name): 72 | return f'{get_full_path("auxiliary/poisonapple.sh")} {technique_name}' 73 | 74 | 75 | def create_cron_job(user, command, comment): 76 | cron = CronTab(user=user) 77 | job = cron.new(command=command, comment=comment) 78 | job.minute.every(1) 79 | cron.write() 80 | 81 | 82 | def remove_line(string, file_path): 83 | lines = list() 84 | with open(file_path) as f: 85 | for line in f.readlines(): 86 | if string in line: 87 | continue 88 | lines.append(line) 89 | with open(file_path, "w") as f: 90 | for line in lines: 91 | f.write(line) 92 | 93 | 94 | def create_app(name, command, technique_name): 95 | auxiliary_path = get_full_path("auxiliary/") 96 | app_path = os.path.join(auxiliary_path, f"{name}.app") 97 | app_path_full = os.path.join(app_path, "Contents/MacOS") 98 | try: 99 | os.makedirs(app_path_full) 100 | except FileExistsError: 101 | pass 102 | app_path_script = os.path.join(app_path_full, name) 103 | with open(app_path_script, "w") as f: 104 | f.write(f"#!/usr/bin/env bash\n{command}") 105 | os.chmod(app_path_script, 0o755) 106 | return app_path 107 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | from setuptools import setup 5 | 6 | directory = os.path.abspath(os.path.dirname(__file__)) 7 | with open(os.path.join(directory, "README.md"), encoding="utf-8") as f: 8 | long_description = f.read() 9 | 10 | setup( 11 | name="poisonapple", 12 | packages=[ 13 | "poisonapple", 14 | "poisonapple.auxiliary", 15 | ], 16 | package_data={"poisonapple.auxiliary": ["*.plist", "*.sh"]}, 17 | version="0.2.3", 18 | description="Command-line tool to perform various persistence mechanism techniques on macOS.", 19 | long_description=long_description, 20 | long_description_content_type="text/markdown", 21 | license="MIT", 22 | url="https://github.com/CyborgSecurity/PoisonApple", 23 | author="Austin Jackson", 24 | author_email="vesche@protonmail.com", 25 | entry_points={ 26 | "console_scripts": [ 27 | "poisonapple = poisonapple.cli:main", 28 | ] 29 | }, 30 | install_requires=[ 31 | "crayons", 32 | "launchd", 33 | "python-crontab", 34 | ], 35 | classifiers=[ 36 | "Development Status :: 4 - Beta", 37 | "Environment :: Console", 38 | "Intended Audience :: Information Technology", 39 | "License :: OSI Approved :: MIT License", 40 | "Programming Language :: Python :: 3", 41 | "Programming Language :: Python :: 3.9", 42 | "Topic :: Security", 43 | ], 44 | ) 45 | --------------------------------------------------------------------------------