카테고리 없음

[토스뱅크 부트캠프] 팀프로젝트 Interrupt 2주차

yejiamoe 2026. 1. 6. 23:57

안녕하세요 

TIL 15주차이자 팀프로젝트 2주차 진행중입니다..(사실 TIL이 많이 밀려서 많이 늦게 작성중이긴해요..)

 

이번주에는 뭔가 조금이라도 뭐를 실행시키고 뭐가 출력되고 뭐가 연결되고 하는거에 초점을 맞췄습니다. 저번주에 제가 제일 답답했던 부분이 아직 아무것도 안보여서 감이 안잡힌다는 점이였기 때문이죠. 그래서 저번주에 찾은 poc코드를 기준으로 kali에서 node.js의 ubuntu환경으로의 공격을 실행해보고 간단한 falco룰 정책을 실행해서 falco 로그들을 wazuh로 보내는 과정부터 구현해보았습니다. 이후 APM, Zeek, Arkime 설치, 그리고 멘토님의 피드백 정리 정도가 2주차에 정리할 내용들이 될 것 같습니다.

 

React2Shell 적용

저번주 TIL에 React2Shell에 대한 개념을 이미 정리했는데..또 이 github보면서 하려니까 또 헷갈리더라구요. 그래서 다시 한번 정리해보려고 합니다.

https://github.com/kdh379/react2shell-poc

 

GitHub - kdh379/react2shell-poc

Contribute to kdh379/react2shell-poc development by creating an account on GitHub.

github.com

기본적으로 React2Shell이라는 이름 자체가 React환경(정확히는 Next.js)에서 시작해 최종적으로 Shell(시스템 권한)을 획득하기 때문에 이러한 이름이 붙여졌습니다. 핵심은 저번주와 같이, 데이터로 취급되어야 할 문자열이, 서버의 로직을 속여 실행 가능한 코드로 변한다는 점에 있습니다. 

 

일단 github에 서술된 내용을 제가 구성한 환경인 kali -> ubuntu 환경에 적용했을 대 어떤 현상을 일으키는지 정리해보겠습니다.

1. 침투 시작

: 공격자인 kali가 fetch를 통해 일반적인 POST 요청을 보냅니다. 하지만 여기에는 2가지 트릭이 있습니다. 

  • Next-Action 헤더 : 서버에게 이건 단순한 데이터 전송이 아닌 server 가 실행해야하는 server action이라고 속임
  • 조작된 FormData : 단순한 사용자 이름 텍스트가 아니라 참조 문자열을 보냅니다.

(여기서 참조 문자열을 보낸다는 것은 Next.js의 Flight 프로토콜에서 발생하는 메커니즘입니다. 쉽게 말하면 Flight 프로토콜도 다른 프로토콜처럼 자신만의 암호규칙 같은게 있겠죠? 프로토콜은 효율적인 통신을 하기 위해서 각각 규칙이 있을 텐데 Flight 프로토콜의 핵심 규칙이 특정 기호로 시작하는 문자열은 단순한 글자가 아니라 참조로 해석한다는 것입니다. 쉽게 말해서 "hello"는 그대로 hello로 인식할 텐데 "$1"은 글자 $1로 이해하는 것이 아니라 1번 인덱스에 있는 데이터를 가져오라고 이해해서 막 다른 bye같은 텍스트로 인식을 해버릴 수 가 있는거죠. )

 

2. 서버의 착각 (역직렬화 & 프로토타입 오염)

: ubuntu 서버는 받은 데이터를 객체로 역직렬화합니다. 이때 Next.js의 취약한 로직이 발동하게 됩니다. 

  • 참조 해석 : 서버는 $1이 뒤에 오는 데이터를 가리킨다고 생각하고 연결해버립니다.
  • 프로토타입 침투 : __proto__라는 키워드를 만나면 서버는 이 데이터를 객체의 속성이 아닌 자바스크립트의 프로토타입을 수정하는 명령어로 오해합니다.
  • Thenable 덫 : then이라는 속성을 주입함으로써 자바스크립트 엔진은 이 객체를 결과가 나오면 실행해야할 Promise 작업이라고 착각하게 됩니다.  

(여기서 then이라는 속성은 저도 처음들었는데 then이라는 단어가 붙은 객체를 무조건 실행해야하는 중요한 함수라고 생각하게 됩니다. 그래서 프로토타입 침투로 프로토타입을 오염시키면서 그 안에 then:"악성코드" 형태로 심어두면 공격자가 이 "악성코드"를 무조건 실행하게 됩니다. )

 

3. 폭발

: 가장 결정적인 단계로 객체가 Promise처럼 취급되면서 then 메서드가 호출되고 이때, 공격자가 주입한 constructor.constructor가 실행됩니다. 

  • 비밀통로 찾기 : 자바스크립트의 모든 데이터는 자신을 만든 부모(생성자)에 대한 정보를 가지고 있고, 이때 배열([])의 부모는 Array라는 함수입니다. 그리고 이 Array의 부모가 자바스크립트 전체를 관장하는 Function입니다. 이때, 공격자는 보안 장비를 피하기 위해 [].constructor.constructor라는 경로를 통해 Function 마법사를 불러냅니다. 
  • 코드 변신 : 위에서 불려낸 Function 마법사는 단순한 문자열을 살아움직이는 코드로 바꾸는 능력이 있는데 prefix에 담긴 "process.mainModule.require('child_process').execSync('id')"라는 문자열을 보내면 RCE를 가지게 됩니다.

(_prefix라는 상자에 담긴 저 긴 문자열을 서버에게 보내면 서버는 저 긴 글을 읽고 require('child_process') 부분은 OS와 대화할 수 있는 통신관을 불러오게 되고 execSync('id') 부분은 통신관을 통해 id를 출력하라라는 뜻입니다. 즉, 저 통신관이 리버스쉘이 되고 RCE가 되는 겁니다. )

 

이렇게 제가 조사한 PoC 코드가 어떤 취약점으로 동작하는지 이해하고, 실제로 강의실 환경에 구축해보았습니다.

 

기본 환경 구축

네트워크 구성도

위 사진이 저희 프로젝트 1차 네트워크 구성도입니다. 저번주에 설명하였듯이, 저희조는 각 언어별로 파트를 나누어서 맡았기 때문에 제 PC에는 vmware를 위에서 kali와 node.js를 위한 web서버를 ubuntu위에 구축하였습니다. 그리고 FW의 OPNsense와 WAF의 ModSecurity, 그리고 Wazuh server는 강의실 내 남는 PC 하나에 구축하였습니다.

 

네트워크 설정

사실 지금 다 구축한 상태에서 보면 네트워크 연결이 가장 애를 많이 썼던 것 같습니다. 사실 초반에는 모두 다 강의실 내 남는 PC 하나에 java, php, node.js를 올릴 계획이였는데 어쩌다가..제 PC에 개인적으로 막 연습하고 있었는데 또 찾아보니까 NAT 말고 Bridge로 연결하면 또 연결이 된다고 해서..ㅎㅎㅎ 그래서 위에 설명한 것과 같은 구성이 되었습니다. 이때 뭔가 Bridge연결해서 어! 공격 된다 성공했다! 생각했는데 또 다시 생각해보니까 저희 네트워크 구성도 상, kali랑 ubuntu가 Bridge를 통해서 그냥 Wazuh 서버랑 연결되서 로그가 잘 쌓이네~에서 끝나면 안되는 거였습니다...비록 kali랑 ubuntu가 vmware바로 옆에 있고..NAT로는 쉽게 연결되지만...kali에서 ubuntu로 바로 넘어가면 안되고 방화벽이자 라우터 역할을 하는 OPNsense를 꼭 지나가게 했었어야 했습니다. (OPNsense-WAF는 이미 연결해둔 상태) 뭔가 이거를 생각 안하고 무작정 눈에 보이는 뭔가를 구축하고 싶다는 생각에 좀 더 과정이 꼬이면서 어렵게 느껴졌습니다. 

 

생각보다 Bridge설정은 간단합니다.(사실 GUI버전도 있는데...저는 GUI랑 CLI 둘다 했다가 한번 꼬였습니다..하하)

1. virtual network editor & vm 네트워크 설정

Bridge라는 단어가 보이면 그것을 선택하면 됩니다!!

2. 게이트웨어 설정 

저 파일 들어가서 라우터 부분에 OPNsense주소를 적으면 됩니다!!

이렇게 간단한데...음...아무튼 저는 뭔가 이슈가 많았었습니다..ㅜ

 

이 환경에서는, 

제가 kali에서 ubuntu ip주소로 공격을 보내면

-> OPNsense를 거쳐

->WAF를 거쳐

->ubuntu에 도착하여 공격이 실행되고

->이 과정이 falco를 통해서 프로세스단의 로그가 남게 되고

->flaco에 남는 로그를 ubuntu에 있는 wazuh agent가 읽어 json형태로 저장하고

->wazuh agent는 wazuh server로 로그들을 보내고

-> wazuh에는 node.js뿐만 아니라 java,php 로그들도 한번에 볼 수 있습니다.

 

Ubuntu & Kali 설정

victim 서버와 attacker 호스트를 어떻게 구성했는지 정리하겠습니다. 

1. Victim 서버 - Ubuntu 22.04

: 취약한 애플리케이션 구동 + 커널 레벨 위협 탐지 + 중앙 관게 서버(Wazuh Manager)로 전송

  • 주요 구성 요소
    • Docker & Compose: 취약한 React/Next.js 환경을 컨테이너화하여 격리된 상태로 빠르게 구동했습니다.
    • React2Shell PoC App: 실습의 핵심 타겟입니다. 깃허브의 PoC 코드를 기반으로 서버 사이드 렌더링(SSR) 및 서버 액션 취약점이 포함된 환경을 포트 3000번에 올렸습니다.
    • Falco (Detection Engine): 커널 시스템 콜을 감시하고 특히 최신 커널 환경에 최적화된 modern-ebpf 방식을 적용하여 성능 저하 없이 실시간 침해 사고를 포착합니다.
    • Wazuh Agent (The Messenger): Falco가 탐지한 로컬 로그(.json)를 수집하여 중앙 SIEM 서버(Wazuh Manager)로 안전하게 전송합니다. 이를 통해 개별 서버의 로그를 통합 관리할 수 있는 파이프라인을 완성했습니다.
💡 구축과정에서 발생한 트러블슈팅
1. DNS 설정 이슈
- 처음에 Falco와 Wazuh 패키지 설치시 리포지토리를 불러오지 못하는 오류가 반복되서 답답했었는데 서버의 DNS 설정 문제가 원인이였습니다. /etc/resolv.conf에 8.8.8.8 설정을 수동으로 해준 뒤에 해결하였습니다. (근데 원래 자동으로 되어있는것 아닙니까??!?!?!? 왜 나한테만...이런일이...)
2. 로그 파일 접근 권한
- falco가 생성한 falco_events.json파일을 wazuh에이전트가 읽지 못하는 문제가 발생했습니다. falco는 root권한으로 로그를 남기기 때문에 chmod 644 명령어로 에이전트가 파일을 읽고 서버로 전송할 수 있도록 권한을 조정하여 해결하였습니다.
3. GPG 키 등록 오류
- 이건 뭐 표준 가이드라인대로 키를 등록해도 오류가 나는 경우가 있다고 하는데 그냥 수동 등록으로 쉽게 해결 가능합니다.

도커로 잘 올라간 모습 + 3000포트로 잘 열린 모습 입니다.
wazuh agent 설치 후 wazuh 서버와도 잘 연결된 모습입니다.

 

2. Attacker - Kali Linux

: 서버의 취약점을 뚫고 들어가 명령어를 실행하는 주체

  • 주요 구성 요소
    • Node.js & NPM
    • Exploit Code

공격도 잘 성공하고 falco룰도 잘 탐지되어 critical 로그도 잘 뜨는 모습입니다.
이 공격을 Wazuh 서버에서 보면, critical 알람도 뜨고 상세보기하면 cat /etc/shadow한것까지 흔적이 자세하게 다 보입니다.

 

APM 설정

상관분석 중 핵심은 "공격자가 보낸 페이로드가 실제로 코드 내 취약점 지점에 도달했는가?"를 증명하는 것입니다. 이때 프로세스 부분에 대한 로그 추출은 Falco로 가능하지만 애플리케이션 부분은 불가능합니다. 때문에 저희는 Elastic APM을 도입하였습니다.

  • APM의 역할과 도입 이유
    • 코드 레벨 가시성 : Falco는 커널을 보고 APM을 이용해 함수 호출과 객체 내부를 봅니다.
    • 공격 증명 : React2Shell의 핵심인 프로토타입 오염이 일어나는 순간을 포착하여 에러 로그를 남깁니다.
    • 상관분석의 연결성 : WAF 단계와 Falco 단계 사이에서 실제 코드가 실행되었음을 알리는 트리거 정보를 제공합니다.

때문에 APM을 자바 스크립트의 proxy 객체를 사용하여 감시 카메라 역할을 하도록 합니다. 서버내의 중요한 객체를 proxy로 감싸고, 공격자가 __proto__, constructor 같은 민감한 키에 접근하여 값을 쓰려고 시도하면 프록시가 이를 가로채서 즉시 APM에 Prototype Pollution Attempted 에러를 전송하게 합니다.

 

좀 더 자세히 말씀드리자면,,, 공격자가 JSON.parse등을 통해서 조작된 데이터를 밀어 넣을 때 서버 내부에서는 4단계의 과정이 일어납니다. 1단계는 서버가 시작되면, 공격의 타겟이 되기 쉬운 핵심 객체들을 new Proxy(target, handler)로 감쌉니다. 2단계로는 handler안에 set이라는 트랩을 만드는데 이 트랩은 객체에 새로운 값을 쓰려는 공격을 받자 마자 무조건 실행됩니다. 3단계로는 set 함수 내부에서는 공격자가 건드리려는 key값이 있는데 이 key와 지금 공격자가 건들이는 부분이 같으면 경고를 전송하게 합니다. 

if (key === '__proto__' || key === 'constructor') {
    // 비상벨 울리기! (APM 전송)
    sendToAPM("🚨 Prototype Pollution Attempt detected!");
    return false; // 값 변경 거부
}

 

코드를 보면 좀 더 이해가 쉽지 않나요?ㅎㅎ 그래서 이 key 값이 일치하면 바로 APM 알람을 보내고 false를 리턴하는그런 흐름 입니다. 

  • 구축과정

next.js는 자체적인 서버 라이브러리를 내자앟고 있어서 사용자가 수정할 수 있는 app.js 파일이 없습니다. 때문에 따로 instrumentationHook을 켜줍니다.

: 일반적인 node.js 구조와 next.js는 좀 달라 instrumentation.ts라는 특수한 hook을 사용하여 구축하였습니다.

    • 에이전트 초기화 (instrumentation.ts): 서버가 시작될 때 가장 먼저 실행되어 APM 에이전트를 가동합니다.
    • 공유 인스턴스 (lib/apm.ts): 애플리케이션 어디서든 동일한 APM 감시자를 사용할 수 있도록 '싱글톤(Singleton)' 패턴을 적용했습니다.
    • 환경 설정 (next.config.ts): APM 관련 패키지들이 빌드 시 누락되지 않도록 serverExternalPackages 설정을 추가했습니다.
    • 도커라이징: docker-compose.yml을 통해 서비스 이름, 서버 주소, 샘플링 비율 등의 환경 변수를 주입했습니다.

 

 

도커라이징 하는 모습입니다. 이때 ELASTIC_APM_SERVER부분에만 server 주소를 작성해주면 됩니다. 저는 옆에 짝궁이 apm 통합 server를 설치해주었기 때문에 192.168.10.58로 변경해주었습니다.
왼쪽 그림은 next.js APM이 켜져서 로그들이 쌓이고 있는 화면, 오른쪽이 APM server가 kibana로 로그를 보여주는데 이 kibana에 잘 쌓이는 모습입니다.

💡 구축과정에서 발생한 트러블슈팅
1. 인스턴스 파편화
- 여러 파일에서 require('elastic-apm-node')를 각각 호출하면서 실제 실행된 객체와 에러를 잡으려는 객체가 달라 로그 유실 문제가 발생하였는데 lib/apm.ts를 만들어 단 하나의 APM 객체만 사용하도록 통일시켰습니다.
2. 트랜잭션 누락
- 최신 APM 에이전트는 transaction이라는 테두리 안에서 발생한 에러만 서버로 전송합니다. 따라서 단순 에러 캡쳐는 무시되는 경우가 많았는데 API 라우트(route.ts)에서 startTransaction을 수동으로 호출하여 모든 공격 감지 로그가 확실히 전송되도록 보장하였습니다.
3. 의존성 및 빌드 오류
- Next.js 빌드 과정에서 require-in-the-middle 같은 하위 패키지 버전 충돌로 인한 도커 빌드 실패 오류는 Dockerfile에서 캐시를 무시하고 패키지를 새로 설치하도록 수정하고 pnpm대신 안정적인 설치 방식으로 변경하였습니다.
4. 서비스 이름 불일치
- 환경 변수와 코드 내에 serviceName이 달라 Kibana 대시보드에서 데이터 분류가 되지 않았습니다. 대체...제가 왜 그렇게 설정했는지는...모르겠습니다. 제정신이 아니였나보죠ㅎㅎ..........^^

 

Zeek 및 Arkime 설치

그다음은 저희 조 공유서버에 Zeek와 Arkime을 추가로 구축하는 역할을 맡았습니다. 

 

먼저 Zeek에 대해서 좀 알아보겠습니당~ 일단 zeek는 단순히 침입을 막는 도구가 아니라 네트워크에서 발생하는 모든 사건을 구조화된 데이터로 변환해주며 기록까지 해주는 장치입니다. 일반적인 IPS/IDS는 패킷이 공격인 것 같으면 차단하는데 집중하지만 Zeek는 누가, 언제, 어디서, 어떤 방식으로 대화했는지 상세한 기록을 남기는 역할입니다. 이전에 정리했듯이 conn.log(연결로그), http.log(웹 요청), ssl.log(인증서 정보)등을 텍스트로그로 실시간 추출해줍니다. 

 

저희 프로젝트에서는 역직렬화 공격의 상관분석을 위해서는 zeek가 꼭 필요합니다. 

공격자가 React2Shell 페이로드를 보냈을 때, 서버 내부 로그(APM, Falco)만 있으면 어디서 온 공격인지 확신하기 어렵습니다. 이때 Zeek는 공격자의 Source IP와 TLS 지문을 가장 먼저 기록해서 이 공격이 외부의 어떤 도구로부터 시작되었는지 물리적인 증거를 제시합니다. 뿐만 아니라 인증서 정보와 암호화 방식도 기록합니다.

 

이제 구축 과정에 대해 설명드리겠습니다.

1. 네트워크 미러링 환경 구축

: 트래픽을 감시하기 위해서는 zeek 센서가 네트워크 흐름을 볼 수 있는 통로를 OPNsense 미러링을 통해서 합니다.

  • Dual-NIC 구성
    • Management (ens33) : SSH 접속 및 관리용 (IP가 할당되어 있어야함)
    • Monitoring (ens38) : 트래픽 수집용 (IP 할당되어 있지 않아야하며 무차별모드/Promiscuous Mode로 활상화)
  • Netplan 설정
    • 감시용 인터페이스(ens38)가 IP를 갖지 않도록 설정하고, 스텘 모드로 작동하게 하여 보안성을 높이고 네트워크 간섭을 최소화하였습니다.

2. Zeek 설치 및 핵심 설정

 

  • 저장소 등록: 최신 보안 패키지를 위해 오픈수세(OpenSUSE) 리포지토리를 활용했습니다.
  • node.cfg 최적화: Zeek가 어떤 입(Interface)으로 트래픽을 먹을지 결정하는 단계로 interface=ens38만 지정해주면 됩니다.
  • 하드웨어 자원 할당: 로그 데이터가 쌓이는 것에 대비해 별도의 디스크(/data)를 마운트하고, 정확한 타임스탬프 기록을 위해 Chrony로 시간 동기화를 마쳤습니다.

zeek 잘 깔린것도 확인 가능하고 제가 zeek에서 확인해야할 로그가 다 저 파일 안에 있는 것 확인 가능합니다.

3. OPNsense와 연결

 

  • VMnet1 브릿지: OPNsense의 특정 인터페이스와 Zeek의 감시용 인터페이스를 동일한 가상 네트워크(VMnet1)에 묶어줍니다.
  • 트래픽 검증: tcpdump -i ens38 -n 명령어를 통해 실제로 외부 트래픽이 Zeek 센서로 흘러 들어오는지 실시간으로 확인했습니다.

OPNsense와 미러링 부분 연결해주면 zeek에서 로그가 잘 들어오는 것을 확인가능합니다.

 

4. 통합 관제 역할 Wazuh 연결

  • Wazuh Agent 설치: 매니저 IP(10.0.30.100)를 지정하여 에이전트를 설치했습니다.
  • 로그 포워딩: Zeek의 로그가 저장되는 경로(/opt/zeek/logs/current/*.log)를 Wazuh의 ossec.conf에 등록하여 실시간으로 보안 이벤트가 전송되도록 설정했습니다.

 


마치며

아 힘드네요. 힘들어요.

사실 또 그렇게 막 힘들진 않아요.ㅎㅎ 생각보다 일어나자마자 아무생각 없이 출발해서 시작하면 금방 밥 먹을 시간 되고 또 오후도 그음방 지나갑니다..ㅎㅎ 

 

이번주는....일단 뭐 조금이라도 뭘 한 것 같기도 하고 .. 또 크리스마스 주간이라서 많이 못한 것 같기도 하고...참...모르게써용 하핫

음음 일단 할 것도 많고 뭐 잘 모르겠어서 회고도 잘 못 쓰겠어요..좀 일단..쉴게욤ㅠ