首先看一下原作者的一些思路,drops上有其前两部分的翻译: part 1 part 2 第三部分太监了,不过,也可能是审核没过,因为我看完这三篇文章,也是云里雾里的,特别是第三篇,原作者可能实力太屌,他简单提到的一些利用方式我研究了一段时间还是不能完全理解(然而他给出的post中没有任何细节,都是一个名词代过,真是装逼之大成)。我也试着把第三部分翻译了一下,不太懂英文的同学可以看下。part 3
之后下面我按照我的一些方式来实现对这个漏洞的利用,感谢龙哥提供的写堆栈方法。
原作者用的是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。
序列化数据结构如下:
之后我们要泄漏任意内存的话,只要构造一个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结构”,如下:
<?php $fakezval = pack( 'IIII', //unsigned int 0x08048000, //address to leak 0x0000000a, //length of string 0x00000000, //refcount 0x00000006 //data type);
这样我们只要在释放内存之后,立即申请一个假的zval,就可以重新使用这块内存,并读取任意地址了。
首先是确定大小端,之后是泄漏一个对象句柄的地址,这些在原作的part2部分已经有说明,不再赘述。 现在有一个对象句柄的地址了,之后干啥呢,要找到php库的基址。这个简单,只要找一个最小的句柄地址,往前搜就可以了,直到搜到elf的头部 \x7fELF ,这个地址就是基址了。
找到这个基址之后,就是根据elf的文件结构,(查看程序员的自我修养),找到动态节,string table,符号表。这样的话,你想要哪个函数,就在string table里搜这个函数名,之后用这个偏移在符号表里找到函数地址就可以了。真正用的时候,记得加上基址。
接下来到了原作的第三部分,我们现在要找的东西跟原作是一样的:
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を制御するだけですが、他の部分の値が間違っているとクラッシュが実行されるか、直接クラッシュするため、やはり復元する必要があります。オリジナルの作成者はパート 3 で、難読化のために PTR_MANGLE というマクロを使用するために glibc を使用していると説明しました。eip と esp を復元したい場合は、最初に set_jmp の戻りアドレスを見つける必要があります。これには、php_execute_script の関数アドレスを見つける必要があります。 jmp_buf をクラックする具体的な方法については、part3 の最後にある動画セクションで原作が説明しているので参照してください。
jmp_buf をクラックした後、eip を制御し、実行のために eval 関数にジャンプさせ、その後任意の PHP コードを実行できます。このメソッドは非常に安定しており、Apache をクラッシュさせません。
原作のこの部分では、パート 3 でメモリに直接書き込む方法が示されていますが、それは実際には少し難解であり、詳しくは説明されていませんでした。ここでは、比較的簡単にスタックを作成する方法を紹介します。
まず、PHP のメモリ キャッシュ ブロックについて説明します。キャッシュ ブロックは、新しい変数がメモリに適用されると、このブロックの容量が十分であれば、チェーンに返されます。解放されたものはすぐにフリーチェーンから取り外されます。したがって、必要なのは最初にメモリの一部を解放し、次にキャッシュ ブロックを指すように zval を構築し、次にポインタを解放して、すぐに新しい変数を逆シリアル化するだけです。その後、変数の値がキャッシュ ブロックに書き込まれます。ビンゴがリリースされたばかりです。これはスタックを書き込む安定した方法です。 キャッシュ ブロックのメモリ構造は次のとおりです。 XX 00 00 00 (0x10 <= XX <= 0x88) XX XX XX XX 上記は 8 バイトのヘッダー、最初のバイトで、このキャッシュ ブロックがどのように機能するかを示しています。頭の上が大きいですか?ヘッダーの後ろにはメモリの内容があり、これはオプションです。
この場合、jmp_buf のアドレスの前のメモリを検索し、そのようなヘッダーを見つけて、それをキャッシュ ブロックとして使用するだけです。たとえば、次のようにその前の 0x1000 のメモリを検索します。
見つけるのが最善です。あまり離れていないいくつかのヘッドを使用します。できれば 600 未満です。原則として、任意のサイズを使用できますが、小さいほど優れています。ほとんどのヘッダーはキャッシュ ブロックから 0x80 よりも離れているため、一度だけ上書きするという状況に遭遇するのは困難ですが、最初のキャッシュ ブロックを使用するときに最後のキャッシュ ブロックを配置することで、これを利用できます。この場合、次回は、jmp_buf のアドレスに到達しない場合は、書き換えたヘッダーからメモリを書き換えて、0x80 バイトのデータを書き込むことができます。 , また、末尾をキャッシュ ブロックの先頭として構築し、jmp_buf が必要なレイアウトに書き換えられるまで構築と書き換えを続けます。これで利用は完了です。
まず、ペイロードに 0x5c が現れることはありません。この場所は少し奇妙です。0x5c が現れると、エスケープ文字の問題だと思いますが、そうでないかどうか。 2 0x5c または 4 0x5cs はいずれも正常に逆シリアル化できません。これは Python のエスケープに関連している可能性があります。これは、理論的には、PHP の逆シリアル化にはフィールドの長さがあるため、「,」が含まれる場合は切り捨てられないため、説明がつきません。シーケンスが失敗した場合、これが問題である可能性があります。
2 番目の点は、このアドレスに入力されているアドレスの 136 バイト前にある変数に注意してください。
之后我就想到,我可以直接调用php中系统命令执行的底层函数。翻了一下php源码,找到其命令执行的函数原型,如下:
/* php_exec * If type==0, only last line of output is returned (exec) * If type==1, all lines will be printed and last lined returned (system) * If type==2, all lines will be saved to given array (exec with &$array) * If type==3, output will be printed binary, no lines will be saved or returned (passthru) * */PHPAPI int php_exec(int type, char *cmd, zval *array, zval *return_value){ FILE *fp; char *buf; size_t l = 0; int pclose_return; char *b, *d=NULL; php_stream *stream; size_t buflen, bufl = 0;#if PHP_SIGCHILD void (*sig_handler)() = NULL;#endif#if PHP_SIGCHILD sig_handler = signal (SIGCHLD, SIG_DFL);#endif#ifdef PHP_WIN32 fp = VCWD_POPEN(cmd, "rb");#else fp = VCWD_POPEN(cmd, "r");#endif if (!fp) { php_error_docref(NULL, E_WARNING, "Unable to fork [%s]", cmd); goto err; } stream = php_stream_fopen_from_pipe(fp, "rb"); buf = (char *) emalloc(EXEC_INPUT_BUF); buflen = EXEC_INPUT_BUF; if (type != 3) { b = buf; while (php_stream_get_line(stream, b, EXEC_INPUT_BUF, &bufl)) { /* no new line found, let's read some more */ if (b[bufl - 1] != '\n' && !php_stream_eof(stream)) { if (buflen < (bufl + (b - buf) + EXEC_INPUT_BUF)) { bufl += b - buf; buflen = bufl + EXEC_INPUT_BUF; buf = erealloc(buf, buflen); b = buf + bufl; } else { b += bufl; } continue; } else if (b != buf) { bufl += b - buf; } if (type == 1) { PHPWRITE(buf, bufl); if (php_output_get_level() < 1) { sapi_flush(); } } else if (type == 2) { /* strip trailing whitespaces */ l = bufl; while (l-- > 0 && isspace(((unsigned char *)buf)[l])); if (l != (bufl - 1)) { bufl = l + 1; buf[bufl] = '\0'; } add_next_index_stringl(array, buf, bufl); } b = buf; } if (bufl) { /* strip trailing whitespaces if we have not done so already */ if ((type == 2 && buf != b) || type != 2) { l = bufl; while (l-- > 0 && isspace(((unsigned char *)buf)[l])); if (l != (bufl - 1)) { bufl = l + 1; buf[bufl] = '\0'; } if (type == 2) { add_next_index_stringl(array, buf, bufl); } } /* Return last line from the shell command */ RETVAL_STRINGL(buf, bufl); } else { /* should return NULL, but for BC we return "" */ RETVAL_EMPTY_STRING(); } } else { while((bufl = php_stream_read(stream, buf, EXEC_INPUT_BUF)) > 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 好了。
构造栈:
exploit!如下:
成功反弹shell。
其实后面还有点问题,因为我把shell exit之后,php继续往下执行,结果apache crash掉了。。。。 crash时gdb状态如图:
也没继续往下看,其实已经差不多了,也就是调一下的事儿。
如果大家有更好的思路,或者对原作的利用方式有更深刻的理解,欢迎与我讨论 :-)