Executando verificação de segurança...
7

Levei 8 minutos para hackear o site que uma IA me garantiu ser 'quase impossível' de invadir.

Esse é o segundo post aqui no Tabnews. Como eu tinha mencionado no do Chat no Neocities usando firebase, esse é especial pro Fakebet. Um site de Cassino (falso) criado uns 90% por IA. Resguardo os 10% pra conexão com o firebase (que segue a mesma lógica que a do chat).
Confesso que não tenho todo aquele carinho por esse projeto como tenho do chat, mas vou tentar explicar certinho aqui.

O Fakebet foi criado logo depois da primeira versão do chat, mais um projeto pra me encher a cabeça. Fiz ele rapidinho usando ChatGPT pro layout e Gemini (acho que era o 1.0 ou 1.5 Pro) pros Scripts. Ao final, com dois jogos e um sendo feito, questionei a IA se o sistema era seguro e, não ironicamente, ela disse que "sim" e que alguém mal intencionado levaria bastante tempo para hackear, mas não citou nenhuma "falha". Eu sabia que não era bem verdade, porque autenticação no Frontend é foda, mas não queria ficar debugando e tals. Como eu disse, não tinha o mesmo amor igual tenho pelo Chat.

Essas semanas atrás, acho que uns 9 dias atrás, eu entrei no site, brinquei um pouco, torrei meus 1000 "R$" (fake). Depois decidi "hackear" o site e.. foi estupidamente fácil, em 8 minutos eu tinha entrado na minha conta sem senha (e na de qualquer um que eu achasse).

Parte 1: IA pau e pedra

A IA não foi burra, quando pedi uma autenticação, ela retornou que seria difícil criar no front, mas que conseguia algo complexo e bem difícil de quebrar (verdade em partes). O forma de verificar senhas parecia que tinha saído de um manual de boas práticas.

  • Registro: Ao criar uma conta, a senha do usuário é misturada a um "salt" (um "tempero" aleatório único) e transformada em um hash SHA-256.
  • Armazenamento: O banco de dados guarda apenas o nome, o salt e o hash. A senha original é responsabilidade do usuário de lembrar.
  • Login: Para logar, o sistema pega a senha digitada, o salt do banco, recria o hash e compara com o hash guardado.

Até aqui, o negócio tá perfeito. Protegido contra vazamentos de dados, rainbow tables, tudo. Se o banco de dados vazar, ninguém descobre as senhas.

O "pequeno" problema é que, depois de verificar a identidade com esse sistema robusto, a IA precisaria "manter o usuário logado". E a solução dela foi o equivalente transformar o "negócio perfeito" em um "negócio imperfeito, inviável, inseguro".. Ela cagou com o próprio sistema "seguro" que ela criou.

Após o login bem-sucedido, a única coisa que o site faz é:

localStorage.setItem('loggedInUser', 'NOME_DO_USUARIOKK');
(Jogar o nome do usuário para o localstorage)

Daí pra frente, toda a identidade do usuário no site é baseada nesse simples texto puro guardado no navegador. Copiou a segurança do manual de boas práticas e depois rasgou os dois ao mesmo tempo.

Parte 2: O Hack em 8 Minutos - Como fiz

O primeiro passo de qualquer ataque desse tipo é ter um alvo. Eu precisava de um nome de usuário que existisse no banco.

Descobri que a IA cometeu um "erro" bobo: quando tentei registrar uma conta com um nome que já existe, o site me devolvia a mensagem específica: "Este nome de usuário já está em uso.". Isso me confirmou que o meu próprio usuário, 'cillsghost', de fato existia e estava vulnerável.
Eu diria que é possível fazer uma Numeração de Usuários, mas isso estouraria os limites gratuitos do firebase antes de achar um usuário válido

Descobrindo isso, executei esse comando:

    localStorage.setItem('loggedInUser', 'cillsghost'); 

Pronto, ligado na minha conta sem usar a senha. Tive acesso ao saldo, perfil, tudo. O site simplesmente leu o nome que eu escrevi no localStorage e confiou cegamente.

Parte 3: O hack depois dos 8 minutos

Só aí já dava pra eu parar e encher a IA de ofensas. Mas decidi brincar mais um pouco.

1. Banco de dados fantasma: As regras de segurança do Firebase permitem que qualquer pessoa possa ler os dados de qualquer usuário. E pior: a regra para escrever o saldo é:

".write": "data.exists() && newData.isNumber() && newData.val() >= 0"

Essa regra só verifica se o novo saldo é um número positivo, mas não quem tá enviando. Com um simples comando curl no terminal, eu posso dar 99 milhões para qualquer jogador sem nem precisar estar no site:

curl -X PUT -d "99999999" "https://fakebet-4d3bb-default-rtdb.firebaseio.com/users/cillsghost/balance.json"

Porque "Banco de dados fantasma": Tem algo que eu não entendo até agora. Quando acesso o banco pelo navegador, ele não lista nada, ele dá permissão negada. E qualquer forma de listar os usuários também deu permissão negada. Mas ele está com a regra pra permitir a leitura. Acredito que seja um dos únicos raciocínios muito bem pensados da IA, feito pra sobrepor outras regras (Algo que não vejo com frequência no firebase).

2. Viciando os dados do jogo: A lógica do jogo de Cara ou Coroa é 100% no Front. O resultado é decidido por Math.random(). Isso significa que eu posso simplesmente abrir o console e reescrever a matemática a meu favor:

// Forçar o resultado "Cara" para sempre
Math.random = () => 0.1;
// Ou, para forçar o resultado "Coroa" pra sempre
Math.random = () => 0.9; 

Depois disso, é só apostar no que você escolheu repetidamente e nunca mais perder. O JS recebe o aviso de "ele ganhou!" do meu navegador e confia, creditando o prêmio sem questionar.

Conclusão: A culpa é da IA. Não minha. (zoas kk)

Seria muito fácil eu jogar toda a culpa nas costas da IA, afinal foi ela que desenvolveu a segurança e programou. Sob o meu comando, mas ainda foi ela. Só que na verdade a culpa sempre é do desenvolvedor, não tem como culpar a IA.

Eu me encantei com a velocidade de criação e folguei uma tarefa crítica (arquitetura de segurança) sem fazer a devida diligência. Aprendi, da forma mais prática possível, algumas lições fundamentais:

  1. O Frontend é Complicado: Nunca, jamais confie em qualquer lógica de segurança que roda no lado do cliente.
  2. Autenticação ≠ Segurança: Fazer um muro de aço não muda nada se ele for baixo.
  3. IA é pra ajudar, Não pra fazer tudo: IAs são estagiários geniais e incansáveis. Elas aceleram, escrevem códigos e resolvem problemas contidos. Mas a visão geral, os princípios de segurança e a responsabilidade final ainda são suas.

Eu gostaria de ir lá e editar o site, refazer a autenticação de uma forma menos cagada, mas isso demandaria tempo e meu tempo em liberdade é regido. Só estou postando coisas interessantes de uns tempos atrás aqui pra deixar arquivado e mostrar um pouco como eu era.

Edit: Uma coisa interessante, já que eu não entendi muito bem como a leitura da coleção pode ser pública e não publica em certos casos, decidi passar exatamente as regras do banco como estão no firebase:

{
  "rules": {
    "users": {
      "$userId": {
        // LEITURA: Permite ler tudo (hash/salt públicos).
        ".read": true,
        // ADICIONADO: Permite a CRIAÇÃO do nó do usuário (escrita)
        // SOMENTE SE o usuário NÃO existia antes E
        // os campos obrigatórios para a criação estão sendo fornecidos.
        // Se você usa Firebase Auth, considere adicionar "&& $userId === auth.uid"
        // ou "&& auth != null" se qualquer usuário autenticado puder criar outros.
        ".write": "!data.exists() && newData.hasChildren(['displayName', 'salt', 'passwordHash', 'createdAt', 'usedFreeMoney'])",
        // --- REGRAS DE ESCRITA POR CAMPO (EXISTENTES) ---
        // BALanço: Permite escrever SE o usuário já existe E o novo valor é número >= 0
        "balance": {
          ".write": "data.exists() && newData.isNumber() && newData.val() >= 0"
        },
        // CAMPOS DEFINIDOS NA CRIAÇÃO: Permite escrever SOMENTE se o usuário NÃO existia antes
        // Estas regras agora servem como VALIDAÇÃO para os campos durante a criação
        // permitida pela regra .write acima.
        "displayName": {
           ".write": "!data.exists() && newData.isString() && newData.val().length > 0 && newData.val().length <= 50"
         },
        "salt": {
           ".write": "!data.exists() && newData.isString() && newData.val().length > 0"
        },
        "passwordHash": {
           ".write": "!data.exists() && newData.isString() && newData.val().length === 64"
        },
        "createdAt": {
           ".write": "!data.exists() && newData.isNumber()"
        },
        // Permite criar 'usedFreeMoney' apenas no registro, como false.
        "usedFreeMoney": {
            ".write": "!data.exists() && newData.isBoolean() && newData.val() === false"
        },
        // Impede criar/escrever quaisquer OUTROS campos dentro do nó do usuário
        "$other": {
          ".write": false
        }
      }
    },
    "$other": { ".read": false, ".write": false }
  }
}

Como eu tinha mencionado, há algumas sobreposições de regras. Se alguém que entende melhor esse megazord puder ver se dá pra descobrir o Nick de outros usuários, a Enumeração de Usuários ficaria mais simples.

Carregando publicação patrocinada...
6

localStorage.setItem('loggedInUser', 'NOME_DO_USUARIOKK');

Ri alto quando vi isso aqui.

Abri o post esperando algo ruim, mas não pensei que seria tanto

1

Ele mandou fazer a autenticação sem backend. Você teria feito como? Até um desenvolvedor humano teria feito a mesma coisa, se não recusado completamente a fazer.

1

Você teria feito como

Caso for uma aplicação critica: não faria, autenticação pra ser relativamente segura só com cookies httponly

Caso for uma aplicação sem muita relevancia (algo só pra acessar determinadas visualizações, sem operações destrutivas) um token JWT de curta duração.

Mas porque autenticação sem backend?

1

No Neocities, por ser um lugarzinho fofo pra você hospedar um site simples usando HTML/CSS/JS, ele não te deixa ter acesso a um backend e nem a funções serverless (como o Netlify por exemplo)..

Eu cheguei a dar uma olhada, eles não usam linguagens padrões pra backend e tals, usam o mínimo pra não pesar, pra rodar sites estáticos e afins. Foi um baita tiro certeiro no escuro quando decidi usar o firebase de banco de dados, porque foi o único banco que conseguiu firmar conexão nessa joça.

E a ideia que eles buscam com essa hospedagem gratuita e basiquinha é trazer aquela "Internet antiga" (não vivi bem essa época/não lembro). Pode ver que tem bastante site foda feito com puro html, CSS e quase nada de JS.. Tem uma pessoal bem hardcore lá.

No meu post do chat, o primeiro site que criei no Neocities usando o firebase, eu explico mais sobre a conexão e um pouquinho sobre o Neocities.. E lá, no chat, a segurança é um pouquinho 🤏 melhor.

2

O sistema que você descreveu não foi projetado para ser seguro. Ele foi projetado pra ser simples, porque o pedido original obrigou a IA a seguir exatamente essa rota.
IA não faz threat modeling, não questiona requisitos e não diz “isso aqui é suicídio de segurança”. Ela só obedece o prompt.
Cabe a pessoa saber delegar as funções de maneira correta.

1

De fato, está bem simples, mas.. a parte que pedi pra ela focar, que é o porquê eu resolvi criar essa betzinha falsa, está muito bem feita. Eu diria que está impossível de hackear uma conta sem antes ter alguma dica ou acesso ao banco (pra listar usuários existentes). Sem bruteforce pelo menos.

Isso porque a IA não deixou o ".read" na coleção "users".. Só na coleção do usuário, ou seja, você saber um 'userId' válido pra ler somente a coleção desse usuário. Simplesmente genial.

Só não sei como que eu iria criar um ranking e coisas assim.

Aliás kk, lembro de ter ficado mó cota tentando iludir a IA pra ela criar o site com auth só no front e ela não queria de forma alguma participar porque era muito inseguro, e ela "não podia" ajudar etc.. No final, eu só consegui fazer porque no AI Studio eu consigo editar a resposta da IA 😈