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:
- O Frontend é Complicado: Nunca, jamais confie em qualquer lógica de segurança que roda no lado do cliente.
- Autenticação ≠ Segurança: Fazer um muro de aço não muda nada se ele for baixo.
- 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.