2020. 6. 25. 00:39ㆍ보안 & 해킹/정보 보안
MongoDB가 해킹당하다
MongoDB를 주로 사용하는데, 회사 개발 서버에 가끔 DB가 사라지는 마법(?)이 일어났다. 개발서버라서 신경안쓰다가 두어번 반복되자 로그를 찾아보았다.
dropDatabase 명령어로 테이블을 무자비하게 날려버리며 HOW_TO_RECOVERY_BASE.README를 생성하였다.
공격자가 생성한 DB를 들어가보니 안들어가졌다. (사실 이때 root 계정으로 들어갔어야 했었던것 같은데...)
공격지ip주소가 러시아로 확인되었다.
페이스북 커뮤니티에 질의응답을 하였다.
댓글들은 mongodb 웹서버 어플리케이션에서 타고 들어온 것과 네트워크 통한 전파 등을 의심하는 분들이 많았다. 하지만 나는 외부저장소나 코딩실수로 인한 DB정보 노출보다는 취약점 또는 스캐너에 의한 침입이라고 의심하였다.
보통 침해사고에서 APT공격과 같이 공수와 시간을 들여 공격한다면 로그상 시간 간격에서 수동(직접 손으로)으로 한 티가 난다. 근데 로그에서 보다시피 일순간(0.5초)에 데이터베이스를 삭제한 후 collection을 생성한다는 것은 자동화된 도구일 확률이 높다.
열흘 후 신경쓰지 않고 방치해두던 DB가 또 날아가 있는 것을 보고 README 파일을 읽을 수 있었다.
요약하자면 그렇다. DB를 암호화한 후 비트코인을 요구하는 랜섬웨어다.
이메일 get_base@tuta.io 키워드를 토대로 구글링해보았다.
비트코인의 경우 공개 주소에 신고를 하는 시스템 또는 사이트들이 있다. 그래서 최근에 해킹당했다고 신고한 피해자가 댓글을 달아놓은것이 보였다.
트랜젝션 주소다. 두군데를 찾았는데 한 곳은 BTC가 전송된 내역을 볼 수 있었다.
이쯤에서 나는 자존심이 팍 상했다. 개발 서버라 귀찮아서 IP접근제어를 하지 않은거라 설정하면야 문제가 없겠지만 어딜 내가 놓친건지 몰랐기 때문이다.
허니팟(고의로 취약하게 만들어 놓은 미끼용 서버)을 구축해보고 공격방법을 알아보기로 했다. DB서버를 외부에 노출시키지 않으면 물론 당장은 안전하겠지만 어느 경로로 들어온 것인지 알 수 없었기 때문이다.
허니팟 구축 및 로그 옵션 설정
혹여 모르니 GCP에서 VPC 망을 분리한 후 더미데이터를 받아 서버를 구축했다.
더미데이터를 받은 후 아래 명령어로 DB를 복원한다.
mongoimport --db testapi --authenticationDatabase testapi --username hp --password mongopass --collection users --drop --file primer-dataset.json
root권한을 가진 계정으로 설정을 진행한다.
ADMIN의 로깅레벨을 공식문서를 보고 설정하였다.
Profiling Level 설정
db.setProfilingLevel(1, 2) 로 설정하면 앞의 인자는 얼마나 verbose 한지를 나타내며 뒤의 인자는 ms단위 보다 느린 요청을 기록한다는 뜻이다. 즉 1수준 만큼 2ms보다 느린 쿼리에 대해 로깅을 수행한다.
근데 다른 사이트를 보며 참고했지만 해당 설정만으로는 공격자의 쿼리를 보기 위한 로그를 기록할 수 없었다.
Profiling Level 확인
db.getProfilingStatus()
Log Level 설정
db.setLogLevel(2)
인자값을 1로 주었을 경우 cli에서 실행커맨드를 기록한다. 하지만 DB조회 쿼리는 기록하지 않는다. 2를 주었을 경우 너무 양이 많았지만 모든 쿼리를 기록했다(예를 들어 document 조회나 수정도 포함한다). 따라서 2로 설정한 후 반복되는 불필요한 로그는 파싱(grep -v)으로 제외했다.
Log Level 확인
db.getProfilingLevel()
Query Log 확인
db.system.profile.find().pretty()
db.system.profile.find({millis:{$gte:1}}).sort({ts:-1})
db.getCollection('system.profile').find({})
해당 명령어는 query하는 사용자 및 접속정보 등을 확인한다. 하지만 내가 필요한 것은 공격자의 조회 쿼리다. 결론은
db.setLogLevel(2)
설정이 중요하다.
로깅 정보 삭제
db.setProfilingLevel(0)
db.system.profile.drop()
한줄로 로깅 정보 삭제
db.setProfilingLevel(0); db.system.profile.drop(); db.setProfilingLevel(2);
로그를 파일로 내보내기
mongo-tools에서 제공하는 mongoreply 등을 이용한 패킷등을 떠보려 했지만 손이 많이가는데다 편하지 않아서 넘어갔다. 도커로 구동중이었으므로 로그를 파일로 내보내었다.
도커 로그를 파일로 내보내기
docker logs -f <yourContainer> &> your.log &
도커 log rotate 설정
vim /etc/logrotate.d/docker-container
>>> 경로에 파일 쓰기
/var/lib/docker/containers/*/*.log {
rotate 100
daily
compress
size=1M
missingok
delaycompress
copytruncate
}
>>> 설정 설명
rotate 7 : 최대 log.1, log.2 등 7개의 파일을 보관하고
daily : 날마다 rotate 시키며
compress : 이전 로그는 압축하며
size=1M : 크기가 1메가 바이트를 넘으면 로테이트,
missingok : 해당 로그가 없어도 ok,
delaycompress :
copytruncate : 복사본을 만들고 크기를 0으로
로그 검색
로그 기록 설정이 끝났으므로 몇일에 걸쳐 공격자가 어떤 쿼리를 날리는지 침해로그를 분석해보았다.
세션 스캐너 검색
grep "connection accepted from" *-json.log.1 | grep -v 127.0.0.1
세션 스캐너는 TCP 세션만 확인하고 포트가 열려있는지만 확인한다. 이때 "connection accepted from" 로그가 기록된다.
로그인 검색
grep "Successfully authenticated" *-json.log.1 | grep -v 121.123
MongoDB에 세션 이후 로그인까지 성공하면 "Successfully authenticated"로그가 기록된다.
로그인 드라이버 검색
grep "driver: { name:" *-json.log
로그인 드라이버는 공격자 또는 접속자의 환경을 출력한다. 자주 보이는 로그는 "PyMongo" 드라이버가 유독 많았다.
공격 쿼리 및 유형
궁금했던 공격 쿼리를 알 수 있었다. 궁금했던 것에 비해 별건 없었지만 그래도 궁금증은 해소되었다.
빌드 정보 확인
db.runCommand( { buildInfo : 1 } )
사용할 수 있는 DB리스트 조회 (admin db에서 쿼리)
db.runCommand( { listDatabases : 1 })
서버가 마스터인지 확인
db.runCommand( { isMaster : 1 })
공격자 IP NAC 우회를 위해서인지 독특한 쿼리
hostInfo: "localhost.localdomain:27017"로 속임
driver정보 또한 mongodb internal client
{"log":"2020-05-21T21:59:21.865+0000 I NETWORK [conn165] received client metadata from 45.67.15.5:41926 conn165: { application: { name: \"MongoDB Shell\" }, driver: { name: \"MongoDB Internal Client\", version: \"4.2.3\" }, os: { type: \"Linux\", name: \"CentOS Linux release 7.7.1908 (Core)\", architecture: \"x86_64\", version: \"Kernel 3.10.0-1062.12.1.el7.x86_64\" } }\n","stream":"stdout","time":"2020-05-21T21:59:21.865722706Z"}
{"log":"2020-05-21T21:59:21.865+0000 I COMMAND [conn165] command admin.$cmd appName: \"MongoDB Shell\" command: isMaster { isMaster: 1, hostInfo: \"localhost.localdomain:27017\", client: { application: { name: \"MongoDB Shell\" }, driver: { name: \"MongoDB Internal Client\", version: \"4.2.3\" }, os: { type: \"Linux\", name: \"CentOS Linux release 7.7.1908 (Core)\", architecture: \"x86_64\", version: \"Kernel 3.10.0-1062.12.1.el7.x86_64\" } }, $db: \"admin\" } numYields:0 reslen:257 locks:{} protocol:op_query 0ms\n","stream":"stdout","time":"2020-05-21T21:59:21.865729226Z"}
{"log":"2020-05-21T21:59:22.113+0000 I COMMAND [conn165] command admin.$cmd appName: \"MongoDB Shell\" command: whatsmyuri { whatsmyuri: 1, $db: \"admin\" } numYields:0 reslen:64 locks:{} protocol:op_msg 0ms\n","stream":"stdout","time":"2020-05-21T21:59:22.113628076Z"}
{"log":"2020-05-21T21:59:22.368+0000 I COMMAND [conn165] command admin.$cmd appName: \"MongoDB Shell\" command: buildInfo { buildinfo: 1.0, $db: \"admin\" } numYields:0 reslen:1334 locks:{} protocol:op_msg 0ms\n","stream":"stdout","time":"2020-05-21T21:59:22.36900718Z"}
msgLen 671088768 로 굉장히 길어서 에러 출력
{"log":"2020-05-21T23:29:59.943+0000 I NETWORK [listener] connection accepted from 68.183.94.129:51066 #179 (2 connections now open)\n","stream":"stdout","time":"2020-05-21T23:29:59.943835075Z"}
{"log":"2020-05-21T23:29:59.944+0000 I NETWORK [conn179] recv(): message msgLen 671088768 is invalid. Min 16 Max: 48000000\n","stream":"stdout","time":"2020-05-21T23:29:59.944109363Z"}
{"log":"2020-05-21T23:29:59.944+0000 I NETWORK [conn179] Error receiving request from client: ProtocolError: recv(): message msgLen 671088768 is invalid. Min 16 Max: 48000000. Ending connection from 68.183.94.129:51066 (connection id: 179)\n","stream":"stdout","time":"2020-05-21T23:29:59.944189285Z"}
{"log":"2020-05-21T23:29:59.944+0000 I NETWORK [conn179] end connection 68.183.94.129:51066 (1 connection now open)\n","stream":"stdout","time":"2020-05-21T23:29:59.94424704Z"}
{"log":"2020-05-21T23:30:00.101+0000 I NETWORK [listener] connection accepted from 50.116.7.112:48580 #180 (2 connections now open)\n","stream":"stdout","time":"2020-05-21T23:30:00.101528337Z"}
{"log":"2020-05-21T23:30:00.167+0000 I NETWORK [listener] connection accepted from 68.183.94.129:51238 #181 (3 connections now open)\n","stream":"stdout","time":"2020-05-21T23:30:00.168107225Z"}
{"log":"2020-05-21T23:30:00.168+0000 I NETWORK [conn181] recv(): message msgLen 100670976 is invalid. Min 16 Max: 48000000\n","stream":"stdout","time":"2020-05-21T23:30:00.16864472Z"}
{"log":"2020-05-21T23:30:00.168+0000 I NETWORK [conn181] Error receiving request from client: ProtocolError: recv(): message msgLen 100670976 is invalid. Min 16 Max: 48000000. Ending connection from 68.183.94.129:51238 (connection id: 181)\n","stream":"stdout","time":"2020-05-21T23:30:00.168777692Z"}
Http 프로토콜을 요청
27017에 대고 왜 HTTP를 요청하는지는 모르겠다.
{"log":"2020-05-22T01:37:59.167+0000 I NETWORK [conn253] Error receiving request from client: ProtocolError: Client sent an HTTP request over a native MongoDB connection. Ending connection from 139.162.131.144:56540 (connection id: 253)\n","stream":"stdout","time":"2020-05-22T01:37:59.16729543Z"}
결론
앞서 당했던 해킹의 결론은 설정 잘못으로 인하여 admin 권한을 제한하지 않은 듯 하다. 기본 설정만 따라하여 구축했었던 것인데 문제가 있었던 듯 하다.
또한 MongoDB 기본 포트 27017이 열려있다면 접속 후 db.runCommand({buildInfo:1})은 제한없이 쿼리 가능하다. 해당 명령 실행이 바로 문제를 발생하는 것은 아니지만 이는 암호를 모르더라도 기본적으로 요청이 가능하므로 잠재적인 Banner Grabbing(배너정보 수집)이 가능하다.
허니팟을 돌리며 약 한달간의 로그를 기록하였다. 추가적인 공격 형태 등도 필요하다면 흔한 공격방법은 아래 로그파일로 대부분 확인할 수 있을 것이다. 필요하면 받아서 분석 해보길 바란다.
'보안 & 해킹 > 정보 보안' 카테고리의 다른 글
가짜 2TB USB 만들기 (0) | 2020.01.12 |
---|---|
Rekall info (0) | 2018.03.22 |
OWASP Top 10 2017년 영문, 한국어 버전, 주요정보통신기반시설 기술적 취약점 분석평가 가이드 (0) | 2017.12.16 |
포렌식 공부를 위한 자료 (0) | 2017.11.12 |
정보보안기사 실기 10회 기출 복원 (22) | 2017.11.11 |