Introduction: Bourne Again Shell

Bash or Bourne Again shell is the scripting language we use to commmunicate with Unix-based OS and give commands to the system. Since 2019, Windows also provides a Windows Subsystem for Linux that allow us to use Bash in a Windows environment.

The main difference between scripting and programming languages is that we don't need to compile the code to execute the scripting language, as opposed to programming languages.

As a penetrations testers, we have to be able to work with both Windows and Unix-based operating system, especially in the privilege escalation field. On Unix-based systems, it is essential to learn how to use the terminal, filter data, and automate processes.

It is also essential to learn how to combine several commands and work with individual results. Scripiting language has these structure:

  • Input & Output
  • Arguments, Variables & Arrays
  • Conditional execution
  • Arithmetic
  • Loops
  • Comparison operators
  • Functions

Script Execution -Examples

To execute a script, we have to specify the interpreter and tell it which script it should process. Such a call looks like this:

chaostudy@htb[/htb]$ bash script.sh <optional arguments>
chaostudy@htb[/htb]$ sh script.sh <optional arguments>
chaostudy@htb[/htb]$ ./script.sh <optional arguments>

CIDR.sh Example

Let us look at such a script and see how they can be created to get specific results. If we execute this script and specify a domain, we see what information this script provides.

chaostudy@htb[/htb]$ ./CIDR.sh inlanefreight.com

Discovered IP address(es):
165.22.119.202

Additional options available:
    1) Identify the corresponding network range of target domain.
    2) Ping discovered hosts.
    3) All checks.
    *) Exit.

Select your option: 3

NetRange for 165.22.119.202:
NetRange:       165.22.0.0 - 165.22.255.255
CIDR:           165.22.0.0/16

Pinging host(s):
165.22.119.202 is up.

1 out of 1 hosts are up.

CIDR.sh

#!/bin/bash

# Check for given arguments
if [ $# -eq 0 ]
then
    echo -e "You need to specify the target domain.\n"
    echo -e "Usage:"
    echo -e "\t$0 <domain>"
    exit 1
else
    domain=$1
fi

# Identify Network range for the specified IP address(es)
function network_range {
    for ip in $ipaddr
    do
        netrange=$(whois $ip | grep "NetRange\|CIDR" | tee -a CIDR.txt)
        cidr=$(whois $ip | grep "CIDR" | awk '{print $2}')
        cidr_ips=$(prips $cidr)
        echo -e "\nNetRange for $ip:"
        echo -e "$netrange"
    done
}

# Ping discovered IP address(es)
function ping_host {
    hosts_up=0
    hosts_total=0

    echo -e "\nPinging host(s):"
    for host in $cidr_ips
    do
        stat=1
        while [ $stat -eq 1 ]
        do
            ping -c 2 $host > /dev/null 2>&1
            if [ $? -eq 0 ]
            then
                echo "$host is up."
                ((stat--))
                ((hosts_up++))
                ((hosts_total++))
            else
                echo "$host is down."
                ((stat--))
                ((hosts_total++))
            fi
        done
    done

    echo -e "\n$hosts_up out of $hosts_total hosts are up."
}

# Identify IP address of the specified domain
hosts=$(host $domain | grep "has address" | cut -d" " -f4 | tee discovered_hosts.txt)

echo -e "Discovered IP address:\n$hosts\n"
ipaddr=$(host $domain | grep "has address" | cut -d" " -f4 | tr "\n" " ")

# Available options
echo -e "Additional options available:"
echo -e "\t1) Identify the corresponding network range of target domain."
echo -e "\t2) Ping discovered hosts."
echo -e "\t3) All checks."
echo -e "\t*) Exit.\n"

read -p "Select your option: " opt

case $opt in
    "1") network_range ;;
    "2") ping_host ;;
    "3") network_range && ping_host ;;
    "*") exit 0 ;;
esac

As we can see, we have commented here several parts of the script into which we can split it.

  1. Check for given arguments
  2. Identify network range for the specified IP address(es)
  3. Ping discovered IP address(es)
  4. Identify IP address(es) of the specified domain
  5. Available options

1. Check for given arguments
In the first part of the script, we have an if-else statement that checks if we have specified a domain representing the target company.

2. Identify network range for the specified IP address(es)
Here we have created a function that makes a "whois" query for each IP address and displays the line for the reserved network range, and stores it in the CIDR.txt.

3. Ping discovered IP address(es)
This additional function is used to check if the found hosts are reachable with the respective IP addresses. With the For-Loop, we ping every IP address in the network range and count the results.

4. Identify IP address(es) of the specified domain
As the first step in this script, we identify the IPv4 address of the domain returned to us.

5. Available Options
Then we decide which functions we want to use to find out more information about the infrastructure.

Analysis CIDR.sh

if [ $# -eq 0 ]
then
    echo -e "You need to specify the target domain.\n"
    echo -e "Usage:"
    echo -e "\t$0 <domain>"
    exit 1
else
    domain=$1
fi

这几个都是特殊变量

$#:代表传递给脚本的参数个数。如果没有传递任何参数,$# 的值为 0

$0:表示脚本的名称(也就是运行该脚本的命令)。在输出使用说明时,$0 显示的是脚本的名称或路径。比如,如果脚本名为 myscript.sh,执行 ./myscript.sh,那么 $0 的值就是 ./myscript.sh。

$1:表示第一个参数。else 块中的 domain=$1 表示将用户传递的第一个参数赋值给变量 domain。

假如你输入 111 222 333,那么 $# 将等于 3

$0:脚本的名称或路径,例如 ./myscript.sh
$1:第一个参数,值为 111
$2:第二个参数,值为 222
$3:第三个参数,值为 333

假如输入了多个变量,程序会报错,因此可以添加额外情况

if [ $# -eq 0 ]
then
    echo -e "You need to specify the target domain.\n"
    echo -e "Usage:"
    echo -e "\t$0 <domain>"
    exit 1
elif [ $# -gt 1 ]
then
    echo -e "Too many arguments provided. Please specify only one domain.\n"
    echo -e "Usage:"
    echo -e "\t$0 <domain>"
    exit 1
else
    domain=$1
fi
在 echo 命令中,-e 选项的作用是启用反斜杠转义序列,允许使用特殊字符进行格式化输出。

常见的转义字符:  
\n:换行符,表示换到下一行输出。  
\t:制表符,插入水平制表符(通常是一个缩进)。  
\\:反斜杠自身。  

没有-e的话就出现下面结果

You need to specify the target domain.\n

接下来的代码是两个function,实际上最后执行的。那么接下来运行的是

# Identify IP address of the specified domain
hosts=$(host $domain | grep "has address" | cut -d" " -f4 | tee discovered_hosts.txt)

echo -e "Discovered IP address:\n$hosts\n"
ipaddr=$(host $domain | grep "has address" | cut -d" " -f4 | tr "\n" " ")

第一部分:查找域名的 IP 地址并存储到文件

host $domain

这是一个 DNS 查询命令,用于查询域名的 DNS 记录。
假设 $domain 是 google.com,执行后可能返回.

┌──(root㉿kali)-[~/Desktop]
└─# host baidu.com                                                          
baidu.com has address 39.156.66.10
baidu.com has address 110.242.68.66
baidu.com mail is handled by 20 usmx01.baidu.com.
baidu.com mail is handled by 20 jpmx.baidu.com.
baidu.com mail is handled by 15 mx.n.shifen.com.
baidu.com mail is handled by 10 mx.maillb.baidu.com.
baidu.com mail is handled by 20 mx1.baidu.com.
baidu.com mail is handled by 20 mx50.baidu.com.

grep "has address"

通过 grep 命令,过滤出包含 "has address" 的行,这些行显示域名的 IP 地址。
输出类似于:

┌──(root㉿kali)-[~/Desktop]
└─# host baidu.com | grep "has address"
baidu.com has address 110.242.68.66
baidu.com has address 39.156.66.10

cut -d" " -f4

cut 命令用于提取输出的第 4 列。
-d" " 表示以空格作为列的分隔符,-f4 表示提取第 4 列,即 IP 地址。
经过这一步,提取到的输出将是:

┌──(root㉿kali)-[~/Desktop]
└─# host baidu.com | grep "has address" | cut -d" " -f4 
110.242.68.66
39.156.66.10

tee discovered_hosts.txt

tee 命令可以将结果同时输出到屏幕和文件。
在这里,它会将上一步提取到的 IP 地址保存到文件 discovered_hosts.txt 中,同时显示在终端。
文件 discovered_hosts.txt 的内容将是:

┌──(root㉿kali)-[~/Desktop]
└─# host baidu.com | grep "has address" | cut -d" " -f4 | tee discover.txt 
110.242.68.66
39.156.66.10

┌──(root㉿kali)-[~/Desktop]
└─# cat discover.txt                                                      
110.242.68.66
39.156.66.10

hosts=$(...)

将所有的 IP 地址存储到变量 hosts 中,方便后续使用。
变量 hosts 将包含:

┌──(root㉿kali)-[~/Desktop]
└─# hosts=$(host baidu.com | grep "has address" | cut -d" " -f4 | tee discovered_hosts.txt)
printf $hosts
39.156.66.10
110.242.68.66

第二部分:显示发现的 IP 地址

echo -e
-e 启用反斜杠转义字符的解释功能(例如,换行符 \n)。
\n$hosts\n:
\n 表示换行。
输出结果是:Discovered IP address: 后面跟着存储在变量 hosts 中的 IP 地址,然后再次换行。

┌──(root㉿kali)-[~/Desktop]
└─# hosts=$(host baidu.com | grep "has address" | cut -d" " -f4 | tee discovered_hosts.txt)
echo -e "Discovered IP address:\n$hosts\n"
Discovered IP address:
110.242.68.66
39.156.66.10

第三部分:获取所有 IP 地址并存储为单行

host $domain | grep "has address" | cut -d" " -f4

这部分与前面的逻辑相同,查询域名的 IP 地址并提取第 4 列 IP 地址。

tr "\n" " "

tr 命令用于替换或删除字符。

这里的 tr "\n" " " 表示将每个换行符(\n)替换为空格。
因为 host 命令的输出可能会有多个 IP 地址,并且每个 IP 地址在新的一行,通过 tr 可以将它们转换成单行输出,以空格分隔。

┌──(root㉿kali)-[~/Desktop]
└─# host baidu.com | grep "has address" | cut -d" " -f4 | tr "\n" " "     
110.242.68.66 39.156.66.10

最后将其赋值给新的变量

┌──(root㉿kali)-[~/Desktop]
└─# ipaddrs=$(host baidu.com | grep "has address" | cut -d" " -f4 | tr "\n" " ")
printf $ipaddrs       
110.242.68.66 39.156.66.10 

然后执行的是 最后面的代码

# Available options
echo -e "Additional options available:"
echo -e "\t1) Identify the corresponding network range of target domain."
echo -e "\t2) Ping discovered hosts."
echo -e "\t3) All checks."
echo -e "\t*) Exit.\n"

read -p "Select your option: " opt

case $opt in
    "1") network_range ;;
    "2") ping_host ;;
    "3") network_range && ping_host ;;
    "*") exit 0 ;;
esac

第一部分是显示可用选项

echo -e "Additional options available:"
echo -e "\t1) Identify the corresponding network range of target domain."
echo -e "\t2) Ping discovered hosts."
echo -e "\t3) All checks."
echo -e "\t*) Exit.\n"

打印这么多条

echo -e:使用 -e 选项启用转义字符(如 \n 和 \t),来格式化输出。

\t:表示插入一个水平制表符,相当于缩进效果。
\n:表示换行。

Additional options available:
    1) Identify the corresponding network range of target domain.
    2) Ping discovered hosts.
    3) All checks.
    *) Exit.

这个部分展示给用户一系列可供选择的操作:
1:识别目标域名的网络范围。
2:Ping 已发现的主机。
3:执行所有的检查(网络范围识别和 Ping 操作)。
*:退出程序。

第二部分,读取用户的选项输入

read -p "Select your option: " opt

read -p "Select your option: ":read 命令用来读取用户输入的值,并将输入内容保存到变量 opt 中。

-p 选项用于在等待输入前显示提示信息(这里提示信息是 Select your option: )。

这里把用户输入的值 赋值给了opt变量

[!NOTE]
这里注意, 在 Bash 中,变量的使用有两种方式:定义时不需要使用 \$ 符号,但在引用(或访问)变量的值时,需要使用 \$ 来表示

[!NOTE]
而之前hosts=$(command)命令替换允许你运行一个命令,并将它的输出结果作为一个值赋给变量。在 Bash 中,命令替换的语法是:

使用反引号(\command\
或者使用 $() 语法(推荐,因为更清晰易读)

第三部分 基于用户选择执行对应操作

case $opt in
    "1") network_range ;;
    "2") ping_host ;;
    "3") network_range && ping_host ;;
    "*") exit 0 ;;
esac

case $opt in ... esac:这是 case 语句的语法,用来匹配用户输入的值,并执行相应的命令。

它用于根据一个变量的值(在这里是 opt)执行不同的操作。case 语句类似于其他编程语言中的 switch 语句,用于处理多重分支情况。接下来我们详细讲解其语法和工作原理。

*:这是通配符,表示匹配所有其他未列出的情况,相当于 default 分支。

case 变量值 in
  模式1)
    命令1
    ;;
  模式2)
    命令2
    ;;
  模式3)
    命令3
    ;;
  *)
    默认命令
    ;;
esac

case 语句中的模式匹配不仅可以使用简单的字符串,还可以使用通配符和正则表达式的简化形式:

?:匹配任意单个字符。
*:匹配任意长度的字符。
[abc]:匹配字符 a、b 或 c。

case $var in
  1) echo "You chose 1" ;;
  [2-4]) echo "You chose 2, 3, or 4" ;;
  *) echo "Invalid choice" ;;
esac

虽然 case 中的模式通常不需要引号,但有些情况下使用引号可能是必要的,尤其是在模式中包含特殊字符或空格时。

例如:

带空格或者特殊字符的模式: 如果模式中包含空格,你需要使用引号,否则 Bash 会认为空格分隔了多个模式。

case $var in
  "one option") echo "You chose 'one option'" ;;
  "another option") echo "You chose 'another option'" ;;
  *) echo "Invalid choice" ;;
esac

[!NOTE]
在 Bash 脚本中,exit 命令用于终止脚本的执行,并返回一个退出状态码(也叫做退出码)。退出码可以帮助调用脚本的其他程序或用户了解脚本的执行状态。

退出码的含义:
exit 0:表示脚本正常结束,成功完成任务。0 是一个通用的成功状态码,表示程序无错误执行。
非零的退出码(如 exit 1、exit 2 等):通常表示脚本执行时发生了某种错误。不同的非零值可以用于区分不同的错误类型。

如果用户在之前的脚本中选择了选项 1 或 3,会调用 network_range 函数

function network_range {
    for ip in $ipaddr
    do
        # 通过 whois 工具查询每个 IP 地址的网络范围或 CIDR 信息
        netrange=$(whois $ip | grep "NetRange\|CIDR" | tee -a CIDR.txt)

        # 进一步从 whois 输出中提取 CIDR
        cidr=$(whois $ip | grep "CIDR" | awk '{print $2}')

        # 使用 prips 工具生成该 CIDR 范围内的所有 IP 地址
        cidr_ips=$(prips $cidr)

        # 打印当前 IP 的网络范围信息
        echo -e "\nNetRange for $ip:"
        echo -e "$netrange"
    done
}

这个函数的目的是:

通过 whois 查询每个 IP 地址的网络范围。
从 whois 查询结果中提取网络范围信息(NetRange 或 CIDR)。
使用 prips 工具生成该 CIDR 范围内的所有 IP 地址。
输出 IP 地址对应的网络范围,并将相关信息保存到文件 CIDR.txt。

for ip in $ipaddr:

ipaddr 是一个包含所有已经发现 IP 地址的变量(可能由 host 命令生成)。
for 循环会遍历 ipaddr 中的每一个 IP 地址,并对每个 IP 地址执行后续操作。

for 循环是 Bash 中的一个重要控制结构,它用于遍历一组元素(例如字符串、数字、文件名或变量的值),并对每个元素执行某些操作。在 Bash 脚本中,for 循环的语法非常灵活,能够高效处理各种集合。

for var in list
do
  commands
done

var:循环变量,代表当前遍历的每个元素。
list:可以是一个由空格或换行符分隔的字符串、数组、命令输出等。
commands:循环体中要执行的命令,对每个 list 中的元素执行这些命令。
done:表示 for 循环的结束。

netrange=$(whois $ip | grep "NetRange|CIDR" | tee -a CIDR.txt):

使用 whois 命令查询该 IP 的详细信息

┌──(root㉿kali)-[~/Desktop]
└─# whois 142.251.221.78

#
# ARIN WHOIS data and services are subject to the Terms of Use
# available at: https://www.arin.net/resources/registry/whois/tou/
#
# If you see inaccuracies in the results, please report at
# https://www.arin.net/resources/registry/whois/inaccuracy_reporting/
#
# Copyright 1997-2024, American Registry for Internet Numbers, Ltd.
#

NetRange:       142.250.0.0 - 142.251.255.255
CIDR:           142.250.0.0/15
NetName:        GOOGLE
NetHandle:      NET-142-250-0-0-1
Parent:         NET142 (NET-142-0-0-0-0)
...

grep 筛选出包含 "NetRange" 或 "CIDR" 字样的行。

grep 是一个用于文本搜索和处理的命令行工具,在 Bash 脚本和命令行中广泛使用。它可以从输入中筛选出匹配特定模式的行,并输出这些行。

grep [选项] "模式" 文件名

"模式":要搜索的字符串或正则表达式。
文件名:要搜索的文件(可以是标准输入)。

grep 从 whois 命令的输出中筛选出包含 "NetRange" 或 "CIDR" 的行。
"NetRange|CIDR" 是一个使用 |(管道)符号的正则表达式,表示逻辑“或”操作。意思是:
如果一行包含 "NetRange",或者
如果一行包含 "CIDR"
这样可以同时查找包含这两个关键字的行。

NetRange 通常表示 IP 地址的范围,而 CIDR 则表示无类别域间路由 (Classless Inter-Domain Routing) 的网络标识符。

tee -a CIDR.txt 追加保存到 CIDR.txt 文件中。

-a(append):这个选项表示以追加的方式写入文件,而不是覆盖文件的内容。

| awk '{print $2}'

awk:这是一个强大的文本处理工具,通常用于提取特定列或进行复杂的文本操作。
'{print $2}':这是 awk 的一个命令,表示打印出当前行的第二列。由于在 CIDR 行中,第二列通常是 CIDR 地址(如 192.168.0.0/16),这个命令会提取并输出该地址。

awk '条件 {动作}' 文件名

字段(Field):awk 默认将每一行分为多个字段,字段之间由空格或制表符(tab)分隔。可以通过 $1, $2, $3 等方式访问这些字段,其中 $1 是第一字段,$2 是第二字段,以此类推。
记录(Record):awk 将每一行视为一条记录,通常记录的默认分隔符是换行符(\n)。

打印字段:

awk '{print $1}' 文件名

这条命令将打印每一行的第一个字段。

使用分隔符: 可以使用 -F 选项自定义字段分隔符:

awk -F',' '{print $1}' 文件名

这将以逗号为分隔符,打印每行的第一个字段。

条件匹配:

awk '$3 > 100 {print $1, $2}' 文件名

这条命令将打印第三字段大于 100 的记录的第一和第二字段。

模式匹配:

awk '/pattern/ {print $0}' 文件名

这将打印所有包含 "pattern" 的行。

内置变量:

NR:当前记录的行号。
NF:当前记录的字段数。
$0:表示整行内容。

prips:这是一个命令行工具,用于生成指定 CIDR 范围内的所有 IP 地址。

假设 cidr 变量的值为 192.168.1.0/30,那么执行 prips $cidr 将产生以下输出:

192.168.1.0
192.168.1.1
192.168.1.2
192.168.1.3

再就是最后一个函数

# Ping discovered IP address(es)
function ping_host {
    hosts_up=0
    hosts_total=0

    echo -e "\nPinging host(s):"
    for host in $cidr_ips
    do
        stat=1
        while [ $stat -eq 1 ]
        do
            ping -c 2 $host > /dev/null 2>&1
            if [ $? -eq 0 ]
            then
                echo "$host is up."
                ((stat--))
                ((hosts_up++))
                ((hosts_total++))
            else
                echo "$host is down."
                ((stat--))
                ((hosts_total++))
            fi
        done
    done

    echo -e "\n$hosts_up out of $hosts_total hosts are up."
}

这个函数调用了上一个函数的2个变量,假如不执行那个而是直接执行该函数一定会出错。

stat=1

初始化状态变量 stat,用于控制 ping 的重试。

while [ $stat -eq 1 ]
do
    ping -c 2 $host > /dev/null 2>&1

使用 while 循环,如果 stat 等于 1,则执行 ping 命令。

ping -c 2 $host 表示对当前主机发送 2 个 ICMP 请求。

> /dev/null 2>&1 将标准输出和标准错误重定向到 /dev/null,这样就不会在终端上显示 ping 命令的输出。

if [ $? -eq 0 ]
then
    echo "$host is up."
    ((stat--))
    ((hosts_up++))
    ((hosts_total++))
else
    echo "$host is down."
    ((stat--))
    ((hosts_total++))
fi

$? 检查上一个命令(即 ping 命令)的退出状态。状态为 0 表示 ping 成功,主机是活动的。

如果主机是活动的,输出主机地址,更新存活主机计数 hosts_up 和总主机计数 hosts_total。

如果主机不响应 ping,输出主机地址,并仅更新总主机计数 hosts_total

在 Bash 脚本中,-eq 和 == 都是用于比较值的,但它们的用法和适用场景是不同的。

-eq 和 == 的区别

-eq:

用于整数比较。
语法:[ "$a" -eq "$b" ] 或 if [ "$a" -eq "$b" ]; then ...

if [ $x -eq 5 ]; then
    echo "x is equal to 5."
fi

==:

用于字符串比较,通常在 [[ ]] 或 [ ] 中使用。
语法:[[ "$a" == "$b" ]] 或 [ "$a" == "$b" ]

if [[ "$name" == "Alice" ]]; then
    echo "Hello, Alice!"
fi

Working Component- Conditional Execution

Conditional execution allows us to control the flow of the script by reaching different conditions.

When defining various conditions, we specify which functions or sections of code should be executed for a specific value. If we reach a specific condition, only the code for that condition is executed, and the others are skipped. As soon as the code section is completed, the following commands will be executed outside the conditional execution.

!#/bin/bash

# Check for given argument

if [ $# -eq 0]
then
    echo -e "You need to specify the target domain"
    echo -e "Usage:"
    echo -e "\t$0 <domain>"
else
    domain=$1
fi

...

In summary, this code section works with the following components:

  • #!/bin/bash - Shebang.
  • if-else-fi - Conditional execution.
  • echo - Prints specific output.
  • $# / $0 / $1 - Special variables.
  • domain - Variables.

The conditions of the conditional executions can be defined using variables ($#, $0, $1, domain), values (0), and strings, as we will see in the next examples. These values are compared with the comparison operators (-eq) that we will look at in the next section.

Shebang

The shebang line is always at the top of each script and always starts with "#!". This line contains the path to the specified interpreter(/bin/bash) with which the script is executed.

We can also use Shebang to define other interpreters like Python, Perl, and others.

#!/usr/bin/env python
#!/usr/bin/env perl

If-Else-Fi

Checking of conditions usually has two different forms in programming and scripting languages, the if-else condition and case statement.

Pseudo-Code

#!/bin/bash

if [ the number of given arguments equals 0 ]
then
    Print: "You need to specify the target domain."
    Print: "<empty line>"
    Print: "Usage:"
    Print: "   <name of the script> <domain>"
    Exit the script with an error
else
    The "domain" variable serves as the alias for the given argument 
finish the if-condition

If-only.sh

#!/bin/bash

value=$1

if [ "$value" -gt "10" ]
then
    echo "Given argument is greater than 10"
fi
┌──(root㉿kali)-[~/Desktop]
└─# ./if-only.sh 5     

┌──(root㉿kali)-[~/Desktop]
└─# ./if-only.sh 12
Given argument is greater than 10.

[!NOTE]
value=$1 rather than value= $1
[ $value -gt "10" ] rather than [$value -gt "10"]

value= $1  # 这会导致语法错误,因为 `=` 号两边不能有空格

空格:在 Bash 中,[ ] 是条件判断运算符,里面的内容两边必须有空格。

引号:为了防止 value 没有值时导致脚本报错,最好将变量用双引号包裹起来。这样即使 $value 为空,也不会导致脚本出错。

数字比较:当你使用 -gt, -lt, -eq 等运算符时,Bash 期望左右两边的值是整数。因此,像 10 这样的整数不需要引号。Bash 会自动将它识别为数字并进行数学比较。

字符串比较:如果你使用字符串比较运算符(如 = 或 !=),Bash 会将两边的内容当作字符串进行比较,此时需要引号来明确字符串的边界。

If-Elif-Else.sh

When adding elif or else, we add alternatives to treat specific values or statuses.

So, the script shoule be updated to this

#!/bin/bash

value=$1

if [ "$value" -gt 10 ]
then
    echo "Given argument is greater than 10."
elif [ "$value" -eq 10 ]
then
    echo "Given argument is equal to 10"
else
    echo "Given argument is less than 10"
fi

[!NOTE]
两种写法是一个效果,同一行需要加;
if []; then
if []
then

[!NOTE]
没有else if 而是elif

┌──(root㉿kali)-[~/Desktop]
└─# ./if-only.sh 12    
Given argument is greater than 10.

┌──(root㉿kali)-[~/Desktop]
└─# ./if-only.sh 10
Given argument is equal to 10

┌──(root㉿kali)-[~/Desktop]
└─# ./if-only.sh 9 
Given argument is less than 10

Several Conditional - Script.sh

#!/bin/bash

# Check for given argument
if [ $# -eq 0 ]
then
    echo -e "You need to specify the target domain.\n"
    echo -e "Usage:"
    echo -e "\t$0 <domain>"
    exit 1
elif [ $# -eq 1 ]
then
    domain=$1
else
    echo -e "Too many arguments given."
    exit 1
fi

<SNIP>

Here we define another condition (elif [\<condition>];then) that prints a line telling us (echo -e "...") that we have given more than one argument and exits the program with an error (exit 1).

exit 命令用于退出脚本或函数,并向调用者返回一个退出状态码。
exit 0 表示正常退出,没有遇到错误。
exit 1(或其他非零状态码)表示脚本中出现了错误,通常由系统或调用者检测来采取不同的行动。例如,某些程序可以根据退出状态码决定下一步动作。

$# 是一个特殊的位置参数,表示传递给脚本的参数个数。

如果你执行 ./script.sh arg1 arg2,那么 $# 的值就是 2,因为有两个参数 arg1 和 arg2。

如果你执行 ./script.sh 而没有传递参数,$# 的值是 0。

Exercise Script

#!/bin/bash
# Count number of characters in a variable:
#     echo $variable | wc -c

# Variable to encode
var="nef892na9s1p9asn2aJs71nIsm"

for counter in {1..40}
do
        var=$(echo $var | base64)
done

Create an "If-Else" condition in the "For"-Loop of the "Exercise Script" that prints you the number of characters of the 35th generated value of the variable "var".

#!/bin/bash
# Count number of characters in a variable:
#     echo $variable | wc -c

# Variable to encode
var="nef892na9s1p9asn2aJs71nIsm"

for counter in {1..40}
do
        var=$(echo $var | base64)

        if [ "$counter" -eq 35 ]
        then
            echo $var | wc -c 
        fi
done

Working Component - Arguments, Variables, and Arrays

Arguments

We can always pass up to 9 arguments($0 - $9) to the script without assigning them to variables or setting the corresponding requirements for these. 9 arguments because the first argument $0 is reserved for the script.

chaostudy@htb[/htb]$ ./script.sh ARG1 ARG2 ARG3 ... ARG9
       ASSIGNMENTS:       $0      $1   $2   $3 ...   $9

So,
$0=./script.sh
$1=ARG1
...

There are several ways how we can execute our script. However, we must first set the script's execution privileges before executing it with the interpreter defined in it.

CIDR.sh - Set Execution Privileges

chaostudy@htb[/htb]$ chmod +x cidr.sh

CIDR.sh - Execution without Arguments

chaostudy@htb[/htb]$ ./cidr.sh

You need to specify the target domain.

Usage:
    cidr.sh <domain>

CIDR.sh - Execution without Execution permissions

chaostudy@htb[/htb]$ bash cidr.sh

You need to specify the target domain.

Usage:
    cidr.sh <domain>

Special Variables

Special variables use the Internal Field Separator (IFS) to identify when an argument ends and the next begins. Bash provides various special variables that assist whild scripting.

IFS Description
$# This variable holds the number of arguments passed to the script.
$@ This variable can be used to retrieve the list of command-line arguments.
$* This variable can be used to retrieve all arguments.
$n Each command-line argument can be selectively retrieved using its position. For example, the first argument is found at $1.
$$ The process ID of the currently executing process.
$? The exit status of the script. This variable is useful to determine a command's success. The value 0 represents successful execution, while 1 is a result of a failure.

Here is an example for each argument:

#!/bin/bash

# 输出脚本名称和进程 ID
echo "Script name: $0"
echo "Process ID: $$"

# 输出参数数量
echo "Number of arguments: $#"

# 如果没有提供参数,输出提示信息
if [ $# -eq 0 ]; then
    echo "No arguments provided. Usage: $0 arg1 arg2 ..."
    exit 1
fi

# 输出所有参数
echo "All arguments using '\$@':"
for arg in "$@"; do
    echo "Argument: $arg"
done

echo "All arguments using '\$*':"
for arg in $*; do
    echo "Argument: $arg"
done

# 输出特定位置的参数
for i in $(seq 1 $#); do
    eval "arg=\$$i"
    echo "Argument at position $i: $arg"
done

# 示例命令并检查其退出状态
ls /nonexistent_directory
status=$?
if [ $status -ne 0 ]; then
    echo "The last command failed with exit status: $status"
else
    echo "The last command succeeded."
fi

# 输出脚本结束信息
echo "Script execution completed."

Execute this script

./example.sh arg1 arg2 arg3
Script name: ./example.sh
Process ID: 12345
Number of arguments: 3
All arguments using '$@':
Argument: arg1
Argument: arg2
Argument: arg3
All arguments using '$*':
Argument: arg1
Argument: arg2
Argument: arg3
Argument at position 1: arg1
Argument at position 2: arg2
Argument at position 3: arg3
ls: cannot access '/nonexistent_directory': No such file or directory
The last command failed with exit status: 2
Script execution completed.

在 Bash 中,使用双引号包裹"$@"是为了确保在处理命令行参数时,保持每个参数的完整性。这是一个重要的细节,尤其是在参数中可能包含空格或其他特殊字符时。

./script.sh arg1 "arg2 with space" arg3

如果没有引号,使用$@时,Bash会将所有参数视为一个整体,按照空格拆分,从而可能导致参数分开。例如:

arg1
arg2 with space
arg3

使用$@(没有引号)时,输出会变成:

arg1
arg2
with
space
arg3

Variables

domain=$1

The assignment of varibales takes place without the dollor sign ($)

In contrast to other programming languages, there is no direct differentiation and recognition between the types of variables in Bash like "strings," "integers," and "boolean." All contents of the variables are treated as string characters. Bash enables arithmetic functions depending on whether only numbers are assigned or not.

[!NOTE] It is important to note when declaring variables that they do not contain a space. Otherwise, the actual variable name will be interpreted as an internal function or a command.

Declaring a Variable - Error

chaostudy@htb[/htb]$ variable = "this will result with an error."

command not found: variable

Declaring a Variable - Without an Error

chaostudy@htb[/htb]$ variable="Declared without an error."
chaostudy@htb[/htb]$ echo $variable

Declared without an error.

Arrays

There is also the possibility of assigning several values to a single variable in Bash. This can be beneficial if we want to scan multiple domains or IP addresses. These variables are called arrays that we can use to store and process an ordered sequence of specific type values.

Arrays identify each stored entry with an index starting with 0. When we want to assign a value to an array component, we do so in the same way as with standard shell variables. All we do is specify the field index enclosed in square brackets. The declaration for arrays looks like this in Bash:

Arrays.sh

#!/bin/bash

domains=(www.inlanefreight.com ftp.inlanefreight.com vpn.inlanefreight.com www2.inlanefreight.com)

echo ${domains[0]}

在访问数组元素时,花括号同样是必需的,尤其是在获取数组长度或访问数组索引时。这有助于清晰地标识数组变量的名称和其索引。

chaostudy@htb[/htb]$ ./Arrays.sh

www.inlanefreight.com

It is important to note that single quotes (' ... ') and double quotes (" ... ") prevent the separation by a space of the individual values in the array. This means that all spaces between the single and double quotes are ignored and handled as a single value assigned to the array.

So, three domain names are treated as one value

#!/bin/bash

domains=("www.inlanefreight.com ftp.inlanefreight.com vpn.inlanefreight.com" www2.inlanefreight.com)
echo ${domains[0]}
chaostudy@htb[/htb]$ ./Arrays.sh

www.inlanefreight.com ftp.inlanefreight.com vpn.inlanefreight.com

Working Componenet - Comparison Operators

To compare specific values with each other, we need elements that are called comparison operators. The comparison operators are used to determine how the defined values will be compared. For these operators, we differentiate between:

  • string operators
  • integer operators
  • file operators
  • boolean operators

String Operators

If we compare strings, then we know what we would like to have in the corresponding value.

Operator Description
== is equal to
!= is not equal to
< is less than in ASCII alphabetical order
> is greater than in ASCII alphabetical order
-z if the string is empty (null)
-n if the string is not null

It is important to note here that we put the variable for the given argument ($1) in double-quotes ("$1"). This tells Bash that the content of the variable should be handled as a string. Otherwise, we would get an error.

#!/bin/bash

# Check the given argument
if [ "$1" != "HackTheBox" ]
then
    echo -e "You need to give 'HackTheBox' as argument."
    exit 1

elif [ $# -gt 1 ]
then
    echo -e "Too many arguments given."
    exit 1

else
    domain=$1
    echo -e "Success!"
fi

This is an good example for all arguments:

#!/bin/bash

# 输入两个用户名进行比较
read -p "Enter the first username: " username1
read -p "Enter the second username: " username2

# 检查用户名是否为空
if [ -z "$username1" ]; then
    echo "First username is empty!"
    exit 1
fi

if [ -z "$username2" ]; then
    echo "Second username is empty!"
    exit 1
fi

# 检查两个用户名是否相等
if [ "$username1" == "$username2" ]; then
    echo "Both usernames are equal."
else
    echo "Usernames are different."
fi

# 检查用户名的字母顺序
if [[ "$username1" < "$username2" ]]; then
    echo "$username1 comes before $username2 in alphabetical order."
elif [[ "$username1" > "$username2" ]]; then
    echo "$username1 comes after $username2 in alphabetical order."
fi

# 检查用户名是否超过 5 个字符
if [ -n "$username1" ] && [ ${#username1} -gt 5 ]; then
    echo "First username is longer than 5 characters."
fi

if [ -n "$username2" ] && [ ${#username2} -gt 5 ]; then
    echo "Second username is longer than 5 characters."
fi

在 Bash 中,[[ ... ]] 是用于条件测试的增强型条件判断结构,提供了比 [ ... ] 更丰富的功能和更强的表达能力。需要双括号 [[ ... ]] 是因为它们支持更复杂的表达式,比如字符串比较、正则表达式匹配等。

支持字符串比较(如 < 和 >):

  • 使用 [ ... ] 时,< 和 > 会被解释为文件重定向操作,所以要进行字符串的比较时,必须使用 [[ ... ]]。
  • [[ ... ]] 中的 < 和 > 操作符可以用于比较两个字符串在 ASCII 字母表中的顺序,这在 [ ... ] 中是无法直接做到的。

同理,双括号可以简化条件判断

if [[ -n "$username1" && ${#username1} -gt 5 ]]; then
    echo "The username is not empty and its length is greater than 5."
fi

${#username1} 中的 # 是用于获取字符串的长度。在 Bash 中,${#variable} 是一种特殊的语法,用来返回变量中字符串的长度。

解释:
username1 是一个字符串变量。
${#username1} 是该字符串的长度。例如,如果 username1="Alice",那么 ${#username1} 的值将是 5,因为 "Alice" 有 5 个字符。

ASCII Table

String comparison operators "< / >" works only within the double square brackets [[ \<condition> ]]. We can find the ASCII table on the Internet or by using the following command in the terminal. We take a look at an example later.

chaostudy@htb[/htb]$ man ascii
Decimal Hexadecial Character Description
0 00 NUL End of a string
... ... ... ...
65 41 A Capital A
66 42 B Capital B
67 43 C Capital C
... ... ... ...
127 7F DEL Delete

ASCII stands for American Standard Code for Information Interchange and represents a 7-bit character encoding. Since each bit can take two values, there are 128 different bit patterns, which can also be interpreted as the decimal integers 0 - 127 or in hexadecimal values 00 - 7F. The first 32 ASCII character codes are reserved as so-called control characters.

Integer Operators

Comparing integer numbers can be very useful for us if we know what values we want to compare. Accordingly, we define the next steps and commands how the script should handle the corresponding value

Operator Description
-eq is equal to
-ne is not equal to
-lt is less than
-le is less than or equal to
-gt is greater than
-ge is greater than or equal to
#!/bin/bash

# Check the given argument
if [ $# -lt 1 ]
then
    echo -e "Number of given arguments is less than 1"
    exit 1

elif [ $# -gt 1 ]
then
    echo -e "Number of given arguments is greater than 1"
    exit 1

else
    domain=$1
    echo -e "Number of given arguments equals 1"
fi

File Operators

The file operators are useful if we want to find out specific permissions or if they exist.

Operator Description
-e if the file exist
-f tests if it is a file
-d tests if it is a directory
-L tests if it is if a symbolic link
-N checks if the file was modified after it was last read
-O if the current user owns the file
-G if the file’s group id matches the current user’s
-s tests if the file has a size greater than 0
-r tests if the file has read permission
-w tests if the file has write permission
-x tests if the file has execute permission
#!/bin/bash

# Check if the specified file exists
if [ -e "$1" ]
then
    echo -e "The file exists."
    exit 0

else
    echo -e "The file does not exist."
    exit 2
fi

exit 0 和 exit 2 都会退出脚本,但它们的区别在于 退出状态码。这个状态码对于调用该脚本的外部程序或其他脚本是非常重要的。

#!/bin/bash

./check_file.sh myfile.txt

if [ $? -eq 0 ]; then
    echo "The file check passed successfully."
else
    echo "There was an error with the file check."
fi

$? 是一个特殊的变量,它存储了最近执行命令的退出状态码。

如果 check_file.sh 脚本退出时返回 0,则输出 "The file check passed successfully."。

如果返回非 0,比如 exit 2,则会输出 "There was an error with the file check."。

Boolean and Logical Operators

We get a boolean value "false" or "true" as a result with logical operators. Bash gives us the possibility to compare strings by using double square brackets [[ \<condition> ]]. To get these boolean values, we can use the string operators. Whether the comparison matches or not, we get the boolean value "false" or "true".

#!/bin/bash

# Check the boolean value
if [[ -z $1 ]]
then
    echo -e "Boolean value: True (is null)"
    exit 1

elif [[ $# > 1 ]]
then
    echo -e "Boolean value: True (is greater than)"
    exit 1

else
    domain=$1
    echo -e "Boolean value: False (is equal to)"
fi

在 Bash 脚本中,exit 命令后面跟的数字(状态码)可以是任何整数。通常情况下:

exit 0 表示脚本成功执行,没有错误。

exit 1 通常用来表示脚本遇到了某种错误或失败的情况。

其他非 0 的退出码(如 exit 2, exit 3 等): 用来表示不同类型的错误,具体的含义取决于你的定义。

Logical Operators

With logical operators, we can define several conditions within one. This means that all the conditions we define must match before the corresponding code can be executed.

Operator Descriptio
! logical negotation NOT
&& logical AND
|| logical OR
#!/bin/bash

# Check if the specified file exists and if we have read permissions
if [[ -e "$1" && -r "$1" ]]
then
    echo -e "We can read the file that has been specified."
    exit 0

elif [[ ! -e "$1" ]]
then
    echo -e "The specified file does not exist."
    exit 2

elif [[ -e "$1" && ! -r "$1" ]]
then
    echo -e "We don't have read permission for this file."
    exit 1

else
    echo -e "Error occured."
    exit 5
fi

! -e "$1" 是一个条件表达式,用于检查文件是否 不存在。在 Bash 脚本中,! 是逻辑取反运算符,表示条件的否定。

-e "$1": 这个表达式检查 $1 所代表的文件是否存在。

!: 这个运算符取反它后面的条件。因此,! -e "$1" 表示“文件不存在”。

Exercise Script

#!/bin/bash

var="8dm7KsjU28B7v621Jls"
value="ERmFRMVZ0U2paTlJYTkxDZz09Cg"

for i in {1..40}
do
        var=$(echo $var | base64)

        #<---- If condition here:
done
#!/bin/bash

var="8dm7KsjU28B7v621Jls"
value="ERmFRMVZ0U2paTlJYTkxDZz09Cg"

for i in {1..40}
do
    var=$(echo "$var" | base64)  # 使用 -n 避免换行
    number=$(echo "$var" | wc -c)  # 计算字节数
        if [[ "$number" -gt 113450 && "$var" =~ "$value" ]]  # 确保不加引号以避免字面匹配
        then
            echo -e "This is $i round \n"
            echo -e "The number is $number \n"
            echo -e "Tha last 20 charaters are ${var: -20}"
        fi
done

echo "${var: -20}" 是一种字符串提取方式,用于从变量 var 中提取最后 20 个字符。下面是对这个表达式的详细解释:

${var: -20}

  • 这是 Bash 中的字符串扩展语法,用于从变量 var 中提取子字符串。
  • var 是变量名,存储要操作的字符串。
  • : -20 表示从字符串的末尾向前提取 20 个字符。
${var:position:length}

从变量 var 中正向取出前 10 个字符,可以这样做:

echo "${var:0:10}"

0 表示从字符串的开始位置(第一个字符)。
10 表示要截取的字符数量。

echo "${var: -6}"   # 输出: World!

负数表示从字符串的末尾开始倒数。
-10 意味着从字符串的最后 10 个字符开始取。

"$var" =~ "$value" 是 Bash 中的正则表达式匹配操作符,用于检查字符串 var 中是否包含字符串 value。

如果 var 包含 value 的内容(即使只是部分匹配),条件为 true,表达式返回 0。

如果 var 中不包含 value,条件为 false,表达式返回非 0 值。

Bash 中有很多与正则表达式匹配相关的操作符和模式,以下是一些常见的表达式和用法:

  1. 字符类
    • [abc]: 匹配字符 a、b 或 c。
  2. 量词
    • : 匹配零个或多个前面的元素。例如,ab 可以匹配 a、ab、abb 等。
    • +: 匹配一个或多个前面的元素。需要在 [[ ... ]] 中使用。例如,ab+ 可以匹配 ab、abb,但不匹配 a。
    • ?: 匹配零个或一个前面的元素。例如,ab? 可以匹配 a 或 ab。
  3. 边界符
    • ^: 匹配字符串的开始。例如,^abc 匹配以 abc 开头的字符串。
    • $: 匹配字符串的结束。例如,abc$ 匹配以 abc 结尾的字符串。
  4. 特殊字符
    • .: 匹配任意单个字符(除了换行符)。
    • \: 用于转义特殊字符。例如,. 匹配字面上的点(.)。
  5. 组合示例
    下面是一些更复杂的正则表达式示例:

示例 1: 检查字符串是否为十六进制数

hex="1A3F"
if [[ "$hex" =~ ^[0-9A-Fa-f]+$ ]]; then
    echo "字符串是一个有效的十六进制数。"
else
    echo "字符串不是有效的十六进制数。"
fi

示例 2: 检查字符串是否仅包含数字

number="12345"
if [[ "$number" =~ ^[0-9]+$ ]]; then
    echo "字符串只包含数字。"
else
    echo "字符串包含非数字字符。"
fi
正则表达式 ^[0-9A-Fa-f]+$ 的含义如下:

^: 表示字符串的开始。它确保匹配必须从字符串的开头开始。

[0-9A-Fa-f]: 这是一个字符类,表示可以匹配任何数字 (0-9) 或字母字符 (A-F 或 a-f)。这意味着可以匹配十六进制数字的有效字符。

0-9 匹配任何数字字符。
A-F 匹配大写字母 A 到 F。
a-f 匹配小写字母 a 到 f。
+: 量词,表示前面的字符类 [0-9A-Fa-f] 至少出现一次或多次。这意味着该字符串必须包含一个或多个有效的十六进制字符。

$: 表示字符串的结束。它确保匹配必须到达字符串的末尾。

Working Component - Arithmetic

In Bash, we have seven different arithmetic operators we can work with. These are used to perform different mathematical operations or to modify certain integers.

Arithmetic Operators

Operator Description
+ Addition
- Substraction
* Multiplication
/ Division
% Modulus
variable++ Increase the value of the variable by 1
variable-- Decrease the value of the variable by 1
#!/bin/bash

increase=1
decrease=1

echo "Addition: 10 + 10 = $((10 + 10))"
echo "Substraction: 10 - 10 = $((10 - 10))"
echo "Multiplication: 10 * 10 = $((10 * 10))"
echo "Division: 10 / 10 = $((10 / 10))"
echo "Modulus: 10 % 4 = $((10 % 4))"

((increase++))
echo "Increase Variable: $increase"

((decrease--))
echo "Decrease Variable: $decrease"

在 Bash 脚本中,双括号 ((...)) 用于执行算术运算。具体原因如下:

算术计算:双括号 ((...)) 是 Bash 的一种语法结构,专门用于进行算术表达式的计算。它可以对整数执行加法、减法、乘法、除法、取模、递增等运算。例如:

((10 + 10)) 表示 10 加 10 的计算。
((increase++)) 表示对变量 increase 进行递增。
递增/递减操作:在双括号中使用 ++ 或 --,可以像其他编程语言(如 C、C++)一样,对变量进行递增或递减操作。((increase++)) 将 increase 的值加 1,而 ((decrease--)) 将 decrease 的值减 1。

避免使用外部命令:与其他算术表达式方法(如 expr 命令)不同,((...)) 直接在 Bash 内部执行算术运算,效率更高且不需要调用外部命令。

支持复杂表达式:((...)) 可以支持较复杂的表达式和运算符组合,例如 ((a + b * c / d))。

Arithmetic.sh -Execution

chaostudy@htb[/htb]$ ./Arithmetic.sh

Addition: 10 + 10 = 20
Substraction: 10 - 10 = 0
Multiplication: 10 * 10 = 100
Division: 10 / 10 = 1
Modulus: 10 % 4 = 2
Increase Variable: 2
Decrease Variable: 0

We can also calculate the length of the variable. Using this function ${#variable}, every character gets counted, and we get the total number of characters in the variable.

Varlength.sh

htb="HackTheBox"

echo ${#htb}

VarLength.sh

chaostudy@htb[/htb]$ ./VarLength.sh

10

If we look at our CIDR.sh script, we will see that we have used the increase and decrease operators several times. This ensures that the while loop, which we will discuss later, runs and pings the hosts while the variable "stat" has a value of 1. If the ping command ends with code 0 (successful), we get a message that the host is up and the "stat" variable, as well as the variables "hosts_up" and "hosts_total" get changed.

<SNIP>
    echo -e "\nPinging host(s):"
    for host in $cidr_ips
    do
        stat=1
        while [ $stat -eq 1 ]
        do
            ping -c 2 $host > /dev/null 2>&1
            if [ $? -eq 0 ]
            then
                echo "$host is up."
                ((stat--))
                ((hosts_up++))
                ((hosts_total++))
            else
                echo "$host is down."
                ((stat--))
                ((hosts_total++))
            fi
        done
    done
<SNIP>

Script Control - Input and output Control

Input Control

We may get results from our sent requests and executed commands, which we have to decide manually on how to proceed. Another example would be that we have defined several functions in our script designed for different scenarios. We have to decide which of them should be executed after a manual check and based on the results. It is also quite possible that specific scans or activities may not be allowed to be performed. Therefore, we need to be familiar with how to get a running script to wait for our instructions. If we look at our CIDR.sh script again, we see that we have added such a call to decide further steps.

CIDR.sh

# Available options
<SNIP>
echo -e "Additional options available:"
echo -e "\t1) Identify the corresponding network range of target domain."
echo -e "\t2) Ping discovered hosts."
echo -e "\t3) All checks."
echo -e "\t*) Exit.\n"

read -p "Select your option: " opt

case $opt in
    "1") network_range ;;
    "2") ping_host ;;
    "3") network_range && ping_host ;;
    "*") exit 0 ;;
esac

The first echo lines serve as a display menu for the options available to us. With the read command, the line with "Select your option:" is displayed, and the additional option -p ensures that our input remains on the same line. Our input is stored in the variable opt, which we then use to execute the corresponding functions with the case statement, which we will look at later. Depending on the number we enter, the case statement determines which functions are executed.

在 Bash 中,echo 和 printf 都用于向终端输出文本,但它们在功能、灵活性和用法上存在一些重要区别:

  1. 基本功能
    echo:简单、快捷,适合输出基本文本内容,自动在输出末尾添加换行符。
    printf:更强大,类似于 C 语言的 printf,允许格式化输出,且不会自动添加换行符。
  2. 换行符
    echo:默认会在输出的末尾自动添加一个换行符,除非使用 -n 选项禁用换行。
echo "Hello, World!"
# 输出: Hello, World!
# 换行符自动添加

echo -n "Hello, World!"
# 输出: Hello, World!(无换行)

printf "Hello, World!"
# 输出: Hello, World!(无换行)

printf "Hello, World!\n"
# 输出: Hello, World! 并换行
  1. 格式化输出
    echo:只能简单地输出文本,不支持格式化参数。
    printf:支持格式化字符串,可以输出数字、字符串、浮点数等,类似于其他编程语言的 printf。
printf "Name: %s, Age: %d\n" "Alice" 30
# 输出: Name: Alice, Age: 30
  1. 转义字符处理
    echo:转义字符的支持取决于系统和实现,有时需要使用 -e 选项启用转义字符解析。
echo -e "Hello\tWorld\n"
# 输出: Hello    World (带制表符和换行符)

printf "Hello\tWorld\n"
# 输出: Hello    World (带制表符和换行符)

Output Control

We have already learned about the output redirections of output in the Linux Fundamentals module. Nevertheless, the problem with the redirections is that we do not get any output from the respective command. It will be redirected to the appropriate file. If our scripts become more complicated later, they can take much more time than just a few seconds. To avoid sitting inactively and waiting for our script's results, we can use the tee utility. It ensures that we see the results we get immediately and that they are stored in the corresponding files. In our CIDR.sh script, we have used this utility twice in different ways.

<SNIP>

# Identify Network range for the specified IP address(es)
function network_range {
    for ip in $ipaddr
    do
        netrange=$(whois $ip | grep "NetRange\|CIDR" | tee -a CIDR.txt)
        cidr=$(whois $ip | grep "CIDR" | awk '{print $2}')
        cidr_ips=$(prips $cidr)
        echo -e "\nNetRange for $ip:"
        echo -e "$netrange"
    done
}

<SNIP>

# Identify IP address of the specified domain
hosts=$(host $domain | grep "has address" | cut -d" " -f4 | tee discovered_hosts.txt)

<SNIP>

When using tee, we transfer the received output and use the pipe (|) to forward it to tee. The "-a / --append" parameter ensures that the specified file is not overwritten but supplemented with the new results. At the same time, it shows us the results and how they will be found in the file.

chaostudy@htb[/htb]$ cat discovered_hosts.txt CIDR.txt

165.22.119.202
NetRange:       165.22.0.0 - 165.22.255.255
CIDR:           165.22.0.0/16

Script Control - Flow Control - Loops

The control of the flow of our scripts is essential. We have already learned about the if-else conditions, which are also part of flow control. After all, we want our script to work quickly and efficiently, and for this, we can use other components to increase efficiency and allow error-free processing. Each control structure is either a branch or a loop. Logical expressions of boolean values usually control the execution of a control structure. These control structures include:

Branches:

  • If-Else Conditions
  • Case Statements

Loops:

  • For Loops
  • While Loops
  • Until Loops

For Loops

The For loop is executed on each pass for precisely one parameter, which the shell takes from a list, calculates from an increment, or takes from another data source. The for loop runs as long as it finds corresponding data. This type of loop can be structured and defined in different ways.

For example, the for loops are often used when we need to work with many different values from an array. This can be used to scan different hosts or ports. We can also use it to execute specific commands for known ports and their services to speed up our enumeration process. The syntax for this can be as follows:

Syntax - Examples

# 遍历一组数字
for variable in 1 2 3 4
do
    echo $variable
done

# 遍历几个文件
for variable in file1 file2 file3
do
    echo $variable
done

for ip in "10.10.10.170 10.10.10.174 10.10.10.175"
do
    ping -c 1 $ip
done

# 遍历一组数字
for i in {1..5}
do
    echo "Number: $i"
done

# 指定每次增长值为2
for i in {1..10..2}
do
    echo "Step: $i"
done

#遍历一个数组
array=("Linux" "Bash" "Scripting")

for element in "${array[@]}"
do
    echo "Element: $element"
done

#遍历一个目录
for file in /path/to/directory/*
do
    echo "File: $file"
done

#遍历一个命令的输出
for user in $(cat /etc/passwd | cut -d ":" -f 1)
do
    echo "User: $user"
done

#类似C语言风格的遍历
for ((i=1; i<=5; i++))
do
    echo "Counter: $i"
done

#利用break退出循环
for i in {1..5}
do
    if [ "$i" -eq 3 ]; then
        break  # 在 i=3 时退出循环
    fi
    echo "Number: $i"
done

#利用continue跳过当前迭代的遍历
for i in {1..5}
do
    if [ "$i" -eq 3 ]; then
        continue  # 跳过 i=3
    fi
    echo "Number: $i"
done

we can also write these commands in a single line. Such a command would look like this:

Remember ;

chaostudy@htb[/htb]$ for ip in 10.10.10.170 10.10.10.174;do ping -c 1 $ip;done

PING 10.10.10.170 (10.10.10.170): 56 data bytes
64 bytes from 10.10.10.170: icmp_seq=0 ttl=63 time=42.106 ms

--- 10.10.10.170 ping statistics ---
1 packets transmitted, 1 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 42.106/42.106/42.106/0.000 ms
PING 10.10.10.174 (10.10.10.174): 56 data bytes
64 bytes from 10.10.10.174: icmp_seq=0 ttl=63 time=45.700 ms

--- 10.10.10.174 ping statistics ---
1 packets transmitted, 1 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 45.700/45.700/45.700/0.000 ms

Let us have another look at our CIDR.sh script. We have added several for loops to the script, but let us stick with this little code section.

CIDR.sh

<SNIP>

# Identify Network range for the specified IP address(es)
function network_range {
    for ip in $ipaddr
    do
        netrange=$(whois $ip | grep "NetRange\|CIDR" | tee -a CIDR.txt)
        cidr=$(whois $ip | grep "CIDR" | awk '{print $2}')
        cidr_ips=$(prips $cidr)
        echo -e "\nNetRange for $ip:"
        echo -e "$netrange"
    done
}

<SNIP>

As in the previous example, for each IP address from the array "ipaddr" we make a "whois" request, whose output is filtered for "NetRange" and "CIDR." This helps us to determine which address range our target is located in. We can use this information to search for additional hosts during a penetration test, if approved by the client. The results that we receive are displayed accordingly and stored in the file "CIDR.txt."

While Loops

The while loop is conceptually simple and follows the following principle:

A statement is executed as long as a condition is fulfilled (true).

We can also combine loops and merge their execution with different values. It is important to note that the excessive combination of several loops in each other can make the code very unclear and lead to errors that can be hard to find and follow. Such a combination can look like in our CIDR.sh script.

CIDR.sh

<SNIP>
        stat=1
        while [ $stat -eq 1 ]
        do
            ping -c 2 $host > /dev/null 2>&1
            if [ $? -eq 0 ]
            then
                echo "$host is up."
                ((stat--))
                ((hosts_up++))
                ((hosts_total++))
            else
                echo "$host is down."
                ((stat--))
                ((hosts_total++))
            fi
        done
<SNIP>

The while loops also work with conditions like if-else. A while loop needs some sort of a counter to orientate itself when it has to stop executing the commands it contains. Otherwise, this leads to an endless loop. Such a counter can be a variable that we have declared with a specific value or a boolean value. While loops run while the boolean value is "True". Besides the counter, we can also use the command "break," which interrupts the loop when reaching this command like in the following example:

#!/bin/bash

counter=0

while [ $counter -lt 10 ]
do
  # Increase $counter by 1
  ((counter++))
  echo "Counter: $counter"

  if [ $counter == 2 ]
  then
    continue
  elif [ $counter == 4 ]
  then
    break
  fi
done
chaostudy@htb[/htb]$ ./WhileBreaker.sh

Counter: 1
Counter: 2
Counter: 3
Counter: 4

Until Loops

There is also theuntil loop, which is relatively rare. Nevertheless, theuntil loop works precisely like the while loop, but with the difference:

The code inside a until loop is executed as long as the particular condition is false.
The other way is to let the loop run until the desired value is reached. The "until" loops are very well suited for this. This type of loop works similarly to the "while" loop but, as already mentioned, with the difference that it runs until the boolean value is "False."

Until.sh

#!/bin/bash

counter=0

until [ $counter -eq 10 ]
do
  # Increase $counter by 1
  ((counter++))
  echo "Counter: $counter"
done
chaostudy@htb[/htb]$ ./Until.sh

Counter: 1
Counter: 2
Counter: 3
Counter: 4
Counter: 5
Counter: 6
Counter: 7
Counter: 8
Counter: 9
Counter: 10

Exercise Script


#!/bin/bash

# Decrypt function
function decrypt {
    MzSaas7k=$(echo $hash | sed 's/988sn1/83unasa/g')
    Mzns7293sk=$(echo $MzSaas7k | sed 's/4d298d/9999/g')
    MzSaas7k=$(echo $Mzns7293sk | sed 's/3i8dqos82/873h4d/g')
    Mzns7293sk=$(echo $MzSaas7k | sed 's/4n9Ls/20X/g')
    MzSaas7k=$(echo $Mzns7293sk | sed 's/912oijs01/i7gg/g')
    Mzns7293sk=$(echo $MzSaas7k | sed 's/k32jx0aa/n391s/g')
    MzSaas7k=$(echo $Mzns7293sk | sed 's/nI72n/YzF1/g')
    Mzns7293sk=$(echo $MzSaas7k | sed 's/82ns71n/2d49/g')
    MzSaas7k=$(echo $Mzns7293sk | sed 's/JGcms1a/zIm12/g')
    Mzns7293sk=$(echo $MzSaas7k | sed 's/MS9/4SIs/g')
    MzSaas7k=$(echo $Mzns7293sk | sed 's/Ymxj00Ims/Uso18/g')
    Mzns7293sk=$(echo $MzSaas7k | sed 's/sSi8Lm/Mit/g')
    MzSaas7k=$(echo $Mzns7293sk | sed 's/9su2n/43n92ka/g')
    Mzns7293sk=$(echo $MzSaas7k | sed 's/ggf3iunds/dn3i8/g')
    MzSaas7k=$(echo $Mzns7293sk | sed 's/uBz/TT0K/g')

    flag=$(echo $MzSaas7k | base64 -d | openssl enc -aes-128-cbc -a -d -salt -pass pass:$salt)
}

# Variables
var="9M"
salt=""
hash="VTJGc2RHVmtYMTl2ZnYyNTdUeERVRnBtQWVGNmFWWVUySG1wTXNmRi9rQT0K"

# Base64 Encoding Example:
#        $ echo "Some Text" | base64

# <- For-Loop here

# Check if $salt is empty
if [[ ! -z "$salt" ]]
then
    decrypt
    echo $flag
else
    exit 1
fi

Create a "For" loop that encodes the variable "var" 28 times in "base64". The number of characters in the 28th hash is the value that must be assigned to the "salt" variable.

#!/bin/bash

# Decrypt function
function decrypt {
    MzSaas7k=$(echo "$hash" | sed 's/988sn1/83unasa/g')
    Mzns7293sk=$(echo "$MzSaas7k" | sed 's/4d298d/9999/g')
    MzSaas7k=$(echo "$Mzns7293sk" | sed 's/3i8dqos82/873h4d/g')
    Mzns7293sk=$(echo "$MzSaas7k" | sed 's/4n9Ls/20X/g')
    MzSaas7k=$(echo "$Mzns7293sk" | sed 's/912oijs01/i7gg/g')
    Mzns7293sk=$(echo "$MzSaas7k" | sed 's/k32jx0aa/n391s/g')
    MzSaas7k=$(echo "$Mzns7293sk" | sed 's/nI72n/YzF1/g')
    Mzns7293sk=$(echo "$MzSaas7k" | sed 's/82ns71n/2d49/g')
    MzSaas7k=$(echo "$Mzns7293sk" | sed 's/JGcms1a/zIm12/g')
    Mzns7293sk=$(echo "$MzSaas7k" | sed 's/MS9/4SIs/g')
    MzSaas7k=$(echo "$Mzns7293sk" | sed 's/Ymxj00Ims/Uso18/g')
    Mzns7293sk=$(echo "$MzSaas7k" | sed 's/sSi8Lm/Mit/g')
    MzSaas7k=$(echo "$Mzns7293sk" | sed 's/9su2n/43n92ka/g')
    Mzns7293sk=$(echo "$MzSaas7k" | sed 's/ggf3iunds/dn3i8/g')
    MzSaas7k=$(echo "$Mzns7293sk" | sed 's/uBz/TT0K/g')

    flag=$(echo $MzSaas7k | base64 -d | openssl enc -aes-128-cbc -a -d -salt -pass pass:$salt)
}

# Variables
var="9M"
salt=""
hash="VTJGc2RHVmtYMTl2ZnYyNTdUeERVRnBtQWVGNmFWWVUySG1wTXNmRi9rQT0K"

for (( i=0; i<28; i++ ))
do
    var=$(echo "$var" | base64)
done

# The salt value is the number of characters of var after 28 times encrypting
salt=${#var}
echo -e "the value of salt is $salt in \${#var}"

salt=$(echo $var | wc -c)
echo -e "the value of salt is $salt in echo \$var | wc -c)"

# Check if $salt is empty
if [[ ! -z "$salt" ]]
then
    decrypt
    echo $flag
else
    exit 1
fi
┌──(root㉿kali)-[~/Desktop]
└─# ./forloop.sh       
the value of salt is 34070 in ${#var}
the value of salt is 34071 in echo $var | wc -c)
*** WARNING : deprecated key derivation used.
Using -iter or -pbkdf2 would be better.
HTBL00p5r0x

使用 ${#var}
该命令会输出 34070,因为字符串中确实有 8 个字符。

计算字符串中的字符数,不考虑换行符的字节数。
如果你在一个字符串中包含换行符,它会将其视为一个字符,而不是字节。

使用 wc -c
该命令会输出 34071,因为 wc -c 计算的是字节数。在这种情况下,字符串本身有 34070 个字符,加上输出的换行符(在命令结束后自动添加.

计算文件或输入中的字节数,包括所有字符、换行符、空格等。例如,如果文件中有换行符 \n,它会将其视为 1 个字节

var="你好"
length=${#var}
echo "使用 \${#var} 的长度:$length"  # 输出:使用 ${#var} 的长度:2

# 使用 wc -c
echo "$var" | wc -c  # 输出:6(因为 "你" 和 "好" 是 UTF-8 中的多字节字符)

# 在字符编码中,每个字符可以用一个或多个字节表示。例如:
# ASCII 编码:使用 1 字节表示一个字符(仅支持英文字符)。
# UTF-8 编码:对于基本的拉丁字符,使用 1 字节;对于某些 Unicode 字符(如中文),则可能使用 2 至 4 个字节。

Script Control - Flow Control - Branches

As we have already seen, the branches in flow control include if-else and the case statements. We have already discussed the if-else statements in detail and know how this works. Now we will take a closer look at the case statements.

Case Statements

Case statements are also known as switch-case statements in other languages, such as C/C++ and C#. The main difference between if-else and switch-case is that if-else constructs allow us to check any boolean expression, while switch-case always compares only the variable with the exact value. Therefore, the same conditions as for if-else, such as "greater-than," are not allowed for switch-case. The syntax for the switch-case statements looks like this:

case <expression> in
    pattern_1 ) statements ;;
    pattern_2 ) statements ;;
    pattern_3 ) statements ;;
esac

The definition of switch-case starts with case, followed by the variable or value as an expression, which is then compared in the pattern. If the variable or value matches the expression, then the statements are executed after the parenthesis and ended with a double semicolon (;;).

In our CIDR.sh script, we have used such a case statement. Here we defined four different options that we assigned to our script, how it should proceed after our decision.

<SNIP>
# Available options
echo -e "Additional options available:"
echo -e "\t1) Identify the corresponding network range of target domain."
echo -e "\t2) Ping discovered hosts."
echo -e "\t3) All checks."
echo -e "\t*) Exit.\n"

read -p "Select your option: " opt

case $opt in
    "1") network_range ;;
    "2") ping_host ;;
    "3") network_range && ping_host ;;
    "*") exit 0 ;;
esac
<SNIP>

With the first two options, this script executes different functions that we had defined before. With the third option, both functions are executed, and with any other option, the script will be terminated.

Execution Flow - Functions

The bigger our scripts get, the more chaotic they become. If we use the same routines several times in the script, the script's size will increase accordingly. In such cases, functions are the solution that improves both the size and the clarity of the script many times. We combine several commands in a block between curly brackets ( { ... }) and call them with a function name defined by us with functions. Once a function has been defined, it can be called and used again during the script.

Functions are an essential part of scripts and programs, as they are used to execute recurring commands for different values and phases of the script or program. Therefore, we do not have to repeat the whole section of code repeatedly but can create a single function that executes the specific commands. The definition of such functions makes the code easier to read and helps to keep the code as short as possible. It is important to note that functions must always be defined logically before the first call since a script is also processed from top to bottom. Therefore the definition of a function is always at the beginning of the script. There are two methods to define the functions:

function name {
    <commands>
}
name() {
    <commands>
}

We can choose the method to define a function that is most comfortable for us. In our CIDR.sh script, we used the first method because it is easier to read with the keyword "function."

CIDR.sh

<SNIP>
# Identify Network range for the specified IP address(es)
function network_range {
    for ip in $ipaddr
    do
        netrange=$(whois $ip | grep "NetRange\|CIDR" | tee -a CIDR.txt)
        cidr=$(whois $ip | grep "CIDR" | awk '{print $2}')
        cidr_ips=$(prips $cidr)
        echo -e "\nNetRange for $ip:"
        echo -e "$netrange"
    done
}
<SNIP>

The function is called only by calling the specified name of the function, as we have seen in the case statement.

<SNIP>
case $opt in
    "1") network_range ;;
    "2") ping_host ;;
    "3") network_range && ping_host ;;
    "*") exit 0 ;;
esac

Parameter Passing

Such functions should be designed so that they can be used with a fixed structure of the values or at least only with a fixed format. Like we have already seen in our CIDR.sh script, we used the format of an IP address for the function "network_range". The parameters are optional, and therefore we can call the function without parameters. In principle, the same applies to the passed parameters as to parameters passed to a shell script. These are $1 - $9 (\${n}), or $variable as we have already seen. Each function has its own set of parameters. So they do not collide with those of other functions or the parameters of the shell script.

An important difference between bash scripts and other programming languages is that all defined variables are always processed globally unless otherwise declared by "local." This means that the first time we have defined a variable in a function, we will call it in our main script (outside the function). Passing the parameters to the functions is done the same way as we passed the arguments to our script and looks like this:

PrintPars.sh

#!/bin/bash

function print_pars {
    echo $1 $2 $3
}

one="First parameter"
two="Second parameter"
three="Third parameter"

print_pars "$one" "$two" "$three"
chaostudy@htb[/htb]$ ./PrintPars.sh

First parameter Second parameter Third parameter

Return Values

When we start a new process, each child process (for example, a function in the executed script) returns a return code to the parent process (bash shell through which we executed the script) at its termination, informing it of the status of the execution. This information is used to determine whether the process ran successfully or whether specific errors occurred. Based on this information, the parent process can decide on further program flow.

Return Code Description
1 General errors
2 Misuse of shell builtins
126 Command invoked cannot execute
127 Command not found
128 Invalid argument to exit
128+n Fatal error signal "n"
130 Script terminated by Control-C
255* Exit status out of range

To get the value of a function back, we can use several methods like return, echo, or a variable. In the next example, we will see how to use "$?" to read the "return code," how to pass the arguments to the function and how to assign the result to a variable.

#!/bin/bash

function given_args {

        if [ $# -lt 1 ]
        then
                echo -e "Number of arguments: $#"
                return 1
        else
                echo -e "Number of arguments: $#"
                return 0
        fi
}

# No arguments given
given_args
echo -e "Function status code: $?\n"

# One argument given
given_args "argument"
echo -e "Function status code: $?\n"

# Pass the results of the funtion into a variable
content=$(given_args "argument")

echo -e "Content of the variable: \n\t$content"
chaostudy@htb[/htb]$ ./Return.sh

Number of arguments: 0
Function status code: 1

Number of arguments: 1
Function status code: 0

Content of the variable:
    Number of arguments: 1

Execution Flow - Debugging

Bash gives us an excellent opportunity to find, track, and fix errors in our code. The term debugging can have many different meanings. Nevertheless, Bash debugging is the process of removing errors (bugs) from our code. Debugging can be performed in many different ways. For example, we can use our code for debugging to check for typos, or we can use it for code analysis to track them and determine why specific errors occur.

This process is also used to find vulnerabilities in programs. For example, we can try to cause errors using different input types and track their handling in the CPU through the assembler, which may provide a way to manipulate the handling of these errors to insert our own code and force the system to execute it. This topic will be covered and discussed in detail in other modules. Bash allows us to debug our code by using the "-x" (xtrace) and "-v" options. Now let us see an example with our CIDR.sh script.

CIDR.sh - Debugging

chaostudy@htb[/htb]$ bash -x CIDR.sh

+ '[' 0 -eq 0 ']'
+ echo -e 'You need to specify the target domain.\n'
You need to specify the target domain.

+ echo -e Usage:
Usage:
+ echo -e '\tCIDR.sh <domain>'
    CIDR.sh <domain>
+ exit 1

Here Bash shows us precisely which function or command was executed with which values. This is indicated by the plus sign (+) at the beginning of the line. If we want to see all the code for a particular function, we can set the "-v" option that displays the output in more detail.

CIDR.sh - Verbose Debuggin

chaostudy@htb[/htb]$ bash -x -v CIDR.sh

#!/bin/bash

# Check for given argument
if [ $# -eq 0 ]
then
    echo -e "You need to specify the target domain.\n"
    echo -e "Usage:"
    echo -e "\t$0 <domain>"
    exit 1
else
    domain=$1
fi
+ '[' 0 -eq 0 ']'
+ echo -e 'You need to specify the target domain.\n'
You need to specify the target domain.

+ echo -e Usage:
Usage:
+ echo -e '\tCIDR.sh <domain>'
    CIDR.sh <domain>
+ exit 1

Chao

一个三天打鱼两天晒网的博主 拖延症严重患者 干啥啥不行,学啥啥不会