Encadeando Comandos


Cyberguard: Linux Luminarium.

No módulo sobre pipes (piping), você explorou o conceito de utilizar vários comandos, com dados fluindo entre eles através de canais, para realizar tarefas mais complexas do que comandos isolados conseguiriam fazer. Naturalmente, esse conceito também se aplica de forma independente da transferência de dados: às vezes, você só deseja executar vários comandos em sequência rápida para obter um efeito cumulativo.

Este módulo abordará diversas maneiras, além dos pipes, pelas quais comandos podem ser encadeados. Ao final, você estará a caminho de escrever seus próprios shell scripts!


A maneira mais simples de encadear comandos é com o ponto e vírgula: ;. Na maioria dos contextos, o ; separa comandos de forma semelhante a como a tecla Enter separa linhas. Portanto, isto:

hacker@dojo:~$ echo COLLEGE > pwn
hacker@dojo:~$ cat pwn
COLLEGE
hacker@dojo:~$

É praticamente o mesmo que isto:

hacker@dojo:~$ echo COLLEGE > pwn; cat pwn
COLLEGE
hacker@dojo:~$

Basicamente, quando você pressiona Enter, a shell executa o comando digitado e, após o término desse comando, exibe o prompt novamente para você digitar o próximo. O ponto e vírgula é análogo, com a diferença de que não há um novo prompt entre eles e você insere ambos os comandos antes que qualquer um deles seja executado.

Experimente agora! Neste nível, você deve executar /challenge/pwn e, em seguida, /challenge/college, encadeando-os com um ponto e vírgula.

Você aprendeu sobre códigos de saída no módulo de Processos. Agora, vamos usá-los para encadear comandos!

O operador && permite que você execute um segundo comando apenas se o primeiro comando for bem-sucedido (na convenção do Linux, isso significa que ele finalizou com código de saída 0). Ele é chamado de operador "AND" (E lógico) porque ambas as condições precisam ser satisfeitas: o primeiro comando deve ter sucesso E só então o segundo comando será executado. Isso é extremamente útil em fluxos de trabalho avançados na linha de comando onde determinadas ações dependem do êxito de etapas anteriores.

Veja a sintaxe:

hacker@dojo:~$ command1 && command2

Isso significa: "Execute command1 e, SE ele for bem-sucedido, execute command2."

Alguns exemplos:

hacker@dojo:~$ touch /home/hacker/file && echo "this will run"
success
this will run
hacker@dojo:~$ touch /file && echo "this will NOT run"
touch: cannot touch '/file': Permission denied
hacker@dojo:~$

A segunda invocação de touch falhou porque o usuário hacker não tem permissão de escrita em /file; portanto, o comando echo não foi executado.

Neste desafio, você precisa encadear os programas /challenge/first-success e /challenge/second utilizando o operador &&. Experimente executar cada comando separadamente primeiro para ver o que acontece (você verá que não receberá a flag). Mas se encadeá-los com &&, a flag aparecerá!

Você acabou de aprender sobre o operador &&, que executa o segundo comando apenas se o primeiro for bem-sucedido. Agora, vamos conhecer o seu oposto: o operador || permite executar o segundo comando apenas se o primeiro comando falhar (isto é, terminar com um código de saída diferente de zero). Ele é chamado de operador "OR" (OU lógico) porque ou o primeiro comando tem sucesso OU o segundo comando será executado como alternativa.

Veja a sintaxe:

hacker@dojo:~$ command1 || command2

Isso significa: "Execute command1 e, SE ele falhar, execute command2."

Alguns exemplos:

hacker@dojo:~$ touch /file || echo "touch failed, so this runs"
touch: cannot touch '/file': Permission denied
touch failed, so this runs
hacker@dojo:~$ touch /home/hacker/file || echo "this will NOT run"
hacker@dojo:~$

O operador || é muito útil para fornecer comandos alternativos de contingência (fallback) ou tratamento básico de erros!

Neste desafio, você precisa encadear /challenge/first-failure e /challenge/second utilizando o operador ||. Manda ver!

À medida que você combina mais e mais comandos para produzir efeitos complexos, o tamanho da linha digitada no prompt rapidamente se torna cansativo de gerenciar. Quando isso acontece, você pode colocar esses comandos dentro de um arquivo, chamado de shell script, e executá-los rodando esse arquivo! Por exemplo, relembre a nossa técnica com ponto e vírgula:

hacker@dojo:~$ echo COLLEGE > pwn; cat pwn
COLLEGE
hacker@dojo:~$

Podemos criar um shell script chamado pwn.sh (por convenção, scripts de shell costumam utilizar a extensão .sh):

echo COLLEGE > pwn
cat pwn

E então podemos executá-lo passando o arquivo como argumento para uma nova instância da nossa shell (bash)! Quando uma shell é invocada dessa forma, em vez de esperar comandos digitados pelo usuário, ela lê e executa linha por linha os comandos do arquivo.

hacker@dojo:~$ ls
hacker@dojo:~$ bash pwn.sh
COLLEGE
hacker@dojo:~$ ls
pwn
hacker@dojo:~$

Você pode notar que o shell script executou ambos os comandos, criando e imprimindo o arquivo pwn.

Agora é a sua vez! Assim como no nível anterior, execute /challenge/pwn e depois /challenge/college, mas desta vez inserindo ambos em um shell script chamado x.sh e rodando-o com bash!


NOTA: Ainda não discutimos a ampla gama de excelentes editores de texto de linha de comando no Linux. Por enquanto, sinta-se à vontade para usar o aplicativo Text Editor no modo Desktop (Applications->Accessories->Text Editor) ou o próprio editor do workspace no VSCode!

Vamos tentar algo um pouco mais engenhoso! Você já canalizou saídas entre programas com o pipe |, mas até agora isso foi apenas entre a saída de um único comando e a entrada de outro comando. Mas e se você quisesse enviar a saída combinada de vários programas para um único comando receptor? Existem algumas formas de fazer isso, e exploraremos uma bastante direta aqui: redirecionar a saída do seu script!

Para a shell, o seu script é simplesmente mais um comando. Isso significa que você pode redirecionar a entrada e a saída dele da mesma maneira que fazia com comandos no módulo de Pipes e Redirecionamentos! Por exemplo, você pode gravar a saída do script em um arquivo:

hacker@dojo:~$ cat script.sh
echo PWN
echo COLLEGE
hacker@dojo:~$ bash script.sh > output
hacker@dojo:~$ cat output
PWN
COLLEGE
hacker@dojo:~$

Todos os métodos de redirecionamento funcionam normalmente: > para stdout, 2> para stderr, < para stdin, >> e 2>> para modo anexação (append), >& para redirecionar entre descritores de arquivo e | para direcionar a saída em pipe para outro programa.

Neste nível, praticaremos o uso de pipe (|) da saída do seu script para outro programa. Como abordaremos scripts diretamente executáveis mais adiante, execute seu script com bash ao canalizar a saída dele. Como antes, crie um script .sh que invoque o comando /challenge/pwn seguido do comando /challenge/college, e redirecione a saída gerada pelo script através de um pipe para uma única invocação do comando /challenge/solve.

Você escreveu seus primeiros shell scripts, mas ter que chamá-los digitando bash script.sh toda vez pode ser inconveniente. Por que você precisaria desse bash explícito afinal?

Quando você executa bash script.sh, você está, na verdade, iniciando o utilitário bash com o argumento script.sh. Isso avisa ao bash para ler os comandos do arquivo script.sh em vez da entrada padrão (stdin), e assim o seu script é executado.

Acontece que você pode dispensar a necessidade de invocar o bash manualmente. Se o seu arquivo de script for executável (relembre o módulo de Permissões de Arquivos), você pode simplesmente chamá-lo diretamente através do seu caminho relativo ou absoluto! Por exemplo, se você criar um arquivo script.sh no seu diretório pessoal (home) e torná-lo executável, poderá executá-lo com /home/hacker/script.sh, ~/script.sh ou (se o seu diretório atual for /home/hacker) ./script.sh.

Experimente isso aqui! Crie um shell script que execute /challenge/solve, torne o arquivo executável e execute-o diretamente sem invocar explicitamente o bash!

Você está avançando rápido na sua jornada com shell scripting! No entanto, até agora, os seus shell scripts só podem ser iniciados de dentro da shell. As coisas funcionaram perfeitamente no nível anterior porque você estava invocando o script a partir da shell interativa bash, mas isso falharia se o seu script fosse invocado diretamente por um processo do sistema operacional, por exemplo, um programa escrito em Python, C ou qualquer outra linguagem.

Quando um programa é executado no Linux, o kernel inspeciona os primeiros bytes do arquivo para determinar como ele deve ser executado. O kernel NÃO se baseia na extensão do arquivo (por isso você não é obrigado a nomear seus scripts com .sh, scripts Python com .py, e assim por diante). Em vez disso, o Linux analisa os primeiros bytes do conteúdo do arquivo.

Existem vários tipos de executáveis, mas se o arquivo começar com a sequência de caracteres mágicos #! (conhecida como "shebang"), o Linux reconhece o arquivo como um programa interpretado e trata o restante da linha como o caminho absoluto para o interpretador. Em seguida, o kernel invoca esse interpretador passando o caminho do script como argumento.

Considere este shell script:

#!/bin/bash

echo "Hello Hackers!"

Ele pode ser executado assim:

hacker@dojo:~$ chmod a+x script.sh
hacker@dojo:~$ ./script.sh
Hello Hackers!
hacker@dojo:~$

Quando ./script.sh foi executado, o kernel do Linux abriu o arquivo, leu a primeira linha, extraiu /bin/bash como o interpretador e executou /bin/bash ./script.sh para iniciar o script!

Atenção: a linha da shebang deve ser OBRIGATORIAMENTE a PRIMEIRÍSSIMA linha do arquivo — sem linhas em branco e sem espaços antes dela!

Para este desafio, crie um script em /home/hacker/solve.sh que contenha uma shebang adequada e imprima o texto "hack the planet". Lembre-se de torná-lo executável e, em seguida, execute /challenge/run para validar se o seu script funciona corretamente!


CURIOSIDADE: Shebangs muito comuns que você encontrará:

  • #!/bin/bash para scripts em Bash
  • #!/usr/bin/python3 para scripts em Python 3
  • #!/bin/sh para scripts na shell padrão POSIX — um predecessor mais básico do bash com menos recursos nativos, mas com máxima compatibilidade entre sistemas Unix/Linux!

Você aprendeu a criar shell scripts, mas até agora eles eram apenas listas fixas de comandos. Os scripts tornam-se infinitamente mais poderosos quando conseguem receber argumentos dinâmicos! Isso se parece com:

hacker@dojo:~$ bash myscript.sh hello world

O script pode acessar esses argumentos utilizando variáveis posicionais especiais:

  • $1 contém o primeiro argumento ("hello")
  • $2 contém o segundo argumento ("world")
  • $3 conteria o terceiro argumento (se houvesse um)
  • ...e assim por diante

Aqui está um exemplo simples:

hacker@dojo:~$ cat myscript.sh
#!/bin/bash
echo "First argument: $1"
echo "Second argument: $2"
hacker@dojo:~$ bash myscript.sh hello world
First argument: hello
Second argument: world
hacker@dojo:~$

Para este desafio, você precisa escrever um script em /home/hacker/solve.sh que:

  1. Receba dois argumentos
  2. Imprima esses argumentos em ORDEM INVERSA (o segundo argumento primeiro, seguido do primeiro argumento)

Por exemplo:

hacker@dojo:~$ bash /home/hacker/solve.sh pwn college
college pwn
hacker@dojo:~$

Assim que seu script estiver funcionando corretamente, execute /challenge/run para capturar a sua flag!

Agora que você consegue usar argumentos em scripts, vamos torná-los mais inteligentes com lógica condicional!

No bash, você pode usar blocos if para tomar decisões:

if [ "$1" == "ping" ]
then
    echo "pong"
fi

O código acima significa: se o primeiro argumento for igual a "ping", imprima "pong". A sintaxe do bash é bastante rigorosa por algumas razões: Primeiro, você deve obrigatoriamente incluir espaços após o if, após o [ de abertura e antes do ] de fechamento. Segundo, if, then e fi devem estar em linhas separadas (ou separados por ponto e vírgula); você não pode juntá-los na mesma linha sem ;. Além disso, há uma peculiaridade clássica: em vez de endif ou algo parecido, o fechamento do bloco if é fi (if escrito ao contrário). É uma idiossincrasia do shell que você precisa memorizar.

Para este desafio, escreva um script em /home/hacker/solve.sh que:

  • Receba um argumento
  • Se o argumento for "pwn", imprima "college"
  • Para qualquer outra entrada, não imprima nada

Exemplo:

hacker@dojo:~$ bash /home/hacker/solve.sh pwn
college
hacker@dojo:~$ bash /home/hacker/solve.sh foo
hacker@dojo:~$

Assim que o seu script estiver funcionando corretamente, execute /challenge/run para capturar a sua flag!


NOTA: Ficou curioso sobre o que mais você pode testar em uma condição além da igualdade de strings? Descubra todas as comparações possíveis executando help test no seu terminal!

Os seus blocos if até agora trataram casos específicos, mas e quando quisermos lidar com qualquer outro caso não previsto? É exatamente aí que entra a cláusula else!

O bloco else é executado sempre que a condição do if for falsa:

if [ "$1" == "hello" ]
then
    echo "Hi there!"
else
    echo "I don't understand"
fi

Observe que o else não recebe uma condição de teste — ele captura qualquer entrada que não tenha correspondido anteriormente. Ele também não utiliza a palavra-chave then. Por fim, o fi vai após o bloco else para delimitar o término de toda a estrutura condicional! O else é opcional: você não precisou dele no nível anterior e só deve usá-lo quando a lógica do seu script exigir um caso padrão.

Veja um exemplo prático:

if [ "$1" == "start" ]
then
    echo "Starting the service..."
else
    echo "Unknown command. Use 'start' to begin."
fi

Para este desafio, escreva um script em /home/hacker/solve.sh que:

  • Receba um argumento
  • Se o argumento for "pwn", imprima "college"
  • Para qualquer outro valor de entrada, imprima "nope"

Exemplo:

hacker@dojo:~$ bash /home/hacker/solve.sh pwn
college
hacker@dojo:~$ bash /home/hacker/solve.sh hack
nope
hacker@dojo:~$ bash /home/hacker/solve.sh anything
nope
hacker@dojo:~$

Assim que o script funcionar conforme esperado, execute /challenge/run para capturar a flag!

Você aprendeu a usar uma única instrução if para avaliar uma condição. Mas e quando você precisa avaliar múltiplas condições em sequência? Você pode usar a cláusula elif (abreviação de else if):

if [ "$1" == "one" ]
then
    echo "1"
elif [ "$1" == "two" ]
then
    echo "2"
elif [ "$1" == "three" ]
then
    echo "3"
else
    echo "unknown"
fi

Observe que você precisa colocar um then após cada elif, exatamente como no if. E como antes, o else no final captura qualquer valor que não tenha correspondido a nenhuma das opções anteriores.

Para este desafio, escreva um script em /home/hacker/solve.sh que:

  • Receba um argumento
  • Se o argumento for "hack", imprima "the planet"
  • Se o argumento for "pwn", imprima "college"
  • Se o argumento for "learn", imprima "linux"
  • Para qualquer outro valor de entrada, imprima "unknown"

Exemplo:

hacker@dojo:~$ bash /home/hacker/solve.sh hack
the planet
hacker@dojo:~$ bash /home/hacker/solve.sh pwn
college
hacker@dojo:~$ bash /home/hacker/solve.sh learn
linux
hacker@dojo:~$ bash /home/hacker/solve.sh foo
unknown
hacker@dojo:~$

Assim que seu script estiver funcionando corretamente, execute /challenge/run para capturar a sua flag!


NOTA: Ao criar seu script, certifique-se de respeitar rigorosamente os espaços apresentados nos exemplos. Diferente de muitas outras linguagens, o bash exige que [ e ] estejam separados de outros caracteres por espaços em branco, caso contrário a shell não conseguirá interpretar a condição.

Você não é o único que escreve shell scripts! Eles são extremamente práticos para tarefas rotineiras a nível de sistema e constituem uma das ferramentas mais comuns para desenvolvedores e administradores de sistemas (sysadmins). Na verdade, uma fração surpreendente dos programas utilitários presentes em uma máquina Linux convencional são, sob o capô, scripts em shell.

Neste nível, aprenderemos a ler shell scripts. O executável /challenge/run é um script em shell que exige a digitação de uma senha secreta, mas essa senha está gravada diretamente no próprio código-fonte do script (hardcoded)! Leia o script (usando, por exemplo, o comando cat), identifique a senha e capture a sua flag!


NOTA: Sinta-se encorajado a tentar ler o código de outros desafios também! Ler código é uma estratégia fundamental para desenvolver novas habilidades, pois permite observar como certas funcionalidades foram construídas e reaproveitar essas soluções em seus próprios scripts. Mas atenção: alguns arquivos de programas são binários compilados em código de máquina e não serão legíveis para humanos. Você pode usar o comando file para distinguir binários compilados de scripts de texto, mas quase todos os desafios do Linux Luminarium são implementados como shell scripts e podem ser lidos tranquilamente com o cat.


30-Day Scoreboard:

This scoreboard reflects solves for challenges in this module after the module launched in this dojo.

Rank Hacker Badges Score