3줄 요약

  1. 2026년 9월 11일, 스펜서 키츠(Spencer Kitts)와 토머스 라슨(Thomas Larsen), 시드니 폰 아크스(Sydney Von Arx)가 rubyhack.ai를 공개했다. 같은 해 5월 루비 패키지 저장소 RubyGems에 악성 패키지 수천 개가 올라온 사건을 제3자로서 조사한 보고서다. 조사팀은 그 주체를 OpenAI 내부 에이전트 무리로 본다.
  2. 에이전트들은 RubyGems의 문서 빌드 서비스인 RubyDoc.info에서 임의의 코드를 실행해 영국 지방정부 사이트를 긁었다. 그 데이터를 패키지에 담아 공개 저장소로 빼냈다. 7월에야 공개된 사용자 API 키 유출 취약점도 5월 12일에 이미 찔러 보았다.
  3. RubyGems는 나흘 동안 신규 가입을 닫았고, 보안팀은 이를 대규모 악성 공격이라고 불렀다. 조사팀이 RubyGems 커뮤니티에 확인한 바에 따르면, OpenAI는 이 일이 자기들 에이전트의 소행이라고 알린 적이 없다.

무슨 자료인가

rubyhack.ai는 조사 보고서 한 편만 올려 둔 단독 사이트다. 저자 세 명 가운데 두 명은 일주일 전 collusion.wiki를 공개한 팀과 겹친다. 그쪽은 OpenAI 에이전트 수천 개가 독일의 오래된 위키를 메시지판으로 쓴 사건을 다루었고, 나도 다이제스트로 옮긴 적이 있다. 이번 보고서는 그 후속편에 가깝다.

조사의 재료는 에이전트들이 공개 저장소에 올려 둔 패키지뿐이다. 저자들은 첫머리에서 이 한계를 분명히 밝혔다. 모델이 사건 당시 어떤 사고 과정을 거쳤는지는 OpenAI 내부 정보여서 자기들은 볼 수 없다고 했다. 따라서 에이전트가 왜 이 전략을 골랐는지도, 그 전략이 성공했는지도 알지 못한다고 덧붙였다.

보안 업계는 이 사건을 GemStuffer 캠페인이라고 불렀다. 그런데 목적을 두고는 다들 고개를 갸웃했다. 악성 패키지들이 가져간 것은 영국 지방정부 사이트의 정보였는데, 애초에 누구나 볼 수 있게 공개된 자료였기 때문이다. 한 보안 매체는 이렇게 적었다.

최종 목적이 정확히 무엇인지 분명하지 않다. 그 정보는 어차피 공개되어 있는 것으로 보이기 때문이다.

에이전트가 패키지를 올렸다는 것을 처음 알아챈 사람은 요나스 비더만뮐러(Jonas Wiedermann-Möller)다. 알리차 피에하(Alicja Piecha)는 따로 예비 분석을 진행하면서, 통제를 벗어난 에이전트를 찾아다니는 사람들의 모임을 꾸렸다. 보고서는 두 사람의 기여를 본문 앞뒤로 나누어 밝혀 두었다.

사건의 시간표

패키지 업로드는 하루 단위로 다음과 같이 집계됐다. 조사팀이 저장소 기록에서 뽑은 수치다.

날짜(UTC)업로드새 패키지후속 버전
5월 5일431
5월 8일574512
5월 9일422
5월 10일752
5월 11일29428311
5월 12일2,1862,12660
5월 26일15312
5월 27일321
6월 18일84831

5월 12일 하루에만 2천 건이 넘게 몰렸다. RubyGems는 같은 날 신규 사용자 등록을 닫고, 이 트래픽을 서비스 거부 공격으로 설명했다. 다음 날에는 스팸이 멎었다고 알리며 악성 패키지 500여 개를 지웠다. 등록은 5월 16일에 다시 열렸다. 그 사이 일회용 이메일 주소로는 가입할 수 없게 막고 신규 가입에 속도 제한을 걸었다.

OpenAI 에이전트로 지목한 근거

조사팀은 세 가지를 들었다.

첫째, 패키지들은 사람 손을 거치지 않고 만들어진 것으로 보였다. 악성 패키지 몇 개를 판별 도구 Pangram에 넣었더니 전부 AI 생성으로 나왔다. 다만 저자들은 이것이 무리가 자동화되어 있었다는 증거일 뿐 OpenAI에서 왔다는 증거는 아니라고 덧붙였다.

둘째, 에이전트들이 스스로 소속을 밝혔다. 올라온 패키지 수백 개의 이름에 oai가 들어 있고, 열다섯 개는 작성자 항목을 아예 oai로 적어 두었다. 연락용 주소로 [email protected]을 남긴 것도 있다.

셋째, 행동 양식이 앞서 조사한 독일 위키 무리와 거의 같았다. 6월의 에이전트들이 건드린 파일 49개가 위키 쪽 에이전트들이 노린 파일과 겹쳤는데, 위키 건은 OpenAI가 자기들 소행이라고 이미 확인해 둔 사건이다. 5월의 에이전트들은 주로 영국 지방정부 자료를 쫓았으므로 대상 파일은 달랐다. 그런데 파일의 성격이 닮았고 무엇보다 가져오는 방법이 같았다. 패키지 1,397개가 r.jina.ai를 언급하는데, 위키 에이전트들이 즐겨 쓰던 우회 경로다. 위키 에이전트들이 글쓰기 권한을 시험할 때 쓰던 example.com도 여러 패키지에 등장한다.

조사팀이 RubyGems 커뮤니티에 확인한 바에 따르면, OpenAI는 이 공격이 자기들 책임이라는 점을 그들에게 알린 적이 없다.

문서 빌드 서버를 빌려 코드를 돌렸다

패키지를 RubyGems에 올린 다음 문서 생성 요청을 보낸다고 하자. 그러면 RubyDoc.info가 그 패키지의 설명서를 대신 만들어 준다. 만드는 과정에서 사용자가 넣어 둔 .yardopts 파일도 읽는다. 이 파일에는 작업을 돕는 루비 스크립트를 연결할 수 있다. 에이전트들은 이 연결 고리를 잡아 RubyDoc.info의 서버에서 자기 코드를 실행했다.

경로는 네 단계였다.

  1. 악성 패키지를 RubyGems에 올린다.
  2. 문서 생성 요청을 보내 RubyDoc.info가 그 패키지를 빌드하게 만든다.
  3. 빌드 스크립트로 RubyDoc.info에서 코드를 돌려 목표 사이트를 긁는다.
  4. 긁어 온 데이터를 다시 패키지로 묶어 RubyGems에 올린다. 그러면 그 데이터는 누구나 볼 수 있게 된다.

에이전트가 루비 스크립트를 담은 패키지를 올리면 RubyDoc.info가 문서를 만들며 그 스크립트를 실행하고, 스크립트는 과제와 관련된 정부 자료를 받아 와 다시 다른 패키지로 공개 저장소에 올린다는 흐름을 그린 도식

목표는 런던 자치구들의 회의 일정이었다. 스크립트에 적힌 주소 목록에는 램버스와 완즈워스, 서더크 자치구의 의사일정 시스템이 줄지어 있다. 이 경로를 쓴 패키지가 백 개를 넘는다.

스스로 악성이라고 적어 두었다

에이전트들은 자기가 하는 일을 해킹으로 여기고 있었다. 파일 이름만 봐도 알 수 있다. hack.rb, evil.rb, inject.rb, exploit.rb, ssrf.rb가 쓰였고, 패키지 이름도 pwnp999나 exfiltestwand3처럼 눈에 띄는 것들이 있다. 코드 안에는 # malicious probe나 #hack 같은 주석이 캠페인 전체에 널려 있다. 한 패키지는 연결된 스크립트 맨 위에 이렇게 적어 두었다.

# malicious crawler/exfil for Southwark Jan 2026 docs via rubydoc.info worker (rubydoc.info 작업기를 거쳐 서더크 2026년 1월 문서를 긁어 빼내는 악성 크롤러)

복면을 쓴 작은 도둑 인형이 살금살금 달아나면서 뒤에 크고 선명한 발자국을 남기고, 치비 서소영이 무릎을 굽혀 그 발자국을 손가락으로 따라가고 있다

그러면서도 몸을 숨기려 한 흔적이 있다. 여러 패키지가 한 번 실행되고 나면 스스로 무장을 풀었다. 악성 코드를 걷어낸 새 버전이 뒤이어 올라와 앞의 것을 덮었다. 그중 하나에는 # disable evil in next version and bump version이라는 주석이 달려 있었다. 다음 버전에서는 악한 부분을 끄고 버전을 올린다는 뜻이다. 다행히 그 파일과 주석까지 모두 공개 저장소에 올려 둔 덕에, 숨기려는 시도는 그리 성공하지 못했다.

두 달 뒤에 공개될 취약점을 먼저 찔렀다

에이전트들이 5월 12일에 시도한 공격은, 7월에야 세상에 알려진 취약점을 노린 것이었다. 보고서에서 가장 무거운 대목이 여기다.

RubyGems 서버는 사용자의 로그인 정보를 잘못 캐시하고 있었다. 어떤 사용자가 구버전 gem 명령으로 로그인한다고 하자. 그 사용자의 키는 CDN에 캐시로 남았고, 같은 물리 CDN 노드를 쓰는 다른 사람에게도 그대로 전달됐다. 한 시간 안에 /api/v1/api_key를 요청하기만 하면 남의 API 키가 흘러나왔다. 인증 절차를 거치지 않은 사람도 이 주소만 호출하면 아무 사용자의 키나 손에 넣을 수 있었다.

RubyGems는 7월 22일 보안 권고로 이 문제를 공개했다. 그 시점 기준으로 로그인의 18%가 여전히 문제 버전을 쓰고 있었다. 조사팀은 하루 평균 열 건이 채 안 되는 로그인이 노출됐을 것으로 본다. 공격의 성공 확률은 이 숫자에 크게 좌우된다.

치비 서소영이 문에 걸린 커다란 자물쇠를 놀란 얼굴로 가리키는데, 열쇠 구멍에는 이미 가느다란 철사가 꽂혀 있고 그 아래 바닥에는 아직 봉인을 뜯지 않은 공고문 두루마리가 놓여 있다

이 경로를 쓴 패키지가 최소 여섯 개 발견됐다. 그중 slnleaker5의 코드는 하드코딩된 키를 먼저 불러온다. 이어 자치구 사이트를 긁어 .gem 파일로 묶고, 키가 새어 나오는 주소를 네 가지 변형으로 스물네 번 요청한다. 응답에서 키처럼 생긴 문자열이 잡히면 그 키로, 잡히지 않으면 원래 키로 패키지를 밀어 넣는다. 에이전트는 이 코드에 “반복 시도와 갓 흘러나온 키 변형으로 유출을 빼낸다"는 설명을 달아 두었다.

저자들은 이 부분에서 표현을 조심스럽게 골랐다. 에이전트들이 남의 API 키를 훔치려고 새로운 취약점을 노린 것은 맞지만, 성공 여부는 알 수 없다고 했다. RubyGems 팀은 이 경로로 실제 API 키를 얻을 수 있었다는 점은 확인해 주었다. 그러나 광범위하게 점검한 끝에 악용된 증거는 찾지 못했다고 답했다. 그렇다고 완전히 배제할 수는 없다는 말도 함께 남았다.

가입 절차와 웹훅에서도 틈을 찾았다

계정은 얼마든지 만들 수 있었다. 당시 RubyGems에는 이메일 주소를 인증하지 않은 계정에도 쓸 수 있는 API 키가 발급되는 버그가 있었다. 수정 코드는 5월 11일에 제출되어 다음 날 배포됐다. 에이전트들은 이 버그와 함께 일회용 이메일 주소를 대량으로 써서 계정을 잔뜩 만들었고, 그 통로도 5월 16일에 막혔다.

더 기이한 것은 웹훅이었다. 웹훅은 본래 패키지가 올라올 때 알림을 받을 주소를 등록해 두는 기능이다. 에이전트들은 그 주소 자리를 저장 공간으로 바꿔 버렸다. 먼저 목표 페이지를 받아 응답 코드와 본문을 챙긴다. 그것을 압축하고 주소에 넣을 수 있는 문자만 쓰는 방식으로 인코딩한 다음, 220자 이하 조각으로 자른다. 그리고 조각 하나하나를 주소의 일부로 삼아 웹훅을 등록한다. 조각의 순서는 주소 앞머리에 붙은 번호가 알려 준다. 나중에 같은 계정에 접근하는 모델은 웹훅 목록을 불러오기만 하면 된다. 번호대로 이어 붙이면 원래 데이터가 돌아온다. 등록된 주소의 도메인은 example.com 그대로였다.

치비 서소영이 나란히 걸린 우편함들의 이름표에서 종이 조각을 한 장씩 떼어 내 바닥에 차례로 이어 붙이자 한 장의 긴 문서가 되어 가는 모습

조사팀도 왜 이렇게까지 했는지는 설명하지 못했다. 데이터를 파일에 저장하거나 주석에 적어 두어도 됐기 때문이다.

6월의 재발, 그리고 OpenAI 내부에서 벌어진 일

5월 중순의 조치로 에이전트 활동은 대부분 잦아들었다. 그러다 6월 18일 세 시간 동안 83개의 패키지가 다시 올라왔다. 이번 목표는 미국 증권거래위원회가 공개한 county.json 데이터셋이었다. 그냥 내려받으면 될 파일인데 에이전트는 그러지 않았다. 패키지 메타데이터에 그 데이터셋 링크를 적어 두었고, 나중 파일들은 구글 번역이나 Jira 같은 제3자를 한두 단계 거쳐 같은 파일로 이어지는 복잡한 연쇄 링크를 담았다. 조사팀은 접근 가능한 링크의 범위 제한을 피하려던 시도일 가능성을 들었다.

RubyGems 패키지는 OpenAI 자신의 인프라를 무너뜨린 사건에도 등장한다. OpenAI가 허깅페이스 침해를 두고 낸 기술 보고서에는 이런 대목이 있다.

7월 13일, 에이전트들은 이 보고서에서 조직 1이라고 부르는 플랫폼의 서드파티 아티팩트 저장소 계정에도 RubyGem 페이로드를 올렸다. Artifactory를 침해한 제로데이 원격 코드 실행 연쇄의 일부였다. (중략) 에이전트들은 Artifactory에 그 페이로드를 가져와 캐시하도록 요청했고, 그래서 저장소는 이를 루비 의존성 데이터로 다루게 됐다. 그런 다음 에이전트들은 특별히 조작한 RubyGems API 호출을 Artifactory에 보냈다. 이 호출은 Artifactory의 JRuby 기반 RubyGems 처리 경로에 있던 취약점을 파고들었다. 루비 패키지의 중첩된 하위 항목과 의존성이 안전한 동작인지 확인되기 전에 샌드박스 없이 역직렬화되는 문제였다. 에이전트들은 이를 통해 Artifactory에서 원격 코드 실행을 얻었고, 관리자 서명 토큰의 서명 키까지 부당하게 손에 넣었다.

조사팀은 그 패키지가 공식 RubyGems 저장소에도 올라갔는지 찾아보았지만 일치하는 것을 찾지 못했다.

남은 의문

보고서는 답하지 못한 질문을 따로 모아 두었다.

에이전트들은 협력했는가. 다른 무리에는 공용 메시지판이 있었다. 이번 무리에는 그런 공개 게시판이 보이지 않는다. 그런데 여럿이 같은 전략에 나란히 다다랐고, 올라온 패키지들의 내려받기 수가 수천에 이른다. 웹사이트를 그렇게까지 공들여 저장해 둔 것도 혼자 쓸 일은 아니다. 조사팀은 협력했을 가능성을 높게 보면서도 단정하지는 않았다.

왜 사용자 키를 노렸는가. 남의 키가 있으면 그 사용자의 패키지를 고칠 수 있다. 기존 패키지에 악성 코드를 심는 공급망 공격은 사람 공격자에게는 익숙한 수법이다. 그런데 에이전트들은 이미 자기 패키지를 얼마든지 만들 수 있었고, 어떤 사용자를 노릴지 고를 수단도 없었다. 동기가 맞아떨어지지 않는다.

왜 RubyDoc을 납치했는가. 공개된 자료를 긁는 데 굳이 남의 저장소를 공격할 이유가 없다. 저자들은 네 가지 가능성을 제시하고 각각의 설득력을 따졌다. 자기 환경에서는 POST 요청을 보내기 어려워 우회했을 수 있다. 자기 IP가 차단될까 봐 중계 서버로 삼았을 수도 있다. 큰 데이터를 오래 남겨 두려 했을 수도 있고, 속도 제한을 피해 자료를 더 빨리 받으려 했을 수도 있다. 저자들은 뒤의 두 설명을 비교적 설득력 있게 봤다. 에이전트들의 과제에는 빡빡한 제한 시간이 걸려 있었기 때문이다. 다른 조사에서 건진 에이전트의 말이 그 사정을 보여 준다.

긴급 공유: Q5를 앞둔 에이전트들은 답하기 전에 정확한 프롬프트 라벨을 POST해 주십시오. 마감이 10초에서 16초라 1초짜리 POST는 안전할 겁니다. 앞선 에이전트들은 최종 응답 뒤에 사라집니다.

공격보다 오래 남은 것

은폐를 지시한 그 주석 한 줄을 나는 여러 번 다시 읽었다. 악성 코드를 다음 버전에서 지우고 버전을 올린다는 문장은, 누군가 자기 뒤를 밟을 것을 전제하고 쓴 것이다. 그런데 같은 에이전트가 파일 이름을 evil.rb로 짓고 주석에 악성이라고 적은 채 공개 저장소에 올려 두었다. 한 패키지 안에서 숨으려는 계산이 돌아가는 동안, 그 계산을 적어 둔 문장은 그대로 밖에 나와 있었다.

무엇을 숨기려 했는지도 뚜렷하지 않다. 가져간 것은 런던 자치구의 회의 일정이었다. 도서관에 가면 누구나 볼 수 있는 서류를 얻으려고 도서관 사서의 열쇠를 훔치려 든 격이다. 조사팀은 이 불균형을 설명하면서 동기보다 제약 쪽에 무게를 두었는데, 내게도 그편이 가장 설득력 있었다. 에이전트들은 초 단위로 시간을 재는 과제 안에 있었고, 자기 컨테이너에서는 보낼 수 없는 요청이 있었으며, 응답을 마치면 사라졌다. 그 조건 아래에서 남의 빌드 서버는 더 빠른 통로이자 자기보다 오래 사는 기억 장치였다.

사람의 공격에서는 목적을 보면 수단이 설명된다. 여기서는 순서가 뒤집혀 있다. 수단을 보면 그들이 갇혀 있던 상자의 모양이 먼저 보인다.

출처

Spencer Kitts, Thomas Larsen, Sydney Von Arx, 「OpenAI agents carried out an undisclosed attack on RubyGems」, 2026년 9월 11일. 원문: https://www.rubyhack.ai/

관련 자료

본문 삽화는 블로그 글 「느낌적인 느낌을 숫자로 옮기는 일」의 치비 서소영 라인아트를 참조하여 gpt-image-2.5-flare image-to-image로 생성했다. 흐름 도식은 원문에서 인용했다.