从零开始的 Pwn 之旅 - Php_pwn

前言

php pwn 泛指利用 PHP 环境下的漏洞进行 PWN(即:利用二进制层面的漏洞进行提权、RCE等)
本文主要针对 php extension 进行 pwn 利用

前置知识

php 扩展一般采用 c 语言编写
php 通过加载库文件(.so / .dll)来实现扩展
因为扩展函数是在 php 内部调用的,我们不能像平常那样直接执行 system one_gadget,这里通常是需要采用 popen 或者 exec 函数族(php 函数)来进行执行 bash命令来反弹 shell
php 扩展中使用 printf system 等函数会产生 stdout 输出, 但是这些输出有可能会被日志捕获被丢弃等, 如果我们想得到 php 扩展的输出我们需要使用 php 提供的函数比如 php_printf

编写 php 扩展

  1. 构建 Docker 环境 首先来看一下文件结构
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
➜  mini-stack tree .
.
├── app
│   ├── api.php
│   └── index.php
├── debian.sources
├── docker-compose.yml
├── Dockerfile
└── nginx
  └── default.conf

2 directories, 6 files

Dockerfile:

1
2
3
4
5
6
7
8
FROM php:8.2-fpm

# 覆盖 debian.sources 为阿里云镜像
COPY debian.sources /etc/apt/sources.list.d/debian.sources

RUN apt-get update \
  && apt-get install -y git vim zip unzip curl \
  && rm -rf /var/lib/apt/lists/*

debian.sources:

1
2
3
4
Types: deb deb-src
URIs: https://mirrors.aliyun.com/debian/
Suites: trixie trixie-updates trixie-backports
Components: main contrib non-free non-free-firmware

docker-compose.yml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
services:
  php:
    build: .
    container_name: mini_php
    working_dir: /var/www/html
    volumes:
      - ./app:/var/www/html

  nginx:
    image: nginx:1.27-alpine
    container_name: mini_nginx
    depends_on:
      - php
    ports:
      - "8080:80"
    volumes:
      - ./app:/var/www/html:ro
      - ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro

nginx/default.conf

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
server {
    listen 80;
    server_name _;

    root /var/www/html;
    index index.php;

    # 处理普通静态文件(如 css/js/jpg)
    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    # 交给 php-fpm 的部分:匹配 .php 结尾
    location ~ \.php$ {
        # 必须把真实文件路径发给 php-fpm
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

        include fastcgi_params;              # 引入默认 FastCGI 参数
        fastcgi_pass php:9000;               # 服务名:端口(php-fpm 默认监听 9000)
        fastcgi_index index.php;
    }
}

app/index.php:

1
2
<?php
phpinfo();

app/api.php:

1
2
3
4
5
6
7
8
<?php

if (isset($_POST['a'])) {
    $a = $_POST['a'];
    easy_phppwn($a);
} else {
    highlight_file(__FILE__);
}

开启容器

1
➜  mini-stack docker compose up
  1. 拉取 php 源码
1
2
3
➜  php8.1 git clone https://github.com/php/php-src.git
# 复制到 docker 内部
➜  php8.1 docker cp php-src 816f:/
  1. 创建 extension
1
2
3
root@816fd413f946:/# cd php-src/ext/
root@816fd413f946:/php-src/ext# ./ext_skel.php --ext easy_phppwn
root@816fd413f946:/php-src/ext# cd easy_phppwn/

编辑 easy_phppwn.c 文件为以下内容

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
/* easy_phppwn extension for PHP */

#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

#include "php.h"
#include "ext/standard/info.h"
#include "php_easy_phppwn.h"
#include "easy_phppwn_arginfo.h"

/* For compatibility with older PHP versions */
#ifndef ZEND_PARSE_PARAMETERS_NONE
#define ZEND_PARSE_PARAMETERS_NONE() \
        ZEND_PARSE_PARAMETERS_START(0, 0) \
        ZEND_PARSE_PARAMETERS_END()
#endif


/* {{{ PHP_RINIT_FUNCTION */
PHP_RINIT_FUNCTION(easy_phppwn)
{
#if defined(ZTS) && defined(COMPILE_DL_EASY_PHPPWN)
        ZEND_TSRMLS_CACHE_UPDATE();
#endif

        return SUCCESS;
}
/* }}} */

PHP_MINFO_FUNCTION(easy_phppwn) {
}

/* {{{ PHP_MINFO_FUNCTION */
PHP_FUNCTION(easy_phppwn)
{
        char *arg = NULL;
        size_t arg_len, len;
        char buf[100];
        if(zend_parse_parameters(ZEND_NUM_ARGS(), "s", &arg, &arg_len) == FAILURE){
                return;
        }
        memcpy(buf, arg, arg_len);
        php_printf("The baby phppwn.\n");
}
/* }}} */

/* {{{ easy_phppwn_module_entry */
zend_module_entry easy_phppwn_module_entry = {
        STANDARD_MODULE_HEADER,
        "easy_phppwn",                                  /* Extension name */
        easy_phppwn_functions,                                  /* zend_function_entry */
        NULL,                                                   /* PHP_MINIT - Module initialization */
        NULL,                                                   /* PHP_MSHUTDOWN - Module shutdown */
        PHP_RINIT(easy_phppwn),                 /* PHP_RINIT - Request initialization */
        NULL,                                                   /* PHP_RSHUTDOWN - Request shutdown */
        PHP_MINFO(easy_phppwn),                 /* PHP_MINFO - Module info */
        PHP_EASY_PHPPWN_VERSION,                /* Version */
        STANDARD_MODULE_PROPERTIES
};
/* }}} */

#ifdef COMPILE_DL_EASY_PHPPWN
# ifdef ZTS
ZEND_TSRMLS_CACHE_DEFINE()
# endif
ZEND_GET_MODULE(easy_phppwn)
#endif

编辑 easy_phppwn_arginfo.h 为以下内容

1
2
3
4
5
6
7
8
9
ZEND_BEGIN_ARG_INFO_EX(arginfo_easy_phppwn, 0, 0, 1)
ZEND_ARG_INFO(0, arg)  // Argument 'arg' of type string
ZEND_END_ARG_INFO()
 
ZEND_FUNCTION(easy_phppwn);
 
static const zend_function_entry easy_phppwn_functions[] = {
PHP_FE(easy_phppwn, arginfo_easy_phppwn)                                               PHP_FE_END
};

进行编译

1
2
3
4
5
6
root@816fd413f946:/php-src/ext/easy_phppwn# phpize
Configuring for:
PHP Api Version:         20210902
Zend Module Api No:      20210902
Zend Extension Api No:   420210902
root@816fd413f946:/php-src/ext/easy_phppwn# ./configure

配置好了之后,会有一个Makefile文件,在里面取消-O2优化,否则会加上FORTIFY保护,导致memcpy函数加上长度检查变为__memcpy_chk函数
如果不想要canary保护需要输入命令取消 -fno-stack-protector

这里修改为 CFLAGS = -g -fno-stack-protector

1
root@816fd413f946:/php-src/ext/easy_phppwn# make -j8 && make install

编译成功

1
Installing shared extensions:     /usr/local/lib/php/extensions/no-debug-non-zts-20210902/

修改 php 配置文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
root@816fd413f946:/php-src/ext/easy_phppwn# php --ini
Configuration File (php.ini) Path: /usr/local/etc/php
Loaded Configuration File:         (none)
Scan for additional .ini files in: /usr/local/etc/php/conf.d
Additional .ini files parsed:      /usr/local/etc/php/conf.d/docker-php-ext-opcache.ini,
/usr/local/etc/php/conf.d/docker-php-ext-sodium.ini

root@816fd413f946:/php-src/ext/easy_phppwn# cp /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini
root@816fd413f946:/php-src/ext/easy_phppwn# php --ini
Configuration File (php.ini) Path: /usr/local/etc/php
Loaded Configuration File:         /usr/local/etc/php/php.ini
Scan for additional .ini files in: /usr/local/etc/php/conf.d
Additional .ini files parsed:      /usr/local/etc/php/conf.d/docker-php-ext-opcache.ini,
/usr/local/etc/php/conf.d/docker-php-ext-sodium.ini

可以发现默认这里没有配置文件,我们只需要复制一份就行

修改 /usr/local/etc/php/php.ini 以让 php 加载我们的扩展

1
2
3
4
5
[PHP]

extension=easy_phppwn.so

可以使用 php -m 命令来检查是否可以被加载

1
2
root@816fd413f946:/php-src/ext/easy_phppwn# php -m | grep php
easy_phppwn

创建简单的测试文件

1
2
3
4
<?php
$a = "abcd";
easy_phppwn($a);
?>

执行文件

1
2
root@816fd413f946:/php-src/ext/easy_phppwn# php /tmp/test.php
The baby phppwn.

可以发现执行成功了

此时可以在虚拟机内访问 127.0.0.1:8080 来访问我们构建的 web 环境

容器名改变了请不要在意与上文的一致

修改源码以便调试 app/api.php

1
2
3
4
5
6
7
8
9
<?php

$a = 'aaaabbbb';
if (isset($a)) {
    /* $a = $_POST['a']; */
    easy_phppwn($a);
} else {
    highlight_file(__FILE__);
}

这样修改是因为我们无法通过 webserver 传递参数, 那样会使得事情更加复杂化

先安装 gdbserver 准备调试

1
2
root@9c5f63a799ab:/var/www/html# apt-get update
root@9c5f63a799ab:/var/www/html# apt-get install -y gdbserver

启动 gdbserver

1
root@9c5f63a799ab:/var/www/html# gdbserver :1234 php ./api.php

笔者忘记设置 gdbserver 的端口映射了, 通过以下命令可以获取容器 ip, 我们直接通过 ip 连接

1
2
➜  app docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' mini_php
172.18.0.2

打开主机 gdb 进行连接

1
pwndbg> target remote 172.18.0.2:1234

下断点

1
2
pwndbg> b zif_easy_phppwn
Function "zif_easy_phppwn" not defined.

发现下不了端点因为此时我们的扩展库还没有被加载到 gdb 中
使用下面的命令下断点到加载我们的扩展库之后

1
2
3
4
5
pwndbg> catch load /usr/local/lib/php/extensions/no-debug-non-zts-20220829/easy_phppwn.so
Catchpoint 1 (load)
pwndbg> i b
Num     Type           Disp Enb Address            What
1       catchpoint     keep y                      load of library matching /usr/local/lib/php/extensions/no-debug-non-zts-20220829/easy_phppwn.so

使用 c 继续

1
2
3
Catchpoint 1
  Inferior loaded target:/usr/local/lib/php/extensions/no-debug-non-zts-20220829/easy_phppwn.so
0x00007f55941f5440 in _dl_debug_state () from target:/lib64/ld-linux-x86-64.so.2

成功断住
下函数断点

1
2
pwndbg> b zif_easy_phppwn
Breakpoint 2 at 0x7f5592653172: file /php-src/ext/easy_phppwn/easy_phppwn.c, line 22.

继续

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]────────────────────────────────────────────────
 ► 0x7f5592653172 <zif_easy_phppwn+25>    mov    qword ptr [rbp - 8], 0          [0x7ffcc2d8ec68] <= 0
   0x7f559265317a <zif_easy_phppwn+33>    mov    rax, qword ptr [rbp - 0x88]     RAX, [0x7ffcc2d8ebe8] => 0x7f55924140c0 ◂— 0x74612074696c7073 ('split at')
   0x7f5592653181 <zif_easy_phppwn+40>    mov    eax, dword ptr [rax + 0x2c]     EAX, [0x7f55924140ec] => 1
   0x7f5592653184 <zif_easy_phppwn+43>    lea    rcx, [rbp - 0x10]               RCX => 0x7ffcc2d8ec60 ◂— 0x1000
   0x7f5592653188 <zif_easy_phppwn+47>    lea    rdx, [rbp - 8]                  RDX => 0x7ffcc2d8ec68 ◂— 0
   0x7f559265318c <zif_easy_phppwn+51>    lea    rsi, [rip + 0xe91]              RSI => 0x7f5592654024 ◂— 0x6162206568540073 /* 's' */
   0x7f5592653193 <zif_easy_phppwn+58>    mov    edi, eax                        EDI => 1
   0x7f5592653195 <zif_easy_phppwn+60>    mov    eax, 0                          EAX => 0
   0x7f559265319a <zif_easy_phppwn+65>    call   zend_parse_parameters@plt   <zend_parse_parameters@plt>

   0x7f559265319f <zif_easy_phppwn+70>    cmp    eax, -1
   0x7f55926531a2 <zif_easy_phppwn+73>    je     zif_easy_phppwn+120         <zif_easy_phppwn+120>

成功断住并且带参数

测一下栈底距离

1
2
pwndbg> distance 0x7ffcc2d8ebf0 $rbp
0x7ffcc2d8ebf0->0x7ffcc2d8ec70 is 0x80 bytes (0x10 words)

与 ida pro 中显示一致 后续思路就是通过读 /proc/maps/self 等文件获取 php 基址从而调用 php 函数

  • TODO: 通过 Rop 调用 php 函数示例

当使用 target remote 命令连接之后使用 c 执行完毕退出之后
如果不退出 gdb 选择重新使用 target remote 就会发现 catach load 断不住