364 lines
12 KiB
Python
364 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
"""Setup 2FA on GSuite ifae.charity using 2fa.live for TOTP + Selenium"""
|
|
import time,json,os,re,traceback
|
|
from datetime import datetime
|
|
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
|
|
|
|
EMAIL="weval@ifae.charity"
|
|
PASSWORD="yK6Rc98b4wJHz2"
|
|
SDIR="/opt/wevads/logs/selenium/screenshots"
|
|
os.makedirs(SDIR,exist_ok=True)
|
|
RESULTS={}
|
|
|
|
def log(m): print(f"[{datetime.now().strftime('%H:%M:%S')}] {m}",flush=True)
|
|
def ss(d,n): p=f"{SDIR}/gs_{n}_{int(time.time())}.png"; d.save_screenshot(p); log(f"SS: {p}"); return p
|
|
|
|
def get_driver():
|
|
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("--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")
|
|
d=webdriver.Chrome(options=opts)
|
|
d.implicitly_wait(5)
|
|
return d
|
|
|
|
def step1_get_totp_from_2fa_live(d):
|
|
"""Get a TOTP secret from 2fa.live"""
|
|
log("STEP 1: Get TOTP secret from 2fa.live")
|
|
d.get("https://2fa.live/")
|
|
time.sleep(5)
|
|
ss(d,"01_2fa_live")
|
|
|
|
page=d.page_source
|
|
body=d.find_element(By.TAG_NAME,"body").text
|
|
log(f"2fa.live body: {body[:300]}")
|
|
|
|
# Look for secret key on the page
|
|
import pyotp
|
|
totp_secret=None
|
|
|
|
# Try to find base32 secret in page
|
|
for m in re.findall(r'[A-Z2-7]{16,64}',page):
|
|
try:
|
|
pyotp.TOTP(m).now()
|
|
totp_secret=m
|
|
log(f"Found TOTP secret: {m}")
|
|
break
|
|
except: continue
|
|
|
|
# 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
|
|
|
|
# Try text elements
|
|
if not totp_secret:
|
|
for el in d.find_elements(By.CSS_SELECTOR,"div,span,p,code,pre,td,input"):
|
|
txt=(el.text or el.get_attribute("value") or "").strip()
|
|
if 16<=len(txt)<=64 and re.match(r'^[A-Z2-7]+$',txt):
|
|
try:
|
|
pyotp.TOTP(txt).now()
|
|
totp_secret=txt
|
|
log(f"TOTP from element: {txt}")
|
|
break
|
|
except: continue
|
|
|
|
if not totp_secret:
|
|
# 2fa.live might need interaction - try clicking generate
|
|
for btn in d.find_elements(By.TAG_NAME,"button"):
|
|
if btn.is_displayed():
|
|
log(f"Clicking button: {btn.text}")
|
|
btn.click()
|
|
time.sleep(3)
|
|
break
|
|
ss(d,"01b_after_click")
|
|
page=d.page_source
|
|
for m in re.findall(r'[A-Z2-7]{16,64}',page):
|
|
try:
|
|
pyotp.TOTP(m).now()
|
|
totp_secret=m
|
|
log(f"Found TOTP after click: {m}")
|
|
break
|
|
except: continue
|
|
|
|
if not totp_secret:
|
|
# Generate our own
|
|
import pyotp
|
|
totp_secret=pyotp.random_base32()
|
|
log(f"Self-generated TOTP: {totp_secret}")
|
|
|
|
RESULTS["totp_secret"]=totp_secret
|
|
return totp_secret
|
|
|
|
def step2_google_login(d):
|
|
"""Login to Google with ifae.charity"""
|
|
log("STEP 2: Google Login")
|
|
d.get("https://accounts.google.com/signin")
|
|
time.sleep(4)
|
|
ss(d,"02_google_login")
|
|
|
|
# Enter email
|
|
email_input=WebDriverWait(d,15).until(EC.element_to_be_clickable((By.CSS_SELECTOR,"input[type='email']")))
|
|
email_input.send_keys(EMAIL)
|
|
email_input.send_keys(Keys.RETURN)
|
|
time.sleep(4)
|
|
ss(d,"02b_after_email")
|
|
|
|
# Enter password
|
|
pwd_input=WebDriverWait(d,15).until(EC.element_to_be_clickable((By.CSS_SELECTOR,"input[type='password']")))
|
|
pwd_input.send_keys(PASSWORD)
|
|
pwd_input.send_keys(Keys.RETURN)
|
|
time.sleep(5)
|
|
ss(d,"02c_after_pwd")
|
|
|
|
page=d.page_source.lower()
|
|
body=d.find_element(By.TAG_NAME,"body").text
|
|
log(f"After login: {body[:200]}")
|
|
|
|
# Check if 2FA already required
|
|
if "2-step" in page or "verification" in page or "verify" in page:
|
|
log("2FA already active - need current code or recovery")
|
|
RESULTS["2fa_already_active"]=True
|
|
return False
|
|
|
|
return True
|
|
|
|
def step3_enable_2fa(d, totp_secret):
|
|
"""Navigate to security settings and enable 2FA with TOTP"""
|
|
log("STEP 3: Enable 2FA")
|
|
import pyotp
|
|
|
|
# Go to security settings
|
|
d.get("https://myaccount.google.com/signinoptions/two-step-verification/enroll-welcome")
|
|
time.sleep(5)
|
|
ss(d,"03_2fa_enroll")
|
|
|
|
body=d.find_element(By.TAG_NAME,"body").text
|
|
log(f"2FA enroll page: {body[:300]}")
|
|
|
|
# Click Get Started / Next
|
|
for btn in d.find_elements(By.TAG_NAME,"button"):
|
|
txt=btn.text.lower()
|
|
if any(k in txt for k in ["get started","started","next","continue","commencer"]):
|
|
btn.click()
|
|
log(f"Clicked: {btn.text}")
|
|
time.sleep(4)
|
|
break
|
|
ss(d,"03b_after_start")
|
|
|
|
# May need to re-enter password
|
|
try:
|
|
pwd_input=d.find_element(By.CSS_SELECTOR,"input[type='password']")
|
|
if pwd_input.is_displayed():
|
|
pwd_input.send_keys(PASSWORD)
|
|
pwd_input.send_keys(Keys.RETURN)
|
|
time.sleep(4)
|
|
log("Re-entered password")
|
|
except: pass
|
|
ss(d,"03c_method_choice")
|
|
|
|
# Look for "Authenticator app" or "Can't use" option
|
|
body=d.find_element(By.TAG_NAME,"body").text
|
|
log(f"Method page: {body[:300]}")
|
|
|
|
# Click on authenticator app option if available
|
|
for el in d.find_elements(By.CSS_SELECTOR,"div,span,a,button,li"):
|
|
txt=el.text.lower()
|
|
if any(k in txt for k in ["authenticator","totp","app","another way","autre"]):
|
|
try:
|
|
el.click()
|
|
log(f"Clicked authenticator option: {el.text[:50]}")
|
|
time.sleep(3)
|
|
break
|
|
except: continue
|
|
|
|
ss(d,"03d_authenticator")
|
|
|
|
# Now should show QR code or setup key
|
|
# Look for "Can't scan it?" or "Enter setup key"
|
|
for el in d.find_elements(By.CSS_SELECTOR,"a,button,span,div"):
|
|
txt=el.text.lower()
|
|
if any(k in txt for k in ["can't scan","setup key","enter a setup","manual","clé"]):
|
|
try:
|
|
el.click()
|
|
log(f"Clicked manual entry: {el.text[:50]}")
|
|
time.sleep(3)
|
|
break
|
|
except: continue
|
|
|
|
ss(d,"03e_manual_key")
|
|
|
|
# Enter our TOTP secret key
|
|
# Find the input field for the key
|
|
for inp in d.find_elements(By.TAG_NAME,"input"):
|
|
itype=inp.get_attribute("type") or ""
|
|
if itype in ["text","tel"] and inp.is_displayed():
|
|
inp.clear()
|
|
inp.send_keys(totp_secret)
|
|
log(f"Entered TOTP secret in input")
|
|
time.sleep(1)
|
|
break
|
|
|
|
# Generate and enter verification code
|
|
code=pyotp.TOTP(totp_secret).now()
|
|
log(f"TOTP code: {code}")
|
|
|
|
# Click Next/Verify
|
|
for btn in d.find_elements(By.TAG_NAME,"button"):
|
|
txt=btn.text.lower()
|
|
if any(k in txt for k in ["next","verify","confirm","suivant","vérifier"]):
|
|
btn.click()
|
|
log(f"Clicked: {btn.text}")
|
|
time.sleep(4)
|
|
break
|
|
|
|
ss(d,"03f_verification")
|
|
|
|
# Enter code if needed
|
|
for inp in d.find_elements(By.TAG_NAME,"input"):
|
|
itype=inp.get_attribute("type") or ""
|
|
if itype in ["text","tel","number"] and inp.is_displayed():
|
|
inp.clear()
|
|
inp.send_keys(code)
|
|
log(f"Entered code: {code}")
|
|
break
|
|
|
|
# Confirm
|
|
for btn in d.find_elements(By.TAG_NAME,"button"):
|
|
txt=btn.text.lower()
|
|
if any(k in txt for k in ["verify","next","confirm","turn on","activer"]):
|
|
btn.click()
|
|
log(f"Clicked: {btn.text}")
|
|
time.sleep(5)
|
|
break
|
|
|
|
ss(d,"03g_2fa_done")
|
|
body=d.find_element(By.TAG_NAME,"body").text
|
|
log(f"After 2FA setup: {body[:300]}")
|
|
|
|
return True
|
|
|
|
def step4_gen_app_password(d):
|
|
"""Generate app password"""
|
|
log("STEP 4: Generate App Password")
|
|
d.get("https://myaccount.google.com/apppasswords")
|
|
time.sleep(5)
|
|
ss(d,"04_app_passwords")
|
|
|
|
body=d.find_element(By.TAG_NAME,"body").text
|
|
log(f"App passwords page: {body[:300]}")
|
|
|
|
# Enter app name
|
|
for inp in d.find_elements(By.TAG_NAME,"input"):
|
|
if inp.is_displayed() and inp.get_attribute("type") in ["text",""]:
|
|
inp.clear()
|
|
inp.send_keys("WEVADS-SMTP")
|
|
log("Entered app name")
|
|
break
|
|
|
|
# Click Create
|
|
for btn in d.find_elements(By.TAG_NAME,"button"):
|
|
txt=btn.text.lower()
|
|
if any(k in txt for k in ["create","generate","créer","générer"]):
|
|
btn.click()
|
|
log(f"Clicked: {btn.text}")
|
|
time.sleep(5)
|
|
break
|
|
|
|
ss(d,"04b_app_pwd_generated")
|
|
|
|
# Extract app password
|
|
body=d.find_element(By.TAG_NAME,"body").text
|
|
page=d.page_source
|
|
|
|
# Look for 16-char lowercase app password (xxxx xxxx xxxx xxxx)
|
|
app_pwd=None
|
|
matches=re.findall(r'[a-z]{4}\s[a-z]{4}\s[a-z]{4}\s[a-z]{4}',body+page)
|
|
if matches:
|
|
app_pwd=matches[0].replace(" ","")
|
|
log(f"APP PASSWORD: {app_pwd}")
|
|
|
|
if not app_pwd:
|
|
for el in d.find_elements(By.CSS_SELECTOR,"div,span,code,pre"):
|
|
txt=el.text.strip()
|
|
m=re.match(r'^[a-z]{4}\s[a-z]{4}\s[a-z]{4}\s[a-z]{4}$',txt)
|
|
if m:
|
|
app_pwd=txt.replace(" ","")
|
|
log(f"APP PASSWORD from element: {app_pwd}")
|
|
break
|
|
|
|
RESULTS["app_password"]=app_pwd
|
|
return app_pwd
|
|
|
|
def main():
|
|
log("="*50)
|
|
log("GSUITE ifae.charity - 2FA Setup + App Password")
|
|
log("="*50)
|
|
|
|
d=get_driver()
|
|
try:
|
|
totp=step1_get_totp_from_2fa_live(d)
|
|
if not totp:
|
|
log("FAIL: No TOTP secret"); return
|
|
|
|
ok=step2_google_login(d)
|
|
if not ok:
|
|
log("Login failed or 2FA already active")
|
|
# If 2FA already active, try to use existing TOTP
|
|
if RESULTS.get("2fa_already_active"):
|
|
log("2FA already active - skip to app password")
|
|
|
|
if ok:
|
|
step3_enable_2fa(d, totp)
|
|
|
|
app=step4_gen_app_password(d)
|
|
|
|
# Update DB
|
|
import psycopg2
|
|
db=psycopg2.connect(host="localhost",dbname="adx_system",user="admin",password="admin123")
|
|
db.autocommit=True
|
|
cur=db.cursor()
|
|
if app:
|
|
cur.execute("UPDATE admin.gsuite_accounts SET app_password=%s WHERE id=3",[app])
|
|
log(f"DB updated with app password: {app}")
|
|
if totp:
|
|
cur.execute("UPDATE admin.gsuite_accounts SET notes=%s WHERE id=3",
|
|
[json.dumps({"totp_secret":totp,"app_password":app,"updated":datetime.now().isoformat()})])
|
|
db.close()
|
|
|
|
RESULTS["status"]="completed"
|
|
with open("/opt/wevads/logs/selenium/ifae_results.json","w") as f:
|
|
json.dump(RESULTS,f,indent=2,default=str)
|
|
|
|
log("="*50)
|
|
log(f"TOTP: {RESULTS.get('totp_secret','N/A')}")
|
|
log(f"APP PWD: {RESULTS.get('app_password','N/A')}")
|
|
log(f"STATUS: {RESULTS.get('status')}")
|
|
log("="*50)
|
|
|
|
except Exception as e:
|
|
log(f"ERROR: {e}")
|
|
traceback.print_exc()
|
|
ss(d,"99_error")
|
|
finally:
|
|
d.quit()
|
|
|
|
if __name__=="__main__":
|
|
main()
|