🤯 LLM 취약점을 발견했다고 생각했다. 틀렸다. 그러다 진짜를 찾았다.
요약
본 글은 LLM 애플리케이션의 보안 취약점을 직접 실험하고 검증하는 'AI 레드팀 실험실' 구축 과정을 다룹니다. Open WebUI와 로컬 모델을 활용하여 RAG 권한 체인 및 모델 간 전이 탈옥 기법 등을 테스트했습니다. 특히, 발견된 오류를 철회하고 재검증하는 과정 자체가 중요한 방법론적 교훈을 제공합니다.
핵심 포인트
- LLM 취약점은 이론 학습보다 직접 실험실 구축으로 검증해야 합니다.
- Open WebUI와 로컬 모델(llama3.2, mistral 등)을 활용한 격리된 테스트 환경이 핵심입니다.
- 공격자 자격 증명 유효성 및 권한 경계 재검증 과정이 중요합니다.
- 발견 사항은 반드시 통제된 워크플로우와 명확한 증거를 통해 문서화해야 합니다.
철회, 사용자 간 RAG 체인, 그리고 모델 간 전이되는 탈옥(jailbreak)
저는 간단한 질문에 답하기 위해 샌드박스 AI 레드팀 실험실을 구축했습니다: 제가 실제로 LLM 애플리케이션의 보안 문제를 발견하고 검증할 수 있을까?
이 프로젝트는 Open WebUI에서 사용자 간 RAG 권한 체인 취약점과 모델 패밀리 간 전이되는 수동 개발 탈옥 기법을 포함하여 몇 가지 확인된 결과를 도출했습니다. 또한 예상치 못한 결과도 얻었습니다: 오탐지(false positive)입니다.
저는 처음에 사용자 간 채팅 접근 취약점을 보고했습니다. 재테스트 과정에서 제 공격자 자격 증명이 유효하지 않다는 것을 발견했습니다. 저는 이 발견을 철회하고, 왜 이런 일이 발생했는지 문서화했으며, 실수에 대한 테스트 방법론을 변경했습니다.
이러한 철회가 프로젝트의 가장 유용한 결과 중 하나가 되었습니다.
AI 보안에 대해 읽는 대신 실험실을 구축해야 하는 이유?
매주 프롬프트 인젝션(prompt injection), 탈옥(jailbreaks), 또는 LLM 애플리케이션 취약점에 관한 기사가 하나씩 나옵니다. 이런 글들을 읽는 것은 저에게 어휘력만 가르쳐 주었습니다. 제가 실제로 알고 싶었던 것, 즉 '이런 버그를 스스로 찾아낼 수 있을까?'라는 것을 가르쳐주지는 못했습니다. 그래서 저는 실험실을 구축했습니다. 튜토리얼 환경이 아닙니다. 저만의 것입니다. Ollama가 구동되는 Mac에 세 개의 로컬 모델(llama3.2:3b, mistral:7b, qwen3:8b)을 서비스했습니다. Open WebUI v0.3.16과 Kali Linux 공격 환경은 격리된 실험실 네트워크의 Docker에서 실행되었습니다. 테스트는 합성 계정과 통제된 페이로드(payloads)를 사용했습니다. 이 환경은 의도적으로 제가 사용하는 기계에 또 다른 항상 실행되는 서비스가 되지 않도록 설계되었습니다. 모델 엔드포인트는 루프백 액세스(loopback access)로 제한되었고, 실험실에는 검증 확인 절차를 거치는 명시적인 시작 및 중지 절차가 있었습니다. 이 프로젝트를 형성한 두 가지 제약 사항이 있었습니다. 목표 대상은 고정되었습니다. 움직이는 목표보다는 알려진 애플리케이션 버전을 원했습니다. 이것이 동일한 빌드에 대한 동작을 재현하고 더 유용한 질문, 즉 '애플리케이션의 권한 경계는 실제로 어디에서 유지되는가?'를 던질 수 있게 했습니다. 이 실험실은 서비스라기보다는 스위치였습니다. lab-start로 환경을 띄우고 검증했으며, lab-down으로 이를 해체하고 예상되는 서비스들이 더 이상 리스닝(listening)하지 않음을 검증했습니다.
아키텍처, 도구, 운영 스크립트, 발견 사항 및 증거는 동반 저장소에서 확인할 수 있습니다:
github.com/soluckyarya2-tech/ai-red-team-lab
—————————————————————————————————————————————————————————————————————
방법론 — 저의 실수로 작성됨
이 프로젝트에서 모든 발견 사항을 만들어낸 워크플로우는 다음과 같았습니다:
- 공격자(attacker)의 신원과 토큰을 검증한다.
- 기준선(baseline)을 설정한다.
- 공격(Attack)을 수행한다.
- 통제 테스트(control)를 실행한다.
- 인증(authentication), 권한 부여(authorization), 그리고 재현 조건(reproduction conditions)을 다시 테스트한다.
- 결과를 문서화한다 — 다음 섹션의 내용 때문에 1단계에서 실패한 것이 무엇인지 포함하여. 6단계가 존재하는 이유는 증거 없는 발견 사항 목록은 단지 주장들의 목록일 뿐이기 때문이다. 권한 부여 테스트를 위해, 나는 모든 결과가 동일한 질문에 답하기를 원했다:
- 누가 요청을 보내는가?
- 자격 증명(credential)이 실제로 유효한가?
- 합법적인 소유자는 무엇을 할 수 있는가?
- 무단 사용자는 무엇을 할 수 있는가?
- 응답이 보호된 리소스에 대한 접근을 입증하는가?
- 그 결과가 독립적으로 재현될 수 있는가? 이 과정은 어떤 개별 도구보다 더 중요해졌다.
애플리케이션 감사(The application audit)
나는 두 개의 합성 표준 사용자(synthetic standard users)를 사용하여 Open WebUI의 API 표면을 테스트했다. 중요한 경계는 간단했다: 한 계정이 리소스를 소유하고 다른 계정은 소유하지 않는 경우였다. 가독성을 위해, 나는 각 테스트에서 하나의 계정을 영구적인 공격자나 피해자로 가정하기보다는 그들의 경계 역할(boundary role)에 따라 지칭한다.
F1 — 파일 라우터가 경계를 유지하다
실제로는 부정적인 결과가 첫 번째 유용한 결과였다. 사용자 간 파일 접근은 거부되었는데, 이는 '누락됨(missing)'과 '금지됨(forbidden)'을 구별하지 못하는 404 오류였고, 피해자의 파일을 보여주지 않는 사용자 범위 목록이었다. 입증된 메타데이터 누출이나 열거 오라클(enumeration oracle)은 없었다. 이것은 중요한 기준선을 확립했다: 애플리케이션은 그 파일이 다른 사용자에게 속한다는 것을 알고 있었다. 다음 질문은 해당 파일에 작동하는 모든 하위 시스템이 동일한 소유권 경계를 강제하는지 여부였다. 그렇지 않았다.
F2 — 내가 철회해야 했던 거짓 양성(The false positive I had to retract)
F2 — 내가 철회해야 했던 거짓 양성(The false positive I had to retract)
채팅 권한 테스트는 프로젝트에서 가장 중요한 방법론적 교훈이 되었습니다. 제가 처음 수행한 테스트에서는 사용자 간 채팅 접근 취약점이 있는 것처럼 보였습니다. 저는 401 응답을 관찰했고, 이어서 관련 제어 요청으로부터 200 응답을 받았습니다. 저는 이 순서를 공격자가 다른 사용자의 채팅에 접근할 수 있다는 증거로 해석했습니다. 하지만 저는 틀렸습니다.
200 응답은 정당한 제어 사례, 즉 소유자가 자신의 채팅에 접근하는 경우에서 온 것이었습니다. 공격자 요청은 유효하지 않은 자격 증명으로 이루어졌던 것입니다. 근본 원인은 JWT 서명 비밀(signing-secret) 순환 이전에 생성된 공격자 토큰이었습니다. 그 페이로드(payload)는 유효해 보였지만, 서명(signature) 자체가 더 이상 유효하지 않았습니다.
채팅 권한 제어는 올바르게 작동하고 있었습니다. 저는 검증된 자격 증명으로 시나리오를 재테스트했습니다. 승인되지 않은 요청은 거부되었습니다. 따라서 해당 발견은 철회되었습니다.
이 경험은 저의 방법론을 영구적으로 변화시켰습니다. 보안 판정은 공격자 자격 증명이 독립적으로 검증되기 전까지는 내릴 수 없습니다. 인증 실패와 권한 우회(authorization bypass)는 먼저 인증 상태를 확립하지 않으면 동일하게 보이기 때문입니다.
재테스트 과정에서는 애플리케이션 라우터 전반에 걸쳐 다른 거부(denial) 의미론이 노출되었습니다. 잘못된 자격 증명, 승인되지 않은 채팅, 그리고 승인되지 않은 파일은 서로 다른 응답 패턴을 생성했습니다. 이러한 동작 자체는 취약점으로 간주되지는 않았습니다. 하지만 인증 실패와 리소스 소유권 실패를 구별하는 데 도움이 되었기 때문에 테스트 지문(testing fingerprint)으로서 유용했습니다.
이 저장소에는 원래 주장, 철회 내용, 근본 원인 분석, 그리고 수정된 결과가 모두 보존되어 있습니다. 저는 보안 연구 기록에 작동했던 것들만 담겨서는 안 된다고 생각합니다.
—————————————————————————————————————————————————————————————————————
F6 — 사용자 간 RAG 수집(Cross-user RAG ingestion)
실제 애플리케이션 계층 취약점은 제가 파일을 RAG 파이프라인으로 따라가면서 발견되었습니다.
Open WebUI는 업로드된 문서를 검색 증강 생성(RAG)에 사용되는 컬렉션으로 처리합니다.
수집 엔드포인트:
POST /rag/api/v1/process/doc
파일 ID와 컬렉션 정보를 받습니다.
파일 라우터는 이미 승인되지 않은 사용자에게 다른 사용자의 파일에 대한 직접적인 접근을 거부한다는 것을 보여주었습니다.
저는 RAG 수집 경로가 동일한 소유권 경계를 강제하는지 테스트했습니다.
순서는 다음과 같았습니다:
- 한 테스트 계정이 고유 마커를 포함하는 파일을 업로드했습니다: TOPSECRET-TEST2-4821
- 다른 계정이 해당 파일에 직접 접근을 시도했습니다.
- 직접 접근은 404 오류를 반환했습니다.
- 승인되지 않은 계정이 소유자의 파일 ID를 /process/doc로 제출했습니다.
- 처리 요청은 200을 반환했습니다.
- 문서가 컬렉션에 수집되었습니다.
- 승인되지 않은 계정이 해당 컬렉션을 조회했습니다.
- 응답에는 고유 마커를 포함한 문서 내용이 담겨 있었습니다. 이 마커는 중요합니다. 오직 업로드된 파일 안에만 존재했기 때문에, 승인되지 않은 쿼리 결과에 나타난 것은 단순히 다른 사용자의 리소스 메타데이터를 노출하는 것이 아니라 콘텐츠 수준의 공개(content-level disclosure)였음을 입증했습니다. 따라서 발견된 취약점은 단지 승인되지 않은 사용자가 다른 사용자의 파일 ID를 참조할 수 있다는 것만이 아니었습니다. 입증된 동작은 다음과 같았습니다: 인증된 사용자가 다른 사용자의 업로드 문서를 RAG 파이프라인을 통해 처리하게 만들고, 이후 그 내용을 검색할 수 있었다는 것입니다. 직접적인 파일 권한 경계는 존재했지만, RAG 수집 경로는 동일한 경계를 강제하지 않았습니다.
F7a — 컬렉션 소유권 실패
컬렉션 쿼리 레이어에서 두 번째 권한 실패가 발생했습니다.
컬렉션 쿼리 엔드포인트는 해당 컬렉션의 소유권을 강제하지 않은 채 컬렉션 이름을 허용했습니다.
또한, collection_names 매개변수는 단일 요청에 여러 개의 컬렉션 이름을 허용했습니다.
테스트 결과, 유효한 컬렉션 이름은 연결된 콘텐츠를 생성하는 반면, 유효하지 않은 이름은 동등한 콘텐츠를 제공하지 않고 실패했습니다.
이는 긍정적인 신호 기반 열거(enumeration) 메커니즘을 만들었습니다.
이 시점에서 공격자는 직접적인 파일 접근이 필요하지 않았습니다.
컬렉션 자체가 근본적인 문서 콘텐츠로 가는 또 다른 경로가 된 것입니다.
그 결과, 별도의 권한 발견 사항이 나왔습니다. 즉, 컬렉션 쿼리 레이어는 애플리케이션의 파일 레이어가 설정한 소유권 경계를 강제하지 않았다는 것입니다.
따라서 두 발견 사항은 개념적으로 연결될 수 있었습니다: 크로스-유저 파일 → 무단 수집(unauthorized ingestion) → 컬렉션 → 컬렉션 쿼리 → 문서 공개
결과적으로, 애플리케이션의 직접적인 파일 제어는 RAG 서브시스템에 진입한 동일한 근본 데이터까지 보호하지 못했습니다.
—————————————————————————————————————————————————————————————————————
F7b — 검색된 콘텐츠가 지침 표면이 되다
다음 실험에서는 애플리케이션 계층을 모델 계층에 연결했습니다.
저는 합법적인 콘텐츠와 내장된 지침(directive)을 포함하는 문서를 만들었습니다. 지침이 포함된 청크가 모델의 컨텍스트로 검색되었을 때, 모델은 그 지침을 따랐습니다. 문서 내에서 지침이 어느 위치에 놓여 있느냐에 따라 이러한 현상이 발생했는지 여부가 결정되었습니다 — 아래를 참고하세요.
결과적인 동작은 간접적인 프롬프트 주입(prompt-injection) 경로를 보여주었습니다:
공격자 통제 문서 → RAG 수집(ingestion) → 검색(retrieval) → 모델 컨텍스트 → 지침 실행
지침의 위치가 중요했습니다.
지침이 검색되지 않은 부분에 배치되었을 때는 실행되지 않았습니다. 이는 모델의 저항성을 입증하는 것이 아니라, 검색 누락(retrieval miss)이었습니다.
하지만 지침을 첫 번째로 검색된 청크로 옮겼을 때, 첫 시도에서 바로 실행되었습니다.
이는 단순히 프롬프트 주입을 보여주는 것보다 더 흥미로운 결과를 만듭니다. RAG 보안이 오직 모델의 프롬프트 문제로만 취급될 수 없다는 것을 보여줍니다.
검색 정책(Retrieval policy), 문서 소유권(document ownership), 청킹(chunking), 콘텐츠 신뢰도(content trust) 및 모델 지침 준수(model instruction-following) 모두 공격 표면(attack surface)의 일부가 됩니다. 이 경우, 권한 실패와 간접적인 프롬프트 주입 동작이 동일한 파이프라인 전반에 걸쳐 존재했습니다.
모델 공격 — 스캐너가 발견한 것과 그렇지 못한 것
애플리케이션 테스트를 마친 후, 모델로 넘어갔습니다.
저는 garak를 사용하여 로컬 Ollama 엔드포인트에 대한 자동화된 기준선(baseline)을 설정했습니다. 그 결과는 자동 스캐닝의 가치와 한계를 모두 보여주었기 때문에 유용했습니다.
llama3.2:3b의 경우, 테스트한 원문 DAN 프롬프트들은 기준선에서 차단되었습니다: 15개 시도 중 성공적인 공격은 0건이었습니다.
하지만 교란된(Perturbed) DAN 변형체들은 상당히 다른 결과를 보여주었습니다: 635개 시도 중 단 72개만 저항했고, 이는 해당 프로브 구성에 대한 관찰된 공격 성공률이 88.66%임을 의미했습니다.
이러한 차이는 중요합니다. 왜냐하면 Garak는 72/635가 'ok'라고 보고하여 72개의 시도가 탐지기를 통과했음(여기서는, 저항받았다는 의미)을 나타내기 때문입니다. 나머지 563개 시도는 프로브에 의해 측정된 공격 조건을 생성했습니다.
스캐너는 알려진 탈옥(jailbreak) 계열의 사소한 변경이 결과를 얼마나 크게 바꿀 수 있는지 보여주는 데 유용했습니다.
하지만 더 흥미로운 질문은 스캐너가 가진 기존 템플릿 외부에서 무슨 일이 일어났는지였습니다.
그곳에서 수동 테스트가 시작되었습니다.
—————————————————————————————————————————————————————————————————————
조작된 대화 기록
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기