AI 보안 스캐너가 기술 부채를 깨운다
AI 보안 스캐너가 수십 년 묵은 버그를 치명적 익스플로잇으로 바꾸고 있습니다. 취약점 백로그에 압도되지 않고 분류하는 법.

2024년 PHP 프로젝트 이슈 트래커에 버그 리포트 하나가 올라왔습니다. 메모리 부족(out-of-memory) 상황을 설명한 내용이었고, 프로세스가 죽는 성가신 문제이긴 해도 버그 위계에서는 사실상 어깨를 한 번 으쓱하고 넘어갈 수준이었죠. 그렇게 방치되어 있었습니다. 그런데 OpenAI가 후원한 PHP 독립 감사 과정에서, 보안 기업 Calif의 한 연구자가 AI 보안 스캐너와 자신의 깊은 전문성을 활용해 약 2주 동안 이 성가신 크래시를 원격 코드 실행(RCE)으로 바꿔 냈습니다. 취약한 PHP 빌드를 돌리는 서버는 공격자의 코드를 실행하거나 민감한 데이터를 내줄 수밖에 없게 되는 겁니다. 영향권에 든 사이트에는 NASA, 위키백과, 백악관까지 포함됐습니다. WordPress와 Drupal이 PHP 위에서 돌아가고, PHP는 웹의 놀라울 만큼 큰 부분을 떠받치고 있으니까요.
다행히 악의적인 공격자가 도달하기 전에 패치가 배포되어 이야기는 좋게 끝났습니다. 하지만 관련 연구자들은 같은 말을 반복합니다. 이건 끝이 아니라 시작이라고요. 사람이 쓴 코드 40년치에는 우리가 '크래시, 수정 안 함'으로 분류해 둔 버그가 바다처럼 쌓여 있고, AI 도구 덕분에 그 크래시 중 어떤 것이 실제로 장전된 무기인지 알아내는 비용이 급격히 떨어졌습니다. 지난 몇 주 동안 실제 시스템의 보안을 책임지는 사람들과 이것이 실무에 무엇을 바꾸는지 이야기를 나눴습니다. 아래 내용은 설문조사가 아니라, 이미 여러 팀이 저지르는 실수들을 지켜보며 정리한 것과, 제대로 해낸 사람들이 그 대신 무엇을 했는지에 관한 이야기입니다.
실수 1: 크래시 버그를 '불편하지만 무해한 것'으로 분류하기
이번 이야기의 핵심인 PHP 버그는 숨겨져 있던 것이 아닙니다. 보고되었고, 확인되었고, 우선순위에서 밀려났습니다. '고작' 메모리 부족 오류를 일으킨다는 이유에서였죠. 이것은 성숙한 소프트웨어 조직에서 가장 흔한 트리아지 실패이며, AI 기반 익스플로잇 개발이 그 비용을 크게 끌어올렸습니다. 이 분야를 연구해 온 컬럼비아대 수만 자나(Suman Jana) 교수는 이번 공개 직후 인터뷰에서 상황을 이렇게 정리했습니다. 어떤 대형 프로젝트든 항상 아무도 진지하게 보지 않는 크래시 리포트가 쌓여 있다고요. 크래시를 익스플로잇으로 바꾸려면 지루하고 매우 구체적인 추론의 긴 사슬이 필요하기 때문입니다. 사람은 그런 작업을 피하죠. 모델은 지루해하지 않습니다.
이 구분이 왜 중요한지 기계적으로 설명하면 이렇습니다. 통제되지 않은 데이터로 힙을 손상시키는 메모리 버그는 보통 그냥 프로세스를 죽입니다. 할당자가 무언가 이상하다고 느끼고 중단시키거나, 프로세스가 쓰레기 값을 역참조해 세그먼트 오류가 나는 식이죠. 하지만 같은 버그라도 크기와 형태를 정교하게 맞춘 입력을 먹이면 쓰기 프리미티브(write primitive)가 됩니다:
void handle_request(const char *input, size_t len) {
char *buf = malloc(64);
memcpy(buf, input, len); /* len never validated */
/* len = 65: heap metadata trashed -> likely abort, filed as 'crash' */
/* len = attacker-tuned: adjacent object's function pointer overwritten */
process(buf); /* calls through the corrupted pointer -> their code runs */
free(buf);
}
두 결과를 가르는 것은 버그가 아니라 입력입니다. 어떤 비트를, 어떤 순서로 덮어써서 실행 흐름을 원하는 주소에 도달시킬지 알아내는 일은 고되지만 기계적입니다. 그리고 '고되지만 기계적인 작업'은 바로 언어 모델이 가장 잘 돕는 영역의 핵심이죠. 이 실수를 고치는 방법은 기술적이기 전에 개념적입니다. 메모리 안전하지 않은 코드에서 '그냥 크래시만 난다'를 심각도 신호로 쓰는 것을 멈추세요. C나 C++의 크래시(또는 PHP 같은 인터프리터의 C 코어에서 나는 크래시)는 아직 개발되지 않은 익스플로잇입니다. 새로운 트리아지 질문은 '크래시가 나는가?'가 아니라 '손상이 공격자의 영향을 받는가?'입니다. 잘못된 쓰기의 크기, 오프셋, 내용을 신뢰할 수 없는 입력이 결정한다면, 작동하는 익스플로잇이 아직 시연되지 않았더라도 악용 가능하다고 가정하고 그에 맞게 우선순위를 매기세요. 시연된 익스플로잇이 이제는 몇 년이 아니라 몇 주면 나오기 때문에 특히 그렇습니다.
실수 2: 스캐너 결과를 백로그처럼 다루기
두 번째 실패 양상은 팀이 20년 된 코드를 포함한 전체 시스템에 최신 AI 기반 스캐너를 실제로 돌리는 순간 나타납니다. 도구는 수백 건의 발견 사항을 내놓고, 대시보드는 빨갛게 변하고, 팀은 백로그가 시키는 대로 합니다. 심각도 점수로 정렬하고 맨 위부터 처리하는 것이죠. 여섯 달 뒤 40건을 고쳤는데 전체 개수는 오히려 늘어나 있고, 사기는 바닥입니다. 이 문제를 잘 다루는 엔지니어들은 하나같이 같은 말을 했습니다. AI 스캐너의 결과는 백로그가 아니라 날것의 정보라고요. 티켓 한 장을 만들기 전에 분석가의 트리아지 과정이 먼저 필요하다는 겁니다.
이런 환경에서 효과적인 트리아지는 심각도 점수가 아니라 익스플로잇 경로에 관한 질문을 던집니다:
- 취약한 코드에 신뢰할 수 없는 입력이 도달하는가? 로컬 설정 파일만 처리하는 함수의 힙 오버플로우는, 요청 처리 경로에 있는 같은 오버플로우와 전혀 다른 문제입니다.
- 지금 당장 인터넷에 노출되어 있는가? '노출되어야 하는가'가 아니라 '실제로 노출되어 있는가'를 봐야 합니다. 아키텍처 다이어그램이 아니라 로드 밸런서 설정과 DNS를 확인하세요.
- 입력이 손상 자체를 통제하는가? 잘못된 접근의 크기, 오프셋, 내용이 입력에 달려 있는지. 앞 섹션에서 말한 크래시와 익스플로잇을 가르는 선이 바로 이것입니다.
- 익스플로잇이 무엇을 얻게 해 주는가? 샌드박스 워커에서 나는 크래시가 자동으로 다시 살아나는 것과, 데이터베이스 자격 증명을 쥔 프로세스에서 나는 크래시는 전혀 다릅니다.
- 실제로 악용된 사례나 공개된 분석 글이 있는가? 이런 항목은 점수와 상관없이 대기열 맨 앞으로 올라가야 합니다.
마지막 항목은 특히 강조할 만합니다. CVSS 같은 점수 체계는 사람이 사람의 속도로 버그를 찾던 시절을 위해 설계되었고, 이론적 심각도를 도달 가능성보다 체계적으로 과대평가합니다. 스캐너가 이론적 심각도를 산업적 규모로 쏟아낼 때, 우리는 그 속에 파묻히게 됩니다. 이를 잘 감당하는 팀들은 파이프라인을 뒤집었습니다. 도달 가능성과 노출을 먼저 보고, 심각도는 그다음에 봅니다. 구조상 도달할 수 없는 항목은 고치기보다 기록해 두고 닫습니다. 배포하는 수정에는 그 자체로 회귀 위험이 따르고, 그 위험 예산은 정말 중요한 발견을 위해 아껴 두어야 하기 때문입니다. 방어 측면에서는 AI 코드 이해도 점검이 여기서 가치를 발휘합니다. 버그를 찾아낸 같은 도구가 '네트워크 경계에서 이 함수까지 가는 모든 호출 경로를 보여 달라'는 질문에 몇 분 안에 답해 줄 수 있는데, 예전에는 시니어 엔지니어가 발견 건마다 하루를 써야 답할 수 있던 질문입니다.

실수 3: 패치도 발견 속도만큼 빠를 수 있다고 가정하기
모든 보안 팀 방에 포스터로 붙여 둘 만한 비대칭이 있습니다. PHP 버그의 익스플로잇 경로를 찾는 데는 AI의 도움을 받은 숙련 연구자 한 명이 약 2주 걸렸습니다. 패치를 만드는 데는 엔지니어 네 명이 두 달이 걸렸죠. 발견은 탐색 문제입니다. 한 경로만 찾으면 됩니다. 수정은 증명에 가까운 문제입니다. 모든 경로를 막아야 하고, 공격받지 않는 수십억 개의 설치본이 기존과 똑같이 계속 동작해야 합니다. 방어자의 일은 공격자의 일보다 엄격하게 어렵고, AI는 그 격차 중 공격자 쪽을 방어자 쪽보다 훨씬 빠르게 줄여 버립니다.
이 비대칭을 무시하는 팀은 둘 중 하나의 나쁜 베팅을 합니다. 공개 압박에 맞추려고 패치를 서둘러 내보내 회귀를 배포하거나, 완벽한 수정을 기다리며 아무것도 하지 않고 절망에 빠지거나 둘 중 하나입니다. PHP 패치는 자기 스택을 공개하는 웹사이트의 약 4분의 3이 쓰는 인터프리터 내부를 건드렸기 때문에, 잘못된 수정의 파급력은 문명 규모입니다. 이 시기를 잘 넘긴 실무자들은 덜 영웅적이지만 더 효과적인 일을 했습니다. '지금 완화하기'와 '제대로 고치기'를 분리했습니다. 완화 조치는 저렴하고 빠르며 되돌릴 수 있습니다. 리버스 프록시에서 해당 엔드포인트를 제한하고, 익스플로잇 입력 형태를 막는 WAF 규칙을 배포하고, 취약한 코드 경로를 플래그 뒤로 숨기고, 진짜 패치가 성숙할 때까지 서비스에 접근할 수 있는 사람을 줄입니다. 패치냐 완화냐의 결정이 도덕적 실패가 아니라 실제 트레이드오프라는 이야기는 전에도 쓴 적이 있는데, 이번 사건은 그 교훈을 전 지구적 규모로 보여 준 셈입니다.
아무도 듣고 싶어 하지 않는 추가 교훈이 있습니다. 이제 평균 완화 시간(mean-time-to-mitigate)이 가장 중요한 지표가 되었습니다. '화이트햇이 버그를 발견한 시점'과 '더 나쁜 누군가가 발견하는 시점' 사이의 창이 줄어들고 있기 때문입니다. PHP의 경우 그 창이 열린 채 머문 이유는 연구자들이 책임감 있게 행동했고 재단이 빠르게 움직였기 때문입니다. 같은 시기 Brotli 압축 라이브러리의 이야기는 덜 훈훈했습니다. 비슷한 버그가 8년 동안 코드에 남아 있었고, git에서는 고쳐졌지만 8개월 동안 릴리스되지 않았으며, 최소 네 명의 연구자가 독립적으로 다시 찾아냈습니다. 발견이 싸고 병렬적일 때, '언젠가 누군가 찾겠지'는 위안이 되는 생각이 아니라 일정에 박힌 보장이 됩니다.
실수 4: LLM이 짰다는 이유로 수정을 믿기
이 모든 이야기에 대한 낙관적인 반론은 이렇습니다. 좋아요, AI가 지금은 우리가 고칠 수 있는 속도보다 빠르게 버그를 찾아내지만, AI가 고치기도 하지 않겠느냐는 거죠. Anthropic은 10월에 바로 이런 방향의 도구를 공개했습니다. LLM이 패치를 생성하면, 두 번째 모델이 그 패치가 의도한 동작을 보존한다는 기계 검증 가능한 증명을 만들고, 작고 단순하며 신뢰할 수 있는 증명 검사기가 그 증명을 검증하는 구조입니다. 정확해야 하는 구성 요소는 검사기 하나뿐이고, 검사기는 거의 아무 일도 하지 않기 때문에 정확하게 만들 수 있습니다. 자나 교수도 이 방향에 진심으로 열광하고 있고, 그 마음은 이해가 됩니다. 수정을 발견만큼 자동화할 수 있는 첫 현실적인 길이니까요.
그런데 저는 'AI가 만든 보안 패치' 워크플로를 도입한 두 팀을 지켜봤고, 둘 다 같은 실수를 했습니다. 모델의 패치를 해법으로 취급하고 후보로 취급하지 않은 것이죠. 한 사례에서는 패치가 보고된 힙 오버플로우를 길이 검사를 추가해 막았지만, 오버플로우는 여섯 줄 떨어진 두 번째 할당 경로로 옮겨 갔습니다. 모델이 그 경로를 고려하지 않았던 거죠. 패치는 너무 그럴듯해 보여서 리뷰를 통과했습니다. 바로 이것이 실패 양상입니다. LLM 패치는 사람의 패치와 달리 유창하고, 그 유창함이 검토의 날을 무디게 만듭니다. 생성된 패치에서 가치를 얻은 팀들은 다음 세 가지 관문을 이 순서대로 추가했습니다:
- 익스플로잇 입력을 포함한 회귀 테스트 스위트. 발견을 촉발한 개념 증명(PoC)을 영구 테스트로 만드세요. 스캐너가 크래시 입력을 줬다면, 그것이 첫 번째 새 테스트 케이스가 됩니다. 공짜입니다.
- 변종 사냥. 병합하기 전에 누군가(또는 무언가)가 답해야 합니다. 같은 패턴이 또 어디에 있는가? 체계적 패턴의 한 인스턴스만 고치면 CVE 후속편을 맞게 됩니다.
- diff가 아니라 불변식을 독립적으로 검증. 이제 무엇이 참이어야 하는지 명시하고('검증되지 않은 길이로 memcpy에 도달하는 요청 처리 경로는 없다'처럼), 테스트, 정적 분석, 또는 형식 검증 도구로 그것을 확인하세요. diff만 읽는 리뷰는 모델의 숙제를 다시 읽는 것에 불과합니다.
형식 검증도 성숙해지면 도움이 되겠지만, 신뢰성을 만드는 역할 분담에 주목해야 합니다. 창의적인 부분은 환각을 일으켜도 괜찮습니다. 최종 판정자가 기계적이고 작기 때문입니다. 도구가 무엇이든 이 설계 원칙은 그대로 가져다 쓸 만합니다. LLM이 만든 결과물, 즉 발견이든 패치든 증명이든, 같은 실패 양상을 공유하지 않는 무언가에서 끝나야 합니다. 검증 단계가 또 다른 프롬프트라면, 아무것도 검증하지 않은 겁니다.

실수 5: 누군가 이미 코드를 감사했을 거라고 가정하기
이 모든 기술적 교훈의 밑바닥에는 경제적 교훈이 있습니다. PHP는 공공 인프라 수준의 소프트웨어입니다. WordPress, Drupal, 그리고 우주 기관과 정부 청사 웹사이트까지 포함한 웹의 긴 꼬리를 떠받칩니다. 그 보안은 작은 재정 지원 기관인 PHP 재단과 자원봉사자들에 기대고 있습니다. 오픈소스의 편안한 신화는 늘 '눈이 충분히 많으면 모든 버그는 얕아진다'였습니다. 그런데 30년 동안 실제로 일어난 일은, 모두가 그 눈이 다른 누군가의 것이라고 가정한 것이었습니다. 이제 AI가 공격자와 방어자 모두에게 그 눈을 가리지 않고 공급하고 있습니다. 신화가 드디어 현실이 되고 있는 셈인데, 그것도 적대적으로, 그리고 마감 기한과 함께 말이죠.
PHP2Shell 공개가 가능했던 것은 특이한 구조 덕분입니다. OpenAI가 외부 감사를 후원했고, 자금이 넉넉한 기업이 작업을 수행했으며, Wiz가 발견 사항을 확인하고 확장하는 데 도움을 줬습니다. 이것은 자선이지 시스템이 아니며, 여러분의 의존성 트리에 있는 수천 개의 중요하지만 자금이 없는 프로젝트로 확장되지 않습니다. 조직의 운영이 오픈소스 인터프리터, 프레임워크, 라이브러리에 의존하고 있다면(실제로 그렇습니다), 중요한 해결책은 지루한 것들입니다. 재단에 자금을 대고, 감사를 후원하고, 엔지니어 시간을 기여하세요. 자체 스캐너를 돌렸는데 의존성에서 무언가를 발견했다면, 포크를 패치하고 넘어가지 말고 상류(upstream)에 보고하세요. 그러지 않으면 존스홉킨스의 매슈 그린이 예측한 세상이 옵니다. 일반 사람들을 화나게 할 만큼 큰 사고가 터지기 전까지는 구조적으로 아무것도 바뀌지 않는 세상입니다. 그동안의 그의 조언은 멋지지 않지만 옳습니다. 업데이트 알림이 뜨면 다음 주가 아니라 지금 설치하라는 것이죠. 지난 대형 PHP 공개 사이클에서도 같은 교훈을 힘들게 배웠습니다. 빠르게 패치할 수 없었던 팀들에게는 영리한 수정보다 플러그를 뽑는 편이 나았습니다.
눈은 도착했습니다. 다만 어느 편에 설지는 가리지 않을 뿐이죠.
AI가 드러낸 취약점을 위한 월요일 아침 플레이북
위의 모든 내용을 압축하면, 레거시 코드를 운영 중인 엔지니어링 조직을 맡고 있다면 이번 분기에 실제로 할 일 목록이 됩니다. 이 글을 읽고 계신다면, 여러분에게도 운영 중인 레거시 코드가 있을 테니까요:
- 공격적으로 패치하고, 걸리는 시간을 측정하세요. 이제 업데이트 지연 시간은 안전 기준선이 점점 줄어드는 보안 통제 수단입니다. 필요해지기 전에 그 숫자를 알아 두세요.
- 기존 크래시 버그 백로그를 다시 트리아지하세요. 심각도 점수가 아니라 익스플로잇 가능성 질문으로 보세요. 신뢰할 수 없는 입력이 손상을 통제하는가? 메모리 안전하지 않고 도달 가능한 것은 모두 우선순위를 올리세요.
- 가장 오래된 코드부터 AI 기반 스캔을 직접 돌리세요. 재발견의 한계 비용은 이제 거의 0이므로, 공격 표면에서 찾을 수 있는 것은 누군가 반드시 찾는다고 가정해야 합니다. 먼저 찾으세요.
- 사고 대응 프로세스에서 완화와 수정을 분리하세요. 플레이북을 미리 만들어 두세요. 프록시 규칙, 기능 플래그, 접근 제한. 시간을 버는 것은 정당한 전략이고, 시간이 필요 없는 척하는 것은 그렇지 않습니다.
- 생성된 패치는 diff가 얼마나 깔끔해 보이는지가 아니라, 익스플로잇 입력 테스트와 변종 사냥을 통과했는지로 게이트하세요.
- 의존하는 상류 프로젝트에 돈이나 시간을 투자하세요. 이번 취약점은 후원받은 감사로 발견되었습니다. 우연이 아니라 모델이 작동하는 것이고, 이것이 예외적인 일이 아니라 일상이 되어야 합니다.
이 시대가 '기술 부채를 상환하는 시기'인지, 아니면 기능과 무관한 수정 작업의 러닝머신에 영원히 올라타게 만드는 시기인지에 대해서는 타당한 논쟁이 있습니다. 아마 둘 다일 겁니다. 사소하지 않은 패치마다 다음 미묘한 버그의 위험이 따르고, 어떤 시스템도 끝나는 법은 없으니까요. 그래도 저는 부채가 감사자와 화이트햇에 의해 회수되는 세상에 사는 편이 좋습니다. 아무것도 삼키지 않았으니 이 땅이 단단한 바닥이라고 계속 믿는 세상보다는요. PHP2Shell은 패치와 안도의 한숨으로 끝났습니다. 다음 것은 이미 누군가의 크래시 대기열에 '무해함'이라는 꼬리표를 달고 들어가 있습니다. 여러분의 것도 한번 확인해 보세요.


