2026년 9월 4일, 보안업체 Rapid7은 한국 자동차·미디어 기업 두 곳에서 이상한 로드밸런서를 발견했다. 겉보기엔 평범하게 웹 트래픽을 분산하는 HAProxy였지만, 그 안에는 공격자가 몰래 심어둔 코드가 함께 컴파일돼 있었다. 연구자들이 디버그 문자열에서 찾아낸 이름을 따 'Ted'라 부르는 이 백도어는 새로운 취약점을 파고들지 않았다. 대신 HAProxy가 트래픽을 검사하고 고쳐 쓰라고 정식으로 제공하는 '필터 API'라는 기능을 그대로 가져다 썼다.
Why — 로드밸런서가 트래픽을 들여다보는 이유
로드밸런서는 원래 요청을 여러 서버로 나눠 보내는 단순한 중계자가 아니다. TLS 종료, 압축, 헤더 재작성, 캐노리 배포를 위한 트래픽 분기, 요청 본문 기반 레이트리밋처럼 트래픽의 내용을 직접 들여다보고 고쳐야 하는 작업이 많다. HAProxy는 이런 작업을 하드코딩하지 않고, 요청·응답 처리 파이프라인 중간에 끼워 넣을 수 있는 '필터(filter)'라는 확장 지점으로 일반화했다. 압축 모듈이나 SPOE(외부 처리 오프로드) 같은 공식 기능도 이 필터 구조 위에서 동작한다.
문제는 이 확장 지점이 강력할수록, 그 자리를 차지한 코드가 무엇을 하는지 HAProxy 스스로는 구분하지 못한다는 점이다. 필터는 응답이 클라이언트에게 나가기 직전 단계까지 접근할 수 있어서, 정상적인 콘텐츠 변조와 악의적인 콘텐츠 변조를 프로토콜 수준에서는 가를 방법이 없다.
What — 필터 API의 구조: 생명주기와 4개 핵심 훅
HAProxy 공식 헤더 파일(include/haproxy/filters-t.h)은 필터가 구현할 수 있는 콜백을 스트림 생명주기별로 정의한다. 스트림이 시작되면 attach가 불리고, 이후 채널(요청·응답 흐름)이 분석되는 동안 아래 훅들이 순서대로 개입한다.
| 훅 포인트 | 공식 문서상 호출 시점 |
|---|
channel_start_analyze | 채널 분석이 막 시작될 때 |
http_headers | 모든 HTTP 헤더 파싱이 끝나고, 본문 파싱 전 |
http_payload | 본문 데이터를 소비할 수 있게 됐을 때 |
http_end | 요청·응답 전체가 처리되고 본문이 다 전달된 뒤 |
"Called before the body parsing, after all HTTP headers was parsed." — HAProxy include/haproxy/filters-t.h, http_headers 콜백 정의
이 네 훅만 조합하면 헤더를 바꾸고, 본문을 통째로 치환하고, 마지막에 추가 데이터를 끼워 넣는 것까지 응답의 모든 단계를 장악할 수 있다. HAProxy는 내부적으로 HTTP 메시지를 HTX라는 구조화된 버퍼로 표현하는데, 필터는 이 HTX를 직접 조작해 클라이언트가 받는 최종 바이트를 결정한다.
How — Ted가 4개 훅을 그대로 재구현한 방법
Ted는 이 공식 훅을 이름만 바꿔 재구현했다. 함수명에 trace_나 ted_ 접두사를 붙였을 뿐, 호출 시점과 역할은 공식 콜백과 정확히 일치한다.
| 공식 훅 | Ted의 구현 | 악용 목적 |
|---|
channel_start_analyze | trace_chn_start_analyze() | HTTP 파서 흐름 가로채기 |
http_headers | ted_http_headers_for_htx() | 응답 헤더 리라이팅 |
http_payload | ted_http_payload() | 응답 본문 통째로 치환 |
http_end | trace_http_end() | 처리 끝난 자리에 잔여 페이로드 추가 |
여기까지는 "필터를 악용했다"는 이야기지만, Ted는 필터 API 밖의 다른 내부 인프라까지 끌어다 썼다. 페이로드 버퍼는 HAProxy의 메모리 풀 할당기(pool_alloc())에서 가져오고, C2 응답을 비동기로 스케줄링할 때는 이벤트 스케줄러의 task_wakeup()을 그대로 호출한다. 별도의 스레드가 필요하면 pthread_create로 디태치 스레드를 띄운다. 정상 기능과 구분되지 않는 내부 API를 그대로 재사용했기 때문에, 실행 흐름만 봐서는 이상 신호가 거의 없다.
더 흥미로운 건 재시작 생존 방식이다. HAProxy는 무중단 재시작(seamless reload)을 위해 파일 디스크립터와 소켓을 부모 프로세스에서 새 워커로 넘겨준다.
"재로드 시 mworker V1은 HAPROXY_MWORKER_PIPE_{RD,WR} 환경변수에 파이프 FD를 저장해 복구하고, V2 이상은 HAPROXY_PROCESSES에 워커별 소켓페어를 직렬화한다." — HAProxy doc/internals/mworker.md 요약
Ted는 이 정식 상속 메커니즘에 자신의 C2 파이프를 얹어, 운영자가 설정을 바꾸고 HAProxy를 재로드해도 백도어 연결이 끊기지 않게 만들었다. 공격자가 reload를 직접 트리거할 권한까지는 필요 없다. 운영자의 정상적인 운영 행위(설정 갱신·재시작)가 곧 백도어의 생존 수단이 되는 구조다.
C2 통신은 이미지 파일처럼 보이는 위장 경로로 이뤄진다. 옵코드는 다섯 가지뿐이다.
| 옵코드 | 기능 | 상세 |
|---|
| 비콘 | 상태 보고 | 빌드ID·HAProxy 버전·연결 카운터 상태 반환 |
| 파일 전송 | 업로드·다운로드 | 파일을 쓰거나 읽어서 스트리밍 |
| 명령 실행 | 셸 명령 | 실행 결과를 XOR로 인코딩해 반환 |
| 설정 갱신 | 캐시 갱신 | 새 설정을 캐시 파일에 쓰고 재검증 |
암호화는 1바이트 롤링 XOR에 불과하고, 응답은 HTTP/1.0 200 OK로 포장된다. 정교한 은닉이라기보다는 "정상 트래픽처럼 보이면 충분하다"는 최소한의 위장이다.
Security — 통계 자체를 조작해 탐지를 피하는 법, 그리고 대응
가장 위험한 부분은 로그 삭제가 아니라 지표 조작이다. Ted는 HAProxy 내부의 연결 카운터(활성 연결 수·누적 요청 수·피크 트래픽 등)를 해당 릴리스에 고정된 메모리 구조체 오프셋으로 직접 찾아가, C2 트래픽이 발생할 때마다 그 값을 원자적으로 차감한다. 언더플로우 방지 로직까지 구현해뒀기 때문에, 운영자가 대시보드에서 통계를 봐도 부자연스러운 값이 보이지 않는다. 로그 삭제는 감사 담당자가 "이상하다"고 느끼게 만들지만, 지표 조작은 "이상 없다"고 확신하게 만든다는 점에서 훨씬 위험하다.
로그 쪽에서도 정밀한 방식을 썼다. bash 히스토리와 여러 시스템 로그에서 tmp, wget, cron, crond 같은 키워드가 포함된 줄만 골라 지우고 나머지는 그대로 둔다. 전체 삭제는 "누가 로그를 지웠다"는 신호 자체가 되기 때문에, 자기 흔적만 정밀 제거하는 쪽을 선택한 것으로 보인다.
Ted는 혼자 움직이지 않는다. cronie, agetty, atd, polkitd 같은 정상 프로세스 이름으로 위장한 보조 악성코드가 매시간 HAProxy 프로세스 상태를 감시하며 재시작·리로드 여부를 추적한다. 귀속은 중간 신뢰도로 북한 국가지원 행위자 쪽에 무게가 실린다. 근거는 네이버 pstatic.net 도메인을 모방한 C2 도메인 패턴과 단순한 XOR·치환 암호 스타일이다. 여기에 앞선 Lazarus·APT37 캠페인의 물 흘려마시기(watering hole) 운영 방식과의 유사성도 더해졌지만, Rapid7도 확정된 귀속은 아니라고 명시했다. 초기 침투 경로 역시 확정되지 않았고, 노출된 그룹웨어 포탈이나 메일 서버가 후보로 추정되는 정도다.
이런 공격은 트래픽 패턴이 아니라 바이너리 자체의 신뢰성을 흔든다는 점에서 대응 방향도 달라져야 한다. 실행 중인 HAProxy 바이너리의 체크섬을 공식 릴리스와 주기적으로 대조하고, 빌드 파이프라인에 재현 가능한 컴파일과 서명 검증을 넣는 게 첫 방어선이다. 통계 조작에 당하지 않으려면 애초에 HAProxy 내부 카운터만 믿지 않아야 한다. 스팬 포트나 별도 네트워크 탭에서 뽑은 트래픽량과 교차 검증하는 관측 채널을 하나 더 두는 방법이 대안이 될 수 있다. 그룹웨어·메일 서버처럼 초기 침투 후보로 자주 지목되는 인프라에서 예상 밖의 아웃바운드 HTTP 요청이 발생하는지 감시하는 것도 유효한 탐지 지점이다. 확정된 사고는 아니지만, 한국 기업 두 곳이 실제 피해자로 확인됐다는 점에서 국내에서 HAProxy를 운영하는 조직이라면 바이너리 무결성 점검을 한 번쯤 해볼 만하다.
Summary
- 필터 API 자체는 위험하지 않다. 위험한 건 그 자리를 차지한 바이너리가 신뢰할 수 있는지이며, 방어의 초점도 필터 로직이 아니라 바이너리 무결성 검증에 있어야 한다.
- 관측 가능성(로그·통계)은 그 자체로 공격 대상이 될 수 있다. 지표를 조작하는 공격은 이상을 감추는 게 아니라 "정상"이라는 확신을 만들어낸다는 점에서 탐지 전략 자체를 다시 생각하게 한다.
- 같은 논리는 HAProxy에만 한정되지 않는다. nginx·Envoy처럼 필터·모듈 기반 확장 구조를 가진 다른 인프라 소프트웨어에도 동일한 위협 모델이 적용될 수 있다.
참고 자료
안내 및 법적 고지
AI 활용 안내
이 글은 AI(Claude)의 도움을 받아 작성되었습니다.
인용된 통계와 사례는 참고 자료에 명시된 출처에 근거하며, 설명을 위한 일부 표현은 각색되었습니다.
면책 조항
본 글은 보안 인식 제고를 위한 교육 목적으로 작성되었습니다.
언급된 공격 기법을 실제로 시도하는 행위는 「정보통신망법」, 「형법」 등에 따라
처벌받을 수 있으며, 본 블로그는 이에 대한 법적 책임을 지지 않습니다.