Campanhas recentes de phishing com proxy reverso (AiTM), como a que abusa de convites do Google Calendar, noticiada aqui no NULL TRACE, roubam a sessão já autenticada e tornam o MFA por código insuficiente. Neste tutorial você vai montar, no seu próprio domínio Google Workspace, um script em Python que consulta os registros de auditoria de login pela Admin SDK Reports API e destaca sinais de sequestro de sessão.
O que você vai construir
- Uma consulta somente leitura aos eventos
applicationName=logindos últimos 7 dias. - Uma lista dos alertas de conta que o Google já registra:
suspicious_login,user_signed_out_due_to_suspicious_session_cookieeaccount_disabled_hijacked. - Uma triagem dos eventos
login_successmarcados comis_suspicious=true, com IP, país (networkInfo.regionCode), ASN e método de verificação. - Uma lista de usuários que fizeram login a partir de mais de um país no período.
Pré-requisitos
- Um domínio Google Workspace e uma conta com privilégios de administrador para ler relatórios. Use apenas no ambiente pelo qual você responde.
- Um projeto no Google Cloud com a Admin SDK API ativada.
- Python 3.10.7 ou superior e
pip. - Um cliente OAuth do tipo Desktop app, salvo como
credentials.jsonna pasta de trabalho (tela de consentimento com público Internal), conforme o quickstart oficial.
Passo 1 — Instalar as bibliotecas
Estes são os pacotes indicados pelo próprio Google no quickstart da Reports API:
python3 -m pip install --upgrade google-api-python-client google-auth-httplib2 google-auth-oauthlib
Passo 2 — Entender os eventos que importam
A referência de eventos de login documenta os campos usados no script:
login_success: login bem-sucedido. O parâmetrois_suspiciousindica que a tentativa teve características incomuns, como um IP desconhecido.login_challenge_methodmostra como o usuário se verificou (por exemplopassword,google_authenticator,passkey,security_key).suspicious_login: o Google detectou um login suspeito para a conta emaffected_email_address.user_signed_out_due_to_suspicious_session_cookie: o usuário foi desconectado porque o Google detectou um cookie de sessão suspeito. É o sinal mais próximo de roubo de sessão.account_disabled_hijacked: conta suspensa por atividade que indica comprometimento.
A documentação do método activities.list confirma que cada atividade traz ipAddress e networkInfo (com regionCode ISO 3166-1 e ipAsn), e que o escopo exigido é admin.reports.audit.readonly, só de leitura.

Passo 3 — Criar o script
Salve o código abaixo como hunt.py. A autenticação é a mesma do quickstart oficial; o resto pagina os resultados com list_next e aplica as três verificações.
import os.path
from collections import defaultdict
from datetime import datetime, timedelta, timezone
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
SCOPES = ["https://www.googleapis.com/auth/admin.reports.audit.readonly"]
DIAS = 7
ALERTAS = [
"suspicious_login",
"user_signed_out_due_to_suspicious_session_cookie",
"account_disabled_hijacked",
]
def autenticar():
creds = None
if os.path.exists("token.json"):
creds = Credentials.from_authorized_user_file("token.json", SCOPES)
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file("credentials.json", SCOPES)
creds = flow.run_local_server(port=0)
with open("token.json", "w") as token:
token.write(creds.to_json())
return build("admin", "reports_v1", credentials=creds)
def listar(service, inicio, evento):
req = service.activities().list(
userKey="all", applicationName="login", eventName=evento,
startTime=inicio, maxResults=1000,
)
while req is not None:
resp = req.execute()
yield from resp.get("items", [])
req = service.activities().list_next(req, resp)
def param(evento, nome):
for p in evento.get("parameters", []):
if p.get("name") == nome:
return p.get("boolValue", p.get("value", p.get("multiValue")))
return None
def main():
service = autenticar()
inicio = (datetime.now(timezone.utc) - timedelta(days=DIAS)).strftime("%Y-%m-%dT%H:%M:%S.000Z")
print(f"== Alertas de conta desde {inicio} ==")
for nome in ALERTAS:
for a in listar(service, inicio, nome):
alvo = param(a["events"][0], "affected_email_address")
print(f"[ALERTA] {a['id']['time']} {nome} -> {alvo}")
print("\n== Logins bem-sucedidos ==")
paises = defaultdict(set)
for a in listar(service, inicio, "login_success"):
ev = a["events"][0]
email = a.get("actor", {}).get("email", "?")
net = a.get("networkInfo", {})
pais = net.get("regionCode", "??")
asn = ",".join(str(x) for x in net.get("ipAsn", []))
paises[email].add(pais)
if param(ev, "is_suspicious") is True:
print(f"[SUSPEITO] {a['id']['time']} {email} ip={a.get('ipAddress')} "
f"pais={pais} asn={asn} metodo={param(ev, 'login_challenge_method')}")
print("\n== Usuarios com login em mais de um pais ==")
for email, conj in sorted(paises.items()):
if len(conj) > 1:
print(f"[MULTIPAIS] {email}: {sorted(conj)}")
if __name__ == "__main__":
main()
O que cada parte faz:
autenticar()abre o navegador na primeira execução para você autorizar com a conta de administrador e guarda o token emtoken.json. Proteja esse arquivo e apague-o ao terminar.listar()chamaactivities.listcomuserKey="all",applicationName="login", oeventNamedesejado estartTimeem RFC 3339, percorrendo todas as páginas.param()extrai um parâmetro do evento, seja booleano, texto ou lista.- O bloco final agrupa os países por usuário e imprime quem aparece em mais de um.
Passo 4 — Executar e ler a saída
python3 hunt.py
A saída tem três seções. Um exemplo ilustrativo, com dados fictícios:
== Alertas de conta desde 2026-10-28T20:00:00.000Z ==
[ALERTA] 2026-11-02T14:11:09.000Z user_signed_out_due_to_suspicious_session_cookie -> ana@exemplo.com.br
== Logins bem-sucedidos ==
[SUSPEITO] 2026-11-02T13:58:41.000Z ana@exemplo.com.br ip=203.0.113.50 pais=NL asn=64500 metodo=['password', 'google_authenticator']
== Usuarios com login em mais de um pais ==
[MULTIPAIS] ana@exemplo.com.br: ['BR', 'NL']
Nesse cenário, a mesma usuária faz um login suspeito no exterior, com senha e código TOTP (métodos que um proxy AiTM consegue repassar), e minutos depois o Google a desconecta por cookie suspeito. A combinação merece investigação imediata.
Passo 5 — Validar e responder
- Confirme no Admin console, em Relatórios > Auditoria e investigação > Eventos de registro de login, que os eventos listados pelo script aparecem com os mesmos horários e IPs.
- Fale com o usuário: viagem, VPN corporativa ou celular em roaming explicam muitos casos de vários países.
- Se o acesso não for legítimo, redefina a senha, encerre as sessões ativas da conta pelo Admin console e revise regras de encaminhamento e aplicativos OAuth autorizados (relatórios
gmailetokenda mesma API). - Para reduzir o risco, migre os usuários para chaves de acesso (passkeys) ou chaves de segurança. Como estão presas ao domínio legítimo, elas resistem ao phishing AiTM — o mesmo padrão da campanha com convites do Google Calendar —, ao contrário de SMS e TOTP.
Limitações
- Os relatórios de login não são instantâneos: há atraso entre o evento e sua disponibilidade na API, então o script serve para caça periódica, não para bloqueio em tempo real.
is_suspiciousé uma heurística do Google; falsos positivos e negativos acontecem. Um proxy AiTM hospedado no mesmo país do usuário não aparece na verificação de vários países.- IP e país podem refletir VPN ou proxy corporativo, como alerta a própria documentação.
- A retenção dos dados e a disponibilidade de alguns campos variam conforme a edição do Workspace. Para monitorar continuamente, encaminhe os logs ao Google Cloud Logging ou ao seu SIEM.
