Bem-vindo, bem-vinda a mais um curso sobre OWASP. E esse curso de OWASP nós vamos ver como trabalhar com Closure para atacar as 10 principais vulnerabilidade, isto é, exemplos das 10 principais vulnerabilidades em Closure e como nós podemos resolver esses exemplos.
Lembrando, o relatório de vulnerabilidades do OWASP muda de tempo em tempo, os 10 principais agrupamentos de vulnerabilidades apresentam inúmeros tipos de vulnerabilidades então não dá para passar pelo código de todos eles.
Então nós vamos fazer duas abordagens, nós vamos utilizar com exemplo uns exemplos que a equipe Nubank disponibilizou para nós. O Mateus Bernardes e outras pessoas especialistas de lá de dentro disponibilizam aqui, exemplos de diversos desses casos.
Então nós vamos usar tanto esses exemplos como outros exemplos que nós vamos criar em cada uma, em diversos dos 10 principais tópicos nós implementaremos na unha esse código e consertar eles na unha.
Espero que você se divirta, o único pré-requisito desse curso é claro, saber Closure e, também, já ter dado uma olhada nos principais 10 tópicos do relatório da OWASP.
Então vamos começar, alguns dos exemplos deste curso nós vamos nos basear em exemplos que o equipe do Nubank, especialistas de OWASP de segurança do Nubank, trabalharam e criaram aqui em Closure dentro de um projeto no Git Hub que está no Git Hub em “nubank/clj-owasp”.
Nós vamos ter alguns exemplos aqui para o Top 10, algum 1, 2, 3, 4, 5, etc. Claro, nós vamos nos basear em alguns deles e vamos criar os nossos próprios, interpretar e criar os nossos próprios. Lembrando o primeiro assunto de OWASP é injeção, e quando nós falamos de injeção tem todos aqueles tipos diferentes de injeção para nós trabalharmos. Então vamos parar para pensar um pouco sobre injeção.
Lembra, a injeção ela acontece quando o usuário final, seja uma máquina, seja um ser humano, seja lá o que for, envia dados para nós que nós interpretamos. Essa interpretação pode ser um comando SQL, pode ser uma query em um banco de dados não relacional, pode ser um Nocycle, pode ser um comando no Bash, um comando que você executa na sua máquina, no seu servidor, pode ser um código que vai ser interpretado no navegador de outro cliente.
Tudo isso é uma forma de injeção, você está injetando código ou injetando algum tipo de comportamento onde você não desejava. Então nós temos uma descrição simples, mais por cima que não é o foco. Aqui tem um exemplo de como fazer a vulnerabilidade que não é o nosso foco, nós queremos entender a vulnerabilidade para poder resolver ela.
E o que nós vamos fazer, vamos dar uma olhada e criar o nosso projeto. Então eu vou criar agora um projeto nosso, esse nosso projeto é de Clojure, vou criar como leiningen e o nome do projeto é “clojure-owasp”. Vou deixar ele criar o projeto para nós, vou dar “Finish”.
Assim que terminar de criar o nosso projeto, eu posso ir no diretório do código fonte onde nós temos aquele nosso arquivo core, que por padrão não está definido nada, eu vou simplesmente apagar.
O que eu quero fazer agora é implementar alguma coisa para valer, então vamos lá. O que eu vou fazer é criar um novo arquivo então “Alt + Insert” no Windows, “Ctrl + N” Mac, vou criar um novo arquivo. Esse meu arquivo vai ser o “owasp1.clj” que é a parte de injeção.
Lembrando quando nós criamos esse arquivo nós vamos definir um namespace que é um namespace do projeto owasp, ns clojure_owasp.owasp1
. Você tem esse meu namespace e aqui dentro eu vou poder fazer o que eu quiser.
O que eu queria mostrar então, primeiro exemplo, era uma função. Eu quero definir uma função que roda um cluster de Kafka. Então você fala para ele, por exemplo, qual é o arquivo de configuração do Kafka e ele vai levantar esse cluster do Kafka.
Então eu vou ter uma função chamada run-cluster
, ela vai receber como parâmetro um config-file
, e ela vai executar no sistema operacional, um processo do sistema operacional que vai ser um comando. Vamos fazer um let [comand]
, para nós fazermos a string para esse comando. Então é concatenação da string Kafka que poderia ser, por exemplo, um “/bin/kafka”
se eu estiver em um sistema operacional onde o Kafka está nesse diretório. E o nome do arquivo config-file
.
Lembrando que a concatenação não adiciona espaço, então após o /bin/kafka
eu espaço a concatenação. Então esse é o comando, e esse comando é o comando que eu vou querer executar. Para executar eu vou fazer um sh
, vou executar bash
, vou executar esse comando específico.
O que faltou foi a função sh
, sem a função sh
nada funciona. Então o que faltou falar aqui foi usar importar, eu quero importar essa função. Várias maneiras de fazer esse import, um use
ou algo do gênero. Eu posso fazer um use
explícito na linha 3, que a forma que é usado até no exemplo do Nubank, então eu vou manter esse mesmo exemplo que é um clojure.java.shell
e aí de lá eu vou importar somente o símbolo sh
.
Então pegando o símbolo sh
eu posso executar esse sh
. Isso quer dizer que toda vez que eu tentar executar alguma coisa, que eu quiser executar alguma coisa, um cluster de Kafka eu dou um run-cluster
e eu falo para ele o que eu queria executar. Eu falo para ele eu quero executar baseado no arquivo server.properties
, ou baseado no arquivo server15.properties
, seja lá qual for o arquivo que você vai utilizar de configuração para o Kafka. Então ele vai rodar o Kafka com esse arquivo de configuração.
Maravilha porque ele concatena esse string com essa string. Mas nós já falamos de injection, que o problema de injection, um dos problemas clássicos é justamente concatenação de string, porque nós não estamos nos perguntando como que este programa da linha 7 vai interpretar essa string, e como ele interpreta essa string para executar programas, nós estamos correndo risco.
Qual é o risco? Bom, se eu fizer só isso certo, o que eu vou fazer, vamos dar uma olhada nisso daqui, minha máquina não tem o sh
na linha 7. Eu estou misturando linguagens a rodo esses dias, como de costume. O que eu vou fazer, simplesmente um println
do comando, para nós vermos o comando que seria executado.
Então se eu rodar esse código, então lembrando que para rodar tem várias maneiras, uma das maneiras era clicar à direita aqui no “clojure-owasp”, rodar um “REPL” genérico, deixar o meu Windows rodar para nós, maravilha, está rodando. Eu posso dar agora um require, namespace, o que eu quiser. E mandar rodar esse arquivo.
Eu posso dar uma execução desse arquivo, mandar rodar esse namespace, um “Alt + Shft + L” no Windows, vou limpar aqui minha tela e rodar um “Alt + Shift + L” para reloadar esse arquivo, quando ele reloadar ele executa e imprime para nós /bin/kafka server.properties
.
Mas qual que é a vulnerabilidade aqui, é chamar um run-cluster
e o nosso usuário final, seja quem for que passa esses string para nós, passar o server properties
, mas além desse server properties
passar um “;” e um ls
para o diretório raiz.
Então o que eu estou fazendo, se eu dou um “Alt + Shift + L” de novo, se eu relodo o programa de novo, ele vai executar isso tudo. Então se o bash executa esses dois comandos, tanto o Kafka, quanto o ls, é isso que ele vai fazer, as duas coisas. “Ah, então talvez eu tenha que colocar o ‘e’ comercial, talvez seja dois ‘e’”, não vai ser dois “e” nesse caso, vai depender do seu bash, vai depender do que você está utilizando, claro.
Mas o usuário final consegue agora passar e explorar, e à medida que vai explorando, consegue executar um comando que nós não tínhamos desejado. Então na linha 12 eu consegui uma injeção em um comando do bash, executaria um ls em um comando do bash. E é isso que nós não queremos fazer. Isso está vulnerável a injeção.
Vamos dar uma olhada no exemplo do próprio Nubank que é 100% equivalente. Importa o shell, ele recebe o comando e o que ele vai fazer é gerar uma chave. Ele vai gerar uma chave no certificado, ele vai gerar um certificado então ele também está executando um bash e o generate-rsa-key
vai gerar a última linha. O ataque foi com um “;”, e manda atacar e é isso.
Como que é feita a correção disso, como que eu vou corrigir isso. Bom, o problema no nosso caso específico é que o comando bash aceita vários comandos a serem executados. O bash-c
, ele suporta vários comandos com o “;”, esse que é o problema. Então quer dizer, tudo que nós fazemos a execução com algo baseado no nosso usuário final, lembra o que nós temos que fazer?
Sanitizar, ou utilizar uma biblioteca que sanitiza para nós os dados, ou limitar para que isso não seja interpretado erroneamente. Então o bash-c
abre esse buraco para nós, tem vezes que o sh
dessa maneira eu vou falar um sh
direto para aquilo que eu quero executar, então o que eu quero executar direto é um /bin/kafka
. Esse é o programa que eu vou executar e esse é o parâmetro que eu vou passar para o meu programa.
Então eu não criei uma string grande para passar para o bash, o sh vai executar somente um programa, e esse um programa vai receber parâmetros, vários parâmetros. Então repara que agora o Command nem faz mais sentido.
Essa era a variação que era perigosa e a variação que eu vou me proteger desse ataque específico, não quer dizer que está protegida de os todos ataques porque vai depender agora do que o comando do Kafka faz com esse config-file
, nós não sabemos exatamente o que ele faz com esse config-file
. Mas agora sim quando eu executo esse código, ele vai executar o sh com esses dois.
Lembrando, como meu programa, meu computador não tem o Kafka instalado, não é o foco nós instalarmos Kafka, ele não vai executar, ele deve reclamar que não existe esse arquivo para ser executado. Não consegue criar o processo, porque não encontrou o arquivo /bin/kafka
.
Era isso mesmo que nós queríamos, tentar executar o /bin/kafka
, então eu posso apagar essa linha 9 porque essa é a forma corrigida. Então essa é forma problemática, essa é a forma corrigida. No navegador nós temos um exemplo bem próximo, e a correção foi exatamente essa, utilizar o sh.
Tem um detalhe que eles fizeram diferente, que é uma das maneiras de trabalhar, estão trabalhando sempre com mapas. E por que estão trabalhando com mapas aqui, é um dos costumes de muita gente trabalha com Clojure é nós termos um nome, passar tudo através de nomes. E no nosso código estamos passando posicional os nomes. É o primeiro parâmetro, o segundo parâmetro, o terceiro parâmetro.
No momento em que nós sempre recebemos um único mapa, tudo passa a ser baseado em nome. Então programas, enquanto em algumas linguagens de programação só suportam baseados em nome, outras linguagens de programação suportam em nome, ou posicional, e combinar, e variar, etc. Aqui vai nem a da linguagem, é uma estrutura que você cria em cima da estrutura da linguagem.
Então esse é um primeiro exemplo, lembrando que tem um outro exemplo aqui, vou dar um copy aqui, eu não vou ficar duplicando código, nós só queríamos discutir. Então na linha 17 é um outro exemplo equivalente, mas eu não queria parar por aqui. Eu queria lembrar que isso não acontece só com bash, isso acontece com qualquer coisa que nós usamos strings do usuário final.
Por exemplo, se eu tenho um sistema de login que recebe o usuário e a senha, não estou usando o mapa estou usando parâmetros posicionais, e ai eu crio 'uma string sql
, lembrando, isso não é só de SQL, e a minha sting é str “select * from Users where username’ “username” ‘and password’” password”’”
. E aí eu acho que nós fechamos direito, eu defini esse let
, e dentro desse let
eu só vou dar um println sql
. Mas aqui nós executaríamos o sql.
Então esse seria a minha situação, na linha 25 também, se eu executar então um login, eu tenho várias maneiras de brincar. Eu poderia logar como “Guilherme” e “senha”, lembrando que eu vou comentar isso nas linhas 13 e 14, se não ele vai executar o /bin/kafka
e não é o que nós queremos. Vamos carregar, está aqui na lateral direita select *
ficou bonito.
Mas qual é o problema da vulnerabilidade, problema não é um usuário esperto, maldoso, malvado, um atacante que vai escrever fechar aspas ou, id=1
, e aí ele precisa terminar com qualquer outra coisa, pode ser having “1’ = ‘1”
, acho que isso é suficiente. Vamos testar se o código final ficou um sql válido, select * from user where username = ‘Guilheme’ and password = ‘’ or id = 1
.
Então quer dizer ou a primeira sentença é verdadeira ou id = 1, beleza é igual a 1, ele vai me logar com id = 1, ele vai achar usuários com id = 1, desses usuários com id = 1 ele vai filtrar quis tem 1=1, que são todos, então ele vai trazer o usuário com id 1=1 e fica feliz. Claro outra variação, fica feliz o atacante, nós ficamos tristes.
Outra variação seria admin=true
, ou qualquer outra coisa do gênero. É claro que daí a pessoa vai começar a explorar o seu banco e tentar descobrir o que tem para fazer login, a medida que percebeu que dá para fazer sql injection
. Então vou rodar isso de novo para nós vermos.
Então esses são os perigos, como que eu corrijo esse caso do sql, de novo, ou eu vou usar uma biblioteca que limita, que é aqui no caso utilizar o sh diretamente, lembrando que nós estamos suscetíveis a problemas do próprio Kafka, ou usar uma biblioteca que já faz o scape, então essas bibliotecas já vão fazer o scape para nós, ou nós temos que fazer um scape na mão. Nós temos que verificar se tem aspas simples na linha 26, 27 ou 28, se tem aspas simples fazer o scape.
Só que nós entramos no perigo de não suportamos suportar todos os scapes necessários e ficar com um buraco de segurança. Então o ideal é sempre utilizar bibliotecas atualizadas que já fazem isso para nós. Não tentar resolver esses problemas de injection na unha, utilize bibliotecas que já fazem a sanitização e o scape de todas as informações que o usuário final passa para nós.
Essa é a maneira de nós resolvermos essa vulnerabilidade, então na linha 11 teve uma sacada que foi utilizar um programa que já faz isso para nós, que vai ignorar, a partir da linha 21 a solução seria usar alguma biblioteca como datomic que na query já vai, como a query faz parte da linguagem, não vai ter essa questão do concatenar.
A não ser que você use bibliotecas que concatenam, e aí por favor evite, e passe a usar as funções que não concatenam. Em geral toda biblioteca, em geral não toda, em geral toda a biblioteca que interpreta strings vai ter problemas de injeção.
E quem interpreta strings? Toda a biblioteca que executa código, toda biblioteca que executa querys, toda biblioteca que faz coisas do gênero do gênero interpreta. E aí nós temos o problema de injection e a solução são essas que eu citei.
Vamos lembrar agora no Owasp top 10 qual que é o segundo item, está tudo cheio de link, no segundo item nós vamos falar de autenticação quebrada.
Na autenticação quebrada, lembra, tem diversas maneiras de quebrar um sistema de autenticação, então vamos ver alguns exemplos. O primeiro exemplo eu vou querer pegar lá da equipe do Nubank que está na outra aba, que vai mostrar para nós uma simulação de uma tabela de usuários. Vamos dar uma olhada.
Nós temos um namespace, não é playground o nosso namespace, ele tem um banco de dados que é um átomo vazio, e quando nós quisermos, nós adicionamos em uma tabela algum dado. Como é um átomo, é um swap, e dentro dessa tabela nós atualizamos, adicionando, porque você está dando um add, então adicionando o documento que você está adicionando.
Então ele está dizendo assim: “Olha, esse banco de dados é um átomo vazio, ele só é atualizável no sentido de adicionar informações que é um documento que você adiciona numa tabela”. Maravilha, é o que esse banco suporta.
Quando você cadastra um novo usuário, você recebe usuário e senha e ele adiciona, ele chama esse add na tabela e users, então quer dizer, dentro do átomo que é um mapa, na chave users ele vai adicionar algum valor, e o valor é esse mapa aqui dentro, então vão ter vários mapas dentro da chave users. Então a chave users vai ter vários, e um que vai ter vai ser esse usuário e essa senha da penúltima linha, matheus.bernardes
, o autor, e banana
. Todas as aspas são aspas normais.
Então essa é a base que nós vamos utilizar para nosso exemplo. Eu vou lá no “intelliJ”, “Alt + Insert”, o nome do nosso arquivo vai ser “owasp2.clj”, ns clojure_owasp.owasp2
. E aí nós começamos a trabalhar. Então o que nós vamos querer fazer, nós vamos definir o nosso banco, então def database
, ele é, lembra, um átomo vazio, então atom {}
.
Depois disso, que eu tenho esse meu database o que eu vou falar é, vou definir uma função adiciona, que recebe uma tabela e recebe um documento. Então o que ele quer fazer mesmo não add, lembra isso aqui é um átomo, então swap!
, nós vamos trocar o valor interno do átomo. O nome do símbolo que acessa o átomo é o database
, nós queremos dar um update dentro desse database, fazendo o que?
Dentro dessa tabela da linha 5, nós vamos querer adicionar nessa lista este documento. É isso que nós queremos fazer.
Então, qual que é o primeiro exemplo mesmo? A função de registro do usuário, que era register-new-user !
com exclamação, porque a gente tem um efeito colateral aqui, o add eu acho que não tinha, vou manter o mesmo padrão. Que recebe usuario e senha, e a função fazia o quê, ela ia carinhosamente para chamar um add
, passar para nós a tabela, que é a tabela users
que é a chave, e dentro dessa chave, um conjunto, vai ter um conjunto com username
e passoword
.
Então esse é o register-new-user
. Eu vou aumentar um pouco a fonte que está me incomodando essa fonte muito pequena, aqui, editor tem um fonte aqui para nós, eu vou em fonte 19, porque números primos são sempre mais legais, nem sempre, mas aqui foi mais legal.
Vamos lá, agora eu tenho essa função register-new-user
e eu vou manter o exemplo do Matheus, matheus.bernardes
em homenagem a ele, espero ter escrito certo, e a senha que ele tinha escolhido era banana
, esperamos que não seja essa banana, com certeza não é essa banana.
Nós temos um “REPL” rodando aqui na lateral, vou abrir o nosso “REPL”, e vou limpar no canto direito e vou interpretar esse código aqui, loadar e ele não imprimiu nada. Claro, não imprimiu nada porque nós chamamos reguster-user
e ele não faz nada. Vamos dar um println
, coloquei aqui para dentro, rodei de novo, devolveu o resultado do átomo que é o users
com esse usuário essa senha.
Qual que é o problema? Por que nós temos uma vulnerabilidade aqui como um problema? Porque a senha está armazenada em texto aberto, banana
solto, livre para quem quiser fazer caca. Lembra, nunca colocar uma senha plana. Nós temos que colocar, eu falo plana, mas o plain aqui não é de plano, mas sim criptografar. Tem que criptografas as senhas.
Então a primeira solução nossa vai ser essa, vamos criptografar. O que nós temos de biblioteca de criptografia em Clojure, como em qualquer linguagem decente, tem uma infinidade de coisas. Eu vou procurar o “Bcrypt” que é no “crypto bcrypt” do Clojure. Eu vou querer pegar esse primeiro link do “crypto-password”. Então aqui está o “crypto-passowrd” que nós vamos utilizar dentro do nosso leiningen, dei um “Ctrl + C” e vou dar um “Ctrl + V” aqui.
Ele vai em alguma maneira perceber nas linhas 6, 7 e 8, mostrar import changes
, gostaria que fizesse automático, não fez, mas eu estou ok, feliz com isso. Então deve ser atualizado, e aí quando ele atualizar nós vamos falar que nós vamos utilizar essa biblioteca, vou dar um require
nela.
O require claro, nós podemos ir sempre para a documentação, como eu fiz no caso do navegador que é esse require que está em installation, ou fazer na unha. No meu caso eu vou descrevendo para vocês.
É um crypto.password.bcrypt :as password
. É o exemplo basicamente que ele faz no navegador, você escolhe um algoritmo como password. E agora a ideia é nós tentarmos criptografar, nós queremos sair criptografando. Como que nós podemos fazer isso? Vamos pegar esse exemplo aqui é só um exemplo, odeio foobar, mas é só um exemplo.
Então nós podemos simplesmente definir um símbolo temporário, que a senha, está em inglês então encrypted-password
que é a versão criptografada dela, então do password/encrypit
e aí nós passamos a nossa senha, por exemplo, a senha banana
. Lembra a senha banana
? Essa daqui é meu encrypted
.
Então nós podemos dar um println
no encrypted-password
e rodar. Digitei alguma coisa errada em algum lugar. Claro, faltou dar “Stop” e dar “Start”, mas é pegadinha, Guilherme sempre erra isso. Para o “REPL” carregar o project clj
novo, tem que carregar a versão nova. Vou rodar de novo, agora sim. Outro problema, agora um problema da biblioteca em si, quer dizer, eu errei alguma coisa da biblioteca. Faz mais sentido. Pelo menos está fazendo sentido agora eu ter errado algo.
Vamos ver o que eu errei vector
, spec
, ele estava esperando o parâmetro list no defn
. Na linha 14 é def
. Então aqui está a criptografada. Então cada algoritmo de criptografia funciona de uma maneira diferente, o encrypt da banana
devolveu isso, se você rodar de novo você vai ver que ele para devolver um outro valor, ele devolve uma primeira parte igual e um outro valor.
Mas essa é a maneira que funciona o “bcrypt”, tem suas características do porque ele funciona assim, e aí cada algoritmo de criptografia tem suas características. Como é que é válido, depois que eu criptografei, como é que eu valido para ver se a senha bateu. Nós chamamos um password/check
se a senha banana
é equivalente ao nosso encrypted-password
. E aí se nós damos um print nisso, porque senão não serve para nada, rodo aqui, true. Então é isso mesmo.
Então a senha criptografada é equivalente a senha banana
. Então assim nós criptografamos, assim nós validamos a criptografia, não é que nós descriptografamos a senha não, nós estamos validando a criptografia que foi aplicada do “bcrypt”.
O que nós temos que fazer agora, atualizar a versão da função das linhas 8 e 9 para não utilizar essa versão plain text
. Antes de apagar, vamos dar um copy nessas linhas 11 e 12 e dar um paste na linha 25.
Então o novo register-new-user
, o que ele vai fazer, ao invés de simplesmente adicionar assim como na linha 21, ele vai ter que criptografar. Então let encrypted
como sendo password/encrypted password
. Então nós criptografamos.
Agora que eu criptografei, o que eu faço, eu dou um “Alt + Shift + K”, eu adiciono nos usuários, esse usuário e a senha criptografada. Se eu rodar agora, olha o usuário matheus.bernardes
, tem esta senha do canto direito. Então não está mais aparecendo a palavra banana
.
Com isso nós já resolvemos esse nosso problema inicial da senha estar plain text lá no banco, certo. Nós já resolvemos esse primeiro passo. Esse é um dos passos muito comuns de quebra de autenticação, de autenticação quebrada, mas tem outros e eu gostaria de mostrar outro daqui a pouco para vocês.
O curso OWASP: melhorando a segurança com Clojure possui 128 minutos de vídeos, em um total de 40 atividades. Gostou? Conheça nossos outros cursos de Segurança em DevOps, ou leia nossos artigos de DevOps.
Matricule-se e comece a estudar com a gente hoje! Conheça outros tópicos abordados durante o curso:
Impulsione a sua carreira com os melhores cursos e faça parte da maior comunidade tech.
1 ano de Alura
Assine o PLUS e garanta:
Formações com mais de 1500 cursos atualizados e novos lançamentos semanais, em Programação, Inteligência Artificial, Front-end, UX & Design, Data Science, Mobile, DevOps e Inovação & Gestão.
A cada curso ou formação concluído, um novo certificado para turbinar seu currículo e LinkedIn.
No Discord, você tem acesso a eventos exclusivos, grupos de estudos e mentorias com especialistas de diferentes áreas.
Faça parte da maior comunidade Dev do país e crie conexões com mais de 120 mil pessoas no Discord.
Acesso ilimitado ao catálogo de Imersões da Alura para praticar conhecimentos em diferentes áreas.
Explore um universo de possibilidades na palma da sua mão. Baixe as aulas para assistir offline, onde e quando quiser.
Acelere o seu aprendizado com a IA da Alura e prepare-se para o mercado internacional.
1 ano de Alura
Todos os benefícios do PLUS e mais vantagens exclusivas:
Luri é nossa inteligência artificial que tira dúvidas, dá exemplos práticos, corrige exercícios e ajuda a mergulhar ainda mais durante as aulas. Você pode conversar com a Luri até 100 mensagens por semana.
Aprenda um novo idioma e expanda seus horizontes profissionais. Cursos de Inglês, Espanhol e Inglês para Devs, 100% focado em tecnologia.
Transforme a sua jornada com benefícios exclusivos e evolua ainda mais na sua carreira.
1 ano de Alura
Todos os benefícios do PRO e mais vantagens exclusivas:
Mensagens ilimitadas para estudar com a Luri, a IA da Alura, disponível 24hs para tirar suas dúvidas, dar exemplos práticos, corrigir exercícios e impulsionar seus estudos.
Envie imagens para a Luri e ela te ajuda a solucionar problemas, identificar erros, esclarecer gráficos, analisar design e muito mais.
Escolha os ebooks da Casa do Código, a editora da Alura, que apoiarão a sua jornada de aprendizado para sempre.