Skip to content
KitploitKITPLOIT
도구익스플로잇블로그
Log in
제출
도구익스플로잇블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

피드문의개인정보© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
php2shell-deep-dive — PHP2Shell에 대한 심층 기술 해설: 메모리 부족 크래시와 use-after-free에서 비롯된 두 개의 PHP 코어 CVE를 힙 그루밍, ASLR 유출, 원격 코드 실행으로 연쇄시키는 과정을 다룹니다. | Kitploit
도구/GitHubGitHub/djad04/php2shell-deep-dive
Memory ForensicsVulnerability AnalysisExploitationReverse EngineeringWeb Application ExploitationCTFPapers & ResearchLearning & EducationBinary Exploitation
GitHubdjad04/php2shell-deep-dive

php2shell-deep-dive

PHP2Shell에 대한 심층 기술 해설: 메모리 부족 크래시와 use-after-free에서 비롯된 두 개의 PHP 코어 CVE를 힙 그루밍, ASLR 유출, 원격 코드 실행으로 연쇄시키는 과정을 다룹니다.

634일 전아직 검토되지 않음

인기

모두 보기 →

커뮤니티에서 가장 많이 사용되는 도구를 찾아보세요.

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유
저장소 보기웹사이트

저는 2026년 10월 26일에 이 연구를 쓰기 시작했으며, 언제 완성할 수 있을지는 아직 확실하지 않습니다.

글을 쓰는 시점에도 PHP2Shell에 대해 이용 가능한 정보는 여전히 매우 적으며, 특히 취약점과 익스플로잇 과정에 대한 상세한 기술 분석에 관해서는 더욱 그렇습니다.

솔직히 말하면, 내부에서 실제로 무슨 일이 일어나는지 보여주지 않고 겉만 살짝 훑는 설명을 찾는 데 지쳐가고 있었습니다. 저는 뭔가 다른 것을 하고 싶었습니다. 전체 과정을 탐구하고, 모든 중요한 세부 사항을 검토하며, 단지 '무엇'이 일어나는지만이 아니라 '왜' 그리고 '어떻게' 일어나는지를 설명하는, 제대로 된 심층 기술 해설 말입니다.

이 연구는 PHP 소스 코드에 대한 저 자신의 이해, 제 실험, 그리고 이 취약점들을 이해하려고 노력하면서 수행한 조사들을 바탕으로 합니다. 저는 이 모든 조각들을 모아 제 발견을 공유하고, 이를 통해 기저에 깔린 개념들 중 일부를 다른 사람들이 더 쉽게 이해할 수 있게 되기를 바랐습니다.

제 주요 목표 중 하나는 기술적 깊이를 희생하지 않으면서도 이 내용을 접근 가능하게 만드는 것이었습니다. 기본적인 C 지식과 일반적인 프로그래밍 경험이 있는 사람이라면, 저수준 메모리 익스플로잇에 대한 사전 경험이 전혀 없더라도 따라올 수 있기를 바랍니다.

또한 이 연구가 현재 제대로 설명되지 않았거나 오해받고 있는 부분들 중 일부를 명확히 하는 데 도움이 되기를 바랍니다. 다만 분명히 밝히고 싶습니다. 이것은 취약점들에 대한 저 자신의 기술적 이해이며, 공식적인 분석이나 절대 틀리지 않는 참고 자료가 아닙니다. 저는 여전히 배우고, 조사하고, 스스로 점들을 연결해 나가는 중입니다. 과정 중에 실수나 부정확한 점, 혹은 제가 잘못 이해한 부분이 있을 수 있습니다.

잘못된 점을 발견하셨거나, 다른 해석을 가지고 계시거나, 더 조사할 가치가 있다고 생각되는 부분이 있다면 주저하지 말고 저에게 연락해 주십시오. 이 연구를 더 정확하고 모두에게 유용하게 만드는 데 도움이 될 수정, 논의, 기여를 진심으로 감사히 여기겠습니다.

결국, 이 글을 공유하는 이유는 함께 배우고 더 많은 사람들이 내부에서 실제로 무슨 일이 일어나는지 이해하도록 돕는 것입니다.


빠른 탐색

기본 개념은 이미 잘 알고 계신가요?? 관심 있는 부분으로 바로 이동하세요:

  • 시작하기 전에: 스택 vs 힙, 메모리 할당 기초
  • 이것이 왜 위험한가?: 스테일 포인터의 이야기, UAF 읽기와 쓰기
  • 스테일 포인터: 어디서 오는가: PHP의 세션 + OOM 상호작용 내부의 UAF 기원 이야기
  • 공격 요청 구성하기: OOM을 트리거하는 실제 HTTP 요청
  • 목표: 무엇을 유출해야 하는지, 그리고 그 이유 (힙 포인터 + 코드 포인터)
  • 대상 구조체: zend_string: PHP가 세션 쿠키를 구성할 때 읽는 구조체
  • 메모리 재점유: 요청 #2와 대상 zend_string 구조체
  • 힙 그루밍 심층 분석: 위조된 데이터를 정확히 원하는 메모리 슬롯에 배치하는 방법
  • multipart_buffer_read_body의 미스터리: 내 정신을 거의 무너뜨릴 뻔한 숨겨진 할당
  • 두 개의 유출, 하나가 아님: 힙 유출과 코드 유출에 서로 다른 보정이 필요한 이유
  • 코드 유출 (3단계): ASLR을 무력화하기 위한 함수 포인터 유출
  • 핑거프린팅: 올바른 포인터를 얻었는지 확인하는 방법
  • 힙 유출 파싱: 256바이트의 혼돈 속에서 청크 베이스 찾기
  • 1+8 파이프라인: 두 유출이 실제로 전달되는 방식
  • 할당자를 재사용하여 구조체 위조하기: pDestructor 하이재킹 아이디어
  • 트리거: PHP가 더러운 일을 처리한다, 실제 zend_hash_destroy 코드
  • 하나에 세 개의 구조체: 384바이트 안의 ForgedArray + FakeExecuteData + FakeString
  • 최종 페이로드 (4단계): pDestructor를 하이재킹하고 RCE를 얻기 위한 HashTable 위조
  • 프라이머 메커니즘: bucket[2]에 있는 해제된 프라이머의 유령이 파괴 체인을 트리거하는 방법
  • 명령 출력 트릭: /proc에서 자신의 TCP 소켓 찾기
  • 전체 파이프라인: 네 단계 전부를 처음부터 끝까지
  • 다른 빌드에 맞춘 보정: 모든 수치가 특정 PHP 빌드의 힙 분석에서 나오는 방식
  • 현실 세계의 마찰: 실제 프로덕션 서버에서 이 공격이 훨씬 어려운 이유
  • 내 비트코인을 훔쳐라 CTF: 이 모든 것을 시작한 챌린지
  • 감사의 말: 크레딧과 참고 자료

힙 익스플로잇이 처음이거나 전체 경험을 원한다면, 그냥 계속 스크롤하세요. 저는 이 글을 처음부터 끝까지 읽도록 작성했습니다.


시작하기 전에

본격적으로 들어가기 전에 모두가 명심해야 할 몇 가지가 있습니다. 이는 연구를 진행하면서 특히 유용해질 것입니다. 앞서 언급했듯이, 저는 이 워크스루가 모든 사람이 접근할 수 있기를 원하므로, 더 기초적인 개념조차 설명하는 시간을 가질 것입니다. 이미 익숙하다면 건너뛰셔도 됩니다!

스택 vs. 힙

일반적인 프로그램에서 메모리는 스택과 힙을 포함한 여러 영역으로 구성됩니다.

예를 들어, C++에서는:

int main() {
    int a = 30;       // Typically stored on the stack
    int* b = new int(30);  // b is typically on the stack,
                           // but the integer it points to is on the heap
}

여기서 a는 값 30을 담고 있는 지역 변수입니다. 포인터 b 역시 지역 변수이지만, 힙에 할당된 정수의 주소를 담고 있습니다.

메모리 할당이 동작하는 방식

PHP는 주로 C로 작성되었기 때문에, 우리는 메모리 할당이 어떻게 동작하는지 이해해야 합니다.

운영체제는 프로세스에 고유한 가상 주소 공간을 제공합니다. 그런 다음 프로세스의 메모리 할당자가 사용 가능한 메모리 내에서 할당을 관리합니다.

예를 들어, C++에서는 new, malloc() 및 기타 메모리 할당 방법이 있습니다.

할당자는 매번 운영체제에 새 메모리를 요청하는 대신 해제된 메모리를 재사용하려고 시도하는 경우가 많습니다.

또한 객체의 수명이 끝나더라도 그 메모리에 저장되어 있던 데이터가 반드시 즉시 지워지는 것은 아닙니다. 해당 메모리는 단순히 재사용 가능한 것으로 표시될 수 있습니다.


이를 더 잘 이해하기 위해, 다음 코드를 잠깐 살펴보겠습니다:

#include <cstdlib>
#include <iostream>

int main() {
    void* a = malloc(64);

    free(a);

    void* b = malloc(64);

    std::cout << a << "\n" << b << std::endl;

    free(b);
    return 0;
}

여기서 a와 b를 출력하면 동일한 숫자 메모리 주소를 포함하고 있음을 발견할 수 있습니다.

왜일까요? free(a)를 호출한 후 malloc()이 이전에 a에 할당되었던 메모리를 재사용하여 또 다른 메모리 블록을 얻을 필요가 없기 때문입니다.

이것이 항상 발생한다고 보장되지는 않지만, 흔한 동작입니다.


이것이 왜 위험한가? 오래된 포인터의 이야기

이것이 문제의 핵심으로 이어집니다. 우리는 메모리가 재활용된다는 것을 알고 있지만, 그 메모리 주소를 담고 있던 변수들은 어떻게 될까요?

free(a)를 호출하면 메모리 할당 시스템은 단순히 내부 기록을 갱신하여 해당 메모리 청크를 재사용 가능으로 표시합니다. 그러나 그 메모리 내부의 데이터는 깨끗이 지워지지 않으며, 포인터 변수 자체도 마법처럼 NULL로 초기화되지 않습니다. 프로그래머가 명시적으로 지우지 않는 한, 포인터는 이전과 동일한 메모리 주소를 계속 담고 있습니다.

이렇게 남겨진 변수를 우리는 오래된 포인터(stale pointer) 또는 댕글링 포인터(dangling pointer)라고 부릅니다.

오래된 포인터를 보유하는 것은 여러 면에서 위험할 수 있습니다. 가장 심각한 두 가지는 더 이상 원래 객체에 속하지 않는 데이터를 읽는 것과, 이전 포인터를 통해 쓰기를 수행하여 새로운 객체를 손상시키는 것입니다.

다른 객체의 데이터 읽기 (Use-After-Free)

사용자 계정을 위해 메모리를 할당한다고 상상해 봅시다:

struct User {
    char username[24];
    int is_admin;
};

int main() {
    // 1. Allocate memory for a user
    User* a = (User*)malloc(sizeof(User));

    // Initialize the original user
    strcpy(a->username, "alice");
    a->is_admin = 0;

    // We keep a copy of the pointer to demonstrate the bug
    User* a_stale = a; 

    // 2. Free the memory. The object is gone, but a_stale still holds the address!
    free(a);

이제 바로 이어서, 프로그램이 다른 곳에서 또 다른 할당을 수행하고 할당자가 동일한 주소를 재사용한다고 가정해 보겠습니다:

User* b = (User*)malloc(sizeof(User));

strcpy(b->username, "jack");
b->is_admin = 1;

따라서 b는 이제 정확히 a가 있던 자리에 위치합니다. 하지만 a를 해제했고 a_stale을 갱신한 것이 아무것도 없기 때문에 a_stale은 여전히 그곳을 가리킵니다.

즉, 여전히 a_stale을 사용하는 코드는 Alice의 데이터를 보고 있다고 생각하지만, 실제로는 Jack의 데이터에 접근하고 있을 수 있습니다:

if (a_stale->is_admin) {
    // This may run, even though Alice was never an admin
    printf("Welcome admin %s\n", a_stale->username);
}

이를 통해 우리는 Jack의 사용자 이름처럼 보아서는 안 되는 데이터에 접근할 수 있게 되며, 프로그램은 잘못된 권한 부여 결정을 내릴 수 있습니다.

기술적으로 해제된 메모리에 접근하는 것은 C/C++에서 "정의되지 않은 동작(undefined behavior)"이므로, 컴파일러와 시스템에 따라 정확한 결과가 보장되지 않습니다. 하지만 이는 근본적인 문제를 보여줍니다.

그러나 실제로는 그렇게 단순하지 않습니다. 아마 이렇게 생각하고 계실 겁니다: "그래, 이건 좋네. 그런데..."

이것이 작동하려면 우선 댕글링 포인터(stale pointer)가 필요합니다. 실제 공격에서는 우리가 피해자의 프로그램을 작성하는 것이 아니므로, a_stale에서 했던 것처럼 의도적으로 변수에 대한 포인터를 저장할 수는 없습니다. 버그는 이미 코드 안에 존재해야 합니다...

공격자는 해제된 메모리 슬롯에 무엇이 들어갈지만 영향을 미칠 수 있습니다.

지금은 이 내용을 깊이 이해하는 것만 확실히 하세요. 왜냐하면 그 질문에 대한 답이 우리가 논의하는 PHP CVE의 가장 중요한 부분이 될 것이기 때문입니다.

다른 객체의 데이터 수정 (메모리 손상)

이제 반대의 경우를 상상해 보세요: 프로그램이 메모리가 해제된 후에도 이전 포인터를 사용하여 메모리에 쓰기를 계속하는 경우입니다.

#include <cstdlib>
#include <cstdio>

int main() {
    int* a = (int*)malloc(sizeof(int));
    *a = 42;
도구 다운로드