前言
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 扩展
- 构建 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:
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
|
- 拉取 php 源码
1
2
3
| ➜ php8.1 git clone https://github.com/php/php-src.git
# 复制到 docker 内部
➜ php8.1 docker cp php-src 816f:/
|
- 创建 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 函数
坑
当使用 target remote 命令连接之后使用 c 执行完毕退出之后
如果不退出 gdb 选择重新使用 target remote 就会发现 catach load 断不住