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=login dos últimos 7 dias.
  • Uma lista dos alertas de conta que o Google já registra: suspicious_login, user_signed_out_due_to_suspicious_session_cookie e account_disabled_hijacked.
  • Uma triagem dos eventos login_success marcados com is_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.json na 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âmetro is_suspicious indica que a tentativa teve características incomuns, como um IP desconhecido. login_challenge_method mostra como o usuário se verificou (por exemplo password, google_authenticator, passkey, security_key).
  • suspicious_login: o Google detectou um login suspeito para a conta em affected_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.

Fluxo do script: autenticação OAuth, chamada activities.list com applicationName=login, filtro de eventos suspeitos (cookie de sessão suspeito, suspicious_login, account_disabled_hijacked, login_success com is_suspicious e vários países), relatório com IP, país, ASN e método, e resposta ao incidente.
Qualquer um dos quatro sinais leva o evento ao relatório; a combinação de mais de um no mesmo usuário pede resposta imediata.

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 em token.json. Proteja esse arquivo e apague-o ao terminar.
  • listar() chama activities.list com userKey="all", applicationName="login", o eventName desejado e startTime em 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 gmail e token da 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.