forked from assafdori/bypass-mdm
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbypass-mdm-v3.sh
More file actions
executable file
·321 lines (290 loc) · 15.3 KB
/
Copy pathbypass-mdm-v3.sh
File metadata and controls
executable file
·321 lines (290 loc) · 15.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
#!/bin/bash
#
# bypass-mdm-v3.sh — DEP/MDM enrollment suppression for macOS, hardened for
# Apple Silicon + Signed System Volume (macOS 11 Big Sur .. 26 Tahoe).
#
# TWO MODES:
# • Suppress enrollment only — for a Mac that is ALREADY SET UP and just
# nags you to enroll. Blocks the enrollment fetch, clears the cached DEP
# record, and disables the enrollment daemon. Does NOT create any user.
# • Full bypass — for a Mac stuck at the Remote Management /
# Setup Assistant screen. Also creates a temp local admin + .AppleSetupDone
# so first boot skips Setup Assistant, then runs the same suppression.
#
# WHY v3 EXISTS (what was broken in v1/v2 on modern macOS):
# 1. v1/v2 wrote /etc/hosts and the ConfigurationProfiles markers to the
# *System* volume. On Apple Silicon the OS boots from a sealed, read-only
# snapshot; the live /etc/hosts and /var/db/ConfigurationProfiles actually
# live on the *Data* volume (via /etc -> /private/etc, /var -> /private/var,
# and the /private firmlink). Those writes never reached the running OS.
# THIS IS THE MAIN FIX: everything is written to the Data volume.
# 2. FileVault is on by default, so in Recovery the Data volume is LOCKED and
# not auto-mounted (v2's "Could not detect data volume"). v3 finds it by
# APFS *role* and unlocks it.
# 3. Markers alone aren't durable: macOS re-fetches the record from Apple after
# an update. v3 also blocks iprofiles.apple.com + the org's own MDM host,
# and disables the enrollment daemon via a launchd override ON THE DATA
# VOLUME (survives the System-volume reseal an update performs).
# 4. cloudconfigurationd no longer exists on macOS 26 — the work moved to
# com.apple.ManagedClient.enroll. v3 targets the current daemon names.
#
# HARD LIMIT: this does NOT remove your device from the organization's Apple
# Business/School Manager. The record is keyed to your serial and re-fetched
# whenever the Mac reaches Apple. This only SUPPRESSES enrollment locally; the
# permanent fix is the owning org releasing your serial in ABM. Never run
# `profiles renew`, and avoid Erase All Content & Settings / factory reset.
#
# RUN FROM RECOVERY (Apple Silicon: hold Power -> Options -> Utilities ->
# Terminal). Use responsibly, on devices you own.
set -o pipefail
RED='\033[1;31m'; GRN='\033[1;32m'; BLU='\033[1;34m'; YEL='\033[1;33m'
CYAN='\033[1;36m'; NC='\033[0m'
error_exit() { echo -e "${RED}ERROR: $1${NC}" >&2; exit 1; }
warn() { echo -e "${YEL}WARNING: $1${NC}" >&2; }
success() { echo -e "${GRN}✓ $1${NC}"; }
info() { echo -e "${BLU}ℹ $1${NC}"; }
step() { echo -e "${CYAN}▸ $1${NC}"; }
PB=/usr/libexec/PlistBuddy
# ---------------------------------------------------------------------------
# Input validation
# ---------------------------------------------------------------------------
validate_username() {
local u="$1"
[ -z "$u" ] && { echo "Username cannot be empty"; return 1; }
[ ${#u} -gt 31 ] && { echo "Username too long (max 31 chars)"; return 1; }
[[ "$u" =~ ^[a-zA-Z0-9_-]+$ ]] || { echo "Use only letters, numbers, _ and -"; return 1; }
[[ "$u" =~ ^[a-zA-Z_] ]] || { echo "Must start with a letter or underscore"; return 1; }
return 0
}
validate_password() {
[ -z "$1" ] && { echo "Password cannot be empty"; return 1; }
[ ${#1} -lt 4 ] && { echo "Password too short (min 4 chars)"; return 1; }
return 0
}
# ---------------------------------------------------------------------------
# Locate and mount the Data volume by APFS ROLE (robust; name-independent).
# Tries plain mount first, then FileVault unlock. Echoes the real mount point
# on stdout; all chatter goes to stderr.
# ---------------------------------------------------------------------------
resolve_data_volume() {
local id data_dev mount_pt
step "Locating the Data volume by APFS role..." >&2
# Position-independent: on the (Data)-role line, pull the diskNsM token by
# PATTERN, not column (diskutil draws "|" tree chars that shift columns).
id=$(diskutil apfs list 2>/dev/null \
| awk '/\(Data\)/{for(i=1;i<=NF;i++) if($i ~ /^disk[0-9]+s[0-9]+$/){print $i; exit}}')
# Fallback 1: a volume literally named "Data" in `diskutil list`.
if [ -z "$id" ]; then
id=$(diskutil list 2>/dev/null \
| awk '/[[:space:]]Data[[:space:]]/{for(i=1;i<=NF;i++) if($i ~ /^disk[0-9]+s[0-9]+$/) v=$i} END{print v}')
fi
# Fallback 2: ask the user, showing the disk layout.
if [ -z "$id" ] || ! diskutil info "/dev/$id" >/dev/null 2>&1; then
warn "Could not auto-detect the Data volume. Your disks:" >&2
diskutil list >&2
echo "" >&2
read -p "Type the Data volume identifier (e.g. disk3s1): " id </dev/tty
id="${id#/dev/}"
fi
[ -n "$id" ] || error_exit "No Data volume identifier provided."
data_dev="/dev/$id"
diskutil info "$data_dev" >/dev/null 2>&1 || error_exit "Not a valid disk: $data_dev"
info "Data volume device: $data_dev" >&2
_mp() { diskutil info "$data_dev" 2>/dev/null | awk -F': *' '/Mount Point/{print $2}' | sed 's/[[:space:]]*$//'; }
mount_pt=$(_mp)
if [ -z "$mount_pt" ] || [ ! -d "$mount_pt" ]; then
# Try a plain mount first (works for non-encrypted volumes).
diskutil mount "$data_dev" >&2 2>/dev/null
mount_pt=$(_mp)
fi
if [ -z "$mount_pt" ] || [ ! -d "$mount_pt" ]; then
# Still not mounted -> almost certainly FileVault-locked. Unlock it.
warn "Data volume appears FileVault-locked — unlocking." >&2
echo -e "${YEL}Enter the password of an account on this Mac (or its FileVault recovery key):${NC}" >&2
diskutil apfs unlockVolume "$data_dev" >&2 \
|| error_exit "Failed to unlock the Data volume. Re-run and enter a valid account password / recovery key."
mount_pt=$(_mp)
fi
[ -d "$mount_pt" ] || error_exit "Data volume mount point not found after mount/unlock."
# Sanity: the dslocal node must exist on this volume.
[ -d "$mount_pt/private/var/db/dslocal/nodes/Default" ] \
|| error_exit "This does not look like a macOS Data volume (no dslocal node at $mount_pt)."
success "Data volume mounted at: $mount_pt" >&2
echo "$mount_pt"
}
check_user_exists() {
dscl -f "$1/private/var/db/dslocal/nodes/Default" localhost -read "/Local/Default/Users/$2" >/dev/null 2>&1
}
find_available_uid() {
local node="$1/private/var/db/dslocal/nodes/Default" uid=501
while [ $uid -lt 600 ]; do
dscl -f "$node" localhost -search /Local/Default/Users UniqueID $uid 2>/dev/null | grep -q "UniqueID" || { echo $uid; return 0; }
uid=$((uid + 1))
done
echo 501
}
# ---------------------------------------------------------------------------
# The core enrollment block (shared by both modes). Uses the Data-volume paths
# set as globals in main(): CFG, HOSTS, LAUNCHD_DISABLED.
# ---------------------------------------------------------------------------
suppress_enrollment() {
# --- Read the org's MDM host from the activation record (before deleting it) ---
step "Inspecting the existing DEP activation record"
local mdm_host="" org=""
if [ -f "$CFG/.cloudConfigRecordFound" ]; then
mdm_host=$(plutil -convert xml1 -o - "$CFG/.cloudConfigRecordFound" 2>/dev/null \
| grep -ioE 'https?://[a-z0-9._-]+' | sed -E 's#https?://##' \
| sort -u | grep -viE '(^|\.)apple\.com$' | head -1)
org=$(plutil -convert xml1 -o - "$CFG/.cloudConfigRecordFound" 2>/dev/null \
| grep -iA1 OrganizationName | tail -1 | sed -E 's/.*<string>(.*)<\/string>.*/\1/')
[ -n "$org" ] && info "This device is assigned in Apple Business Manager to: $org"
[ -n "$mdm_host" ] && info "Org MDM server host: $mdm_host (will also be blocked)"
else
info "No activation record currently present."
fi
echo ""
# --- Block the DEP / enrollment domains on the DATA volume's hosts file ---
step "Blocking DEP enrollment domains (Data-volume hosts file)"
[ -f "$HOSTS" ] || { mkdir -p "$(dirname "$HOSTS")"; touch "$HOSTS"; }
# iprofiles.apple.com = the device-side activation-record fetch (THE essential one).
# device/mdm-enrollment = server-side DEP API (harmless to include).
# acmdm = Apple cert/MDM endpoint. We deliberately DO NOT block:
# gdmf.apple.com (breaks Software Update) or albert.apple.com (breaks iMessage/FaceTime).
local block_domains=(iprofiles.apple.com deviceenrollment.apple.com mdmenrollment.apple.com acmdm.apple.com)
[ -n "$mdm_host" ] && block_domains+=("$mdm_host")
grep -q "Added by bypass-mdm-v3" "$HOSTS" 2>/dev/null || {
echo "" >>"$HOSTS"
echo "# Added by bypass-mdm-v3 — DEP enrollment block" >>"$HOSTS"
}
local d
for d in "${block_domains[@]}"; do
grep -qiE "[[:space:]]$d(\$|[[:space:]])" "$HOSTS" 2>/dev/null && { info "$d already blocked"; continue; }
printf '0.0.0.0 %s\n:: %s\n' "$d" "$d" >>"$HOSTS"
success "blocked $d"
done
echo ""
# --- Reset the DEP markers (clears the cached record + current nag state) ---
step "Resetting DEP markers"
mkdir -p "$CFG"
rm -f "$CFG/.cloudConfigHasActivationRecord" "$CFG/.cloudConfigRecordFound" \
"$CFG/.cloudConfigTimerCheck" "$CFG/com.apple.mdm.depnag.plist" \
"$CFG/com.apple.mdm.prelogin.plist" 2>/dev/null
touch "$CFG/.cloudConfigRecordNotFound" "$CFG/.cloudConfigProfileInstalled"
success "Cached record removed; markers set to record-not-found + profile-installed"
echo ""
# --- Durability: disable the enrollment daemon via a launchd override ---
# This override lives on the DATA volume, so it survives the System-volume
# reseal a macOS update performs. Targets the CURRENT (macOS 26) daemon;
# cloudconfigurationd no longer exists.
step "Disabling the enrollment daemon (durable override on Data volume)"
mkdir -p "$(dirname "$LAUNCHD_DISABLED")"
[ -f "$LAUNCHD_DISABLED" ] || printf '<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0"><dict/></plist>\n' >"$LAUNCHD_DISABLED"
local label
for label in com.apple.ManagedClient.enroll com.apple.mdmclient.daemon.runatboot; do
$PB -c "Add :$label bool true" "$LAUNCHD_DISABLED" 2>/dev/null \
|| $PB -c "Set :$label true" "$LAUNCHD_DISABLED" 2>/dev/null
done
success "Enrollment daemon disabled via $LAUNCHD_DISABLED"
echo ""
}
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
data_mount=$(resolve_data_volume) || exit 1
# Derived Data-volume paths (the running OS reads ALL of these from here).
DS_NODE="$data_mount/private/var/db/dslocal/nodes/Default"
HOSTS="$data_mount/private/etc/hosts"
CFG="$data_mount/private/var/db/ConfigurationProfiles/Settings"
SETUPDONE="$data_mount/private/var/db/.AppleSetupDone"
LAUNCHD_DISABLED="$data_mount/private/var/db/com.apple.xpc.launchd/disabled.plist"
echo ""
echo -e "${CYAN}╔════════════════════════════════════════════════╗${NC}"
echo -e "${CYAN}║ Bypass MDM v3 — Apple Silicon / SSV aware ║${NC}"
echo -e "${CYAN}╚════════════════════════════════════════════════╝${NC}"
success "Data volume: $data_mount"
echo ""
PS3='Please enter your choice: '
options=(
"Suppress enrollment only (Mac already set up)"
"Full bypass (create admin + suppress — for a stuck setup)"
"Verify current state"
"Reboot & Exit"
)
select opt in "${options[@]}"; do
case $opt in
"Suppress enrollment only (Mac already set up)")
echo ""
info "Suppress-only mode: no user will be created; your accounts/data are untouched."
echo ""
suppress_enrollment
echo -e "${GRN}╔════════════════════════════════════════════════╗${NC}"
echo -e "${GRN}║ Enrollment Suppressed — reboot to apply ║${NC}"
echo -e "${GRN}╚════════════════════════════════════════════════╝${NC}"
echo ""
echo -e "${CYAN}Next:${NC} reboot. The enroll nag should be gone."
echo -e "${CYAN}After a future macOS update:${NC} re-run this (updates reset hosts + markers)."
echo -e "${YEL}Never run 'profiles renew' or Erase All Content & Settings — both re-arm DEP.${NC}"
echo ""
break
;;
"Full bypass (create admin + suppress — for a stuck setup)")
echo ""
step "Creating a temporary local admin account"
read -p "Full name (default 'Apple'): " realName; realName="${realName:=Apple}"
while true; do
read -p "Username (default 'Apple'): " username; username="${username:=Apple}"
if msg=$(validate_username "$username"); then
if check_user_exists "$data_mount" "$username"; then
warn "User '$username' already exists — choose another."; continue
fi
break
else warn "$msg"; fi
done
while true; do
read -p "Password (default '1234'): " passw; passw="${passw:=1234}"
if msg=$(validate_password "$passw"); then break; else warn "$msg"; fi
done
uid=$(find_available_uid "$data_mount")
info "Using UID $uid"
dscl -f "$DS_NODE" localhost -create "/Local/Default/Users/$username" || error_exit "Failed to create user"
dscl -f "$DS_NODE" localhost -create "/Local/Default/Users/$username" UserShell "/bin/zsh"
dscl -f "$DS_NODE" localhost -create "/Local/Default/Users/$username" RealName "$realName"
dscl -f "$DS_NODE" localhost -create "/Local/Default/Users/$username" UniqueID "$uid"
dscl -f "$DS_NODE" localhost -create "/Local/Default/Users/$username" PrimaryGroupID "20"
dscl -f "$DS_NODE" localhost -create "/Local/Default/Users/$username" NFSHomeDirectory "/Users/$username"
dscl -f "$DS_NODE" localhost -passwd "/Local/Default/Users/$username" "$passw" || error_exit "Failed to set password"
dscl -f "$DS_NODE" localhost -append "/Local/Default/Groups/admin" GroupMembership "$username" || error_exit "Failed to grant admin"
mkdir -p "$data_mount/Users/$username" && success "Admin '$username' created"
# Skip Setup Assistant on first boot (load-bearing on Sonoma+: only works
# together with the local user just created).
touch "$SETUPDONE" && success "Setup Assistant will be skipped (.AppleSetupDone)"
echo ""
suppress_enrollment
echo -e "${GRN}╔════════════════════════════════════════════════╗${NC}"
echo -e "${GRN}║ MDM Bypass Applied Successfully ║${NC}"
echo -e "${GRN}╚════════════════════════════════════════════════╝${NC}"
echo ""
echo -e "${CYAN}Login after reboot:${NC} ${YEL}$username${NC} / ${YEL}$passw${NC}"
echo -e "${CYAN}After a future macOS update:${NC} re-run this (updates reset hosts + markers)."
echo -e "${YEL}Never run 'profiles renew' or Erase All Content & Settings — both re-arm DEP.${NC}"
echo ""
break
;;
"Verify current state")
echo ""
step "Markers in $CFG"
ls -la "$CFG" 2>/dev/null || warn "Settings dir not found"
echo ""
step "DEP block lines in $HOSTS"
grep -iE 'iprofiles|enrollment|mdm|acmdm' "$HOSTS" 2>/dev/null || warn "No block lines present"
echo ""
step "launchd disable override"
[ -f "$LAUNCHD_DISABLED" ] && $PB -c "Print" "$LAUNCHD_DISABLED" 2>/dev/null || info "none"
echo ""
;;
"Reboot & Exit")
info "Rebooting..."; reboot; break
;;
*) echo -e "${RED}Invalid option $REPLY${NC}" ;;
esac
done