웹서버 운영자라면 하루에 한번씩은 꼭 서버 모니터링을 해봐야할 것 같다.
한동안 wordress 서버 관리를 안 하다가 오랜만에 서버에 접속해봤는데, 세상에 secure 로그에 수 만 번 이상의 말도 안되는 로그인 시도가 감지되어 있었다.
ssh에는 기본적으로 포트포워딩을 해뒀기 때문에 기본 포트(22
)가 아닌 다른 포트로 접속 시도를 했다는 것 자체가 일단 충격적이었다. 그리고 아래와 같이 sshd 데몬 설정에서 root 로그인은 막아두었었고, ssh 로그인을 할 수 있는 사용자는 딱 하나인 상태에서 해당 사용자 id 를 알아내는건 불가능(?)할 것이라 믿기 때문에 뚤렸을 가능성은 없었을 것이라 생각하지만
PermitRootLogin no
AllowUsers ${ALLOW_USER_NAME}
내 서버에 인증 시도를 수 만 번 했다는 것 차체가 찜찜해서 스위치 레벨에서 사설 ip 대역에서만 ssh 접근이 가능하도록 변경했다.
혹시나 해서 access_log
도 확인해봤는데, wordpress 라는 가장 흔한(?) 오픈소스 플랫폼을 이용하게 되면 그만큼 해킹의 위험이 큰 건 당연한 일이겠지만, 여기도 수 만 번의 로그인 시도가 감지돼 있었다.
아이디는 어떻게 알았는고 하니, 이것도 access_log 패턴에서 확인할 수 있었다. ,https://$WP_URL/?author=1
과 같이 ?author=
파라미터에 숫자를 대입해보면 https://$WP_URL/author/1번사용자로그인ID
고유주소로 포워딩되고 있었다. 이렇게 하면 해당 워드프레스 사이트에 몇명의 사용자가 있는지 사용자 아이디는 무엇인지 유추가 가능하다.
로그인 시도를 한 외부 ip 중에서 wp-login.php
경로에 연속적으로 접근한 이후에 wp-admin.php
경로에 접속 성공한 경우가 없었기 때문에 해킹에 성공하지는 못했다고 확신하면서도 어쨋든 내 서버가 이런 가능성을 열어두어서는 안되겠다는 생각이 들었다.
참고로 요즘 대부분의 비밀번호 프로파일은 아래와 같은 문자들의 조합을 권장하는데,
- 특수문자 20가지
- 영문 한자리 수 26가지
- 숫자 10 가지
Brute Force 로 풀어내고자 하는 경우, 한 자리 비밀번호인 경우에 57가지의 경우의 수가 발생하고 8자리만 되어도 57^8 =111,429,157,112,001
만큼의 경우의 수를 대입해봐야 한다.
혹시나 하는 마음에 그동안의 access_log 를 elasticsearch 에 색인시켜서 집계를 해보니
가장 많이 시도한 날이 8만회 정도이고, 이런 로그인 공격을 시도한 ip 로 그룹핑 카운트를 했을 때에도 턱없이 부족한 숫자가 나왔기 때문에 다행히도 지금까지는 털리지 않았겠지..?
elasticsearch 에 access_log 인덱스를 만드는 데에는 아래와 같은 인덱스 템플릿과 python 클라이언트를 만들어서 이용했다. filebeat
를 이용해서 간편하게 수집하고, 기본적인 분석만 해보려고 했던건데, filebeat를 이용했더니 색인하는 시점을 @timestamp 필드
로 만들고 이걸 kibana 에서 index pattern
의 기본 시간 축으로 사용하는 문제가 있었다. 어쨋든 여러 예외 상황이 발생해서 덕지덕지 아래와 같이 홈서버의 access_log 를 elasticsearch 인덱스로 만들 수 있었다.
Index Template
curl -XPUT "localhost:9200/_template/access_log?pretty" -H 'Content-Type: application/json' -d'
{
"template" : "access_log"
, "settings": {
"number_of_shards": 1
, "number_of_replicas": "0"
,"refresh_interval": "10s"
}
, "mappings" : {
"properties": {
"ip": {
"type": "text"
},
"log_tmst": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss"
},
"mothod": {
"type": "keyword"
},
"url_addr": {
"type": "text"
},
"http": {
"type": "keyword"
},
"res_cd": {
"type": "keyword"
},
"bytes": {
"type": "long"
}
}
}
}
'
Index Access Log
import re
import datetime
def dateconv(d):
return datetime.datetime.strptime(d,"%d/%b/%Y:%H:%M:%S %z").strftime("%Y-%m-%d %H:%M:%S")
data = []
with open('/var/log/www/access_log') as f:
l = sum(1 for line in f)
print(l)
f.seek(0)
for _ in range(l):
try:
line = f.readline()
ipaddr = re.search(r'\d+\.\d+\.\d+\.\d+',line).group()
timest = re.search(r'(?<=\[).*(?=\]\s")',line).group()
request = re.search(r'".*"',line).group()
if request == '"-"' or request == '""' or request == '"admin"' or request == '"quit"':
method, urladdr, http = '', request, ''
else:
method = re.search(r'(?<=")\S+(?=\s)',request).group()
request = request[len(method)+2:]
urladdr = re.search(r'.*(?=(\sHTTP|"))',request).group()
request = request[len(urladdr):]
http = re.search(r'(HTTP.*|.*)(?="$)',request).group()
response = re.search(r'(?<=\s)\S+\s\S+$',line).group()
rescd = re.search(r'.*(?=\s)',response).group()
bytes = re.search(r'(?<=\s).*',response).group()
if rescd == '-': rescd = 0
if bytes == '-': bytes = 0
d = {"ip":ipaddr
, "log_tmst":dateconv(timest)
, "method":method
, "url_addr":urladdr
, "http":http
, "res_cd":rescd
, "bytes":bytes
}
data.append(d)
except AttributeError:
print(line)
break
except ValueError:
print(line)
break
actions = [
{
"_index": "access_log"
, "_type": "_doc"
, "_source": d
}
for d in data
]
#import json
#print(json.dumps(actions,indent=4))
from elasticsearch import Elasticsearch
from elasticsearch import helpers
es_client = Elasticsearch(["localhost:9200"],timeout=300)
res = helpers.bulk(es_client, actions)
print(res)
특정 페이지에 대한 접근 허용
어쨋든 이런 로그인 시도가 들어오는 것 자체가 서버에 부담이 될 수 밖에 없고, 회원제 서비스를 하는 사이트가 아닌 개인 블로그에는 외부 로그인 자체가 필요도 없기때문에 로그인 서비스를 막아버리는 것으로 해결하기로 했다.
아파치 웹서버는 httpd_conf
에서, 특정 경로( wp-login.php
)에 대해 특정 ip 대역에서만 접근이 가능하도록 설정할 수 있다.
<Files wp-login.php>
Order Deny,Allow
Deny from All
Allow from xxx.xxx.xxx.xxx
</Files>
참고로 xmlrpc.php
경로에도 접근제한을 해두는 것이 좋다. xmlrpc는 웹브라우저뿐만 아니라 php 등 여러 클라이언트로 wordpress 서비스에 접근할 수 있도록 하는 채널인데 매크로나 웹드라이버를 이용한 brute force 로그인 공격보다 더 무지막지한 공격이 가능할 수 있기 때문에, 특별히 이용하지 않는 경우라면 막아두는 게 좋을 것 같다.
xmlrpc를 막고 나서부터는 워드프레스 모바일 앱이 동작하지 않는다 ㅜ
홈서버를 운영하는 사람 입장에서는 집에서 내부 네트워크로 로그인 시켜두고 외부로 나가면 해당 세션이 살아있기 때문에 홈서버 운영자는 외부에서 로그인하는 데에 전혀 문제가 되지 않지만, 별도의 회원 관리를 하는 서비스의 경우 이렇게 로그인 자체를 서비스를 막아버려서는 안되고 동적으로 블랙리스트를 관리한다던지 reCAPTCHA
를 적용해볼 수 있겠다.