Check logic vulnerability point using GET/HEAD in Ruby on Rails

최근에 Github OAuth flow bypass 취약점이 공개되었습니다. 이 취약점은 Rails 앱의 특성을 이용한 취약점이고, Github만의 문제가 아니고 패치로 모든 Rails 앱을 보호할 수도 없습니다. Today, I going to review one vulnerability that needs to be checked in the Rails App environment through the Github OAuth flow bypass vulnerability. (B recently shared something interesting to me.)

그래서 어떤 내용이고 어떤 부분을 중점으로 분석해야할지 정리해봅니다.

Thanks to B for letting me test it again.

Summary

A brief description of the link below. For English, just refer to the link. :)

우선 취약점에 대해 간략히 살펴봅시다. 깃허브 사용자가 외부 앱에 로그인하는 경우 버튼을 통해 확인을 받게 됩니다. 이 과정에는 당연하게 CSRF에 대한 대응로직은 당연하게 존재합니다.

Github의 3rd App에 대한 Authorize 플로우 중 https://github.com/login/oauth/authorize 페이지가 인증 버튼을 보여주는 뷰어(GET)이자, 인증 요청이 맨 마지막 페이지(POST)이기도 합니다. Rails 앱은 기본적으로 CSRF Token으로 모든 요청을 검증하고 있지만, 이 부분에서 아래 설명할 HEAD 메소드를 이용하여 CSRF 대응 로직을 우회하고 이를 가능하게 합니다.

자세한 내용은 아래 링크를 참고해주세요. https://blog.teddykatz.com/2019/11/05/github-oauth-bypass.html

What is HEAD Method

HEAD Method는 HTTP의 기본 메소드로 바디를 포함하지 않는 헤더만을 리턴받는 메소드입니다. 보통은 파일의 크기를 가늠하기 위해서 사용하는 경우들이 있는데, 아래 RFC 문서를 보면 이렇게 명시되어 있습니다. The HEAD Method is the default method for HTTP and only the header that does not contain the body is returned. There are usually cases that you use to measure the size of a file. refer to RFC document below.

...
The server SHOULD send the same
   header fields in response to a HEAD request as it would have sent if
   the request had been a GET
...

Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content 중 HEAD

결국 HEAD 메소드는 GET과 동일한 요청 베이스를 가지는거죠. The point is that GET and HEAD behave the same.

HEAD Method in Rails

Rails에서도 HEAD Method는 동일하게 GET과 같은 취급을 받습니다. 그래서.. routes.rb에 이런식으로 명시되어 있다면, GET 뿐만이 아니라 HEAD로도 요청이 가능합니다. The point is that GET and HEAD behave the same on Rails router

Rails.application.routes.draw do
  get 'app/index'
end

Vulnerable Point

대충.. 이런식으로 코드를 하나 만들어봤습니다. 웹 요청을 받으면 GET인 경우 302로 제 블로그, 이외 케이스는 모두 307로 localhost로 가게끔 말이죠. I made a test code. For GET, 302 and all other methods are 307.

controller

class TesterController < ApplicationController
  def test_redirect
    p 'this is test_redirect'
    p "Method: "+request.method
    # 메소드를 출력 후
    # GET인 경우 제 블로그로 redirect
    if request.method.to_s == 'GET'
      respond_to do |format|
        format.html {redirect_to '[https://www.hahwul.com](https://www.hahwul.com/)', :status => 302}
        format.json {}
      end
    # GET이 아닌 경우 localhost로 리다이렉트 시켰습니다.
    else
      respond_to do |format|
        format.html {redirect_to '[http://127.0.0.1](http://127.0.0.1/)', :status => 307}
        format.json {}
      end
    end
  end

당연히 route는 get만 처리할 수 있도록 지정해줍니다.

routes.rb

Rails.application.routes.draw do
  get 'tester/test_redirect'
end

자 이제 실제로 테스트해보면 이렇습니다. 우선 GET부터 보내게 되면, 당연히 제 블로그 주소로 302 redirect 됩니다.

Test with GET Method

curl -i -k http://127.0.0.1/tester/test_redirect
HTTP/1.1 301 Moved Permanently
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Download-Options: noopen
X-Permitted-Cross-Domain-Policies: none
Referrer-Policy: strict-origin-when-cross-origin
Content-Type: text/html; charset=utf-8
Location: [https://www.hahwul.com](https://www.hahwul.com/)
Cache-Control: no-cache
X-Request-Id: f0c1e322-cbc7-4bc8-9535-84471bd8c11e
X-Runtime: 0.039037
Transfer-Encoding: chunked<html><body>You are being <a href="[https://www.hahwul.com](https://www.hahwul.com/)">redirected</a>.</body></html>

그럼 HEAD로 요청을 날리게 될 땐 어떻게 처리될까요?

Test with HEAD Method

curl -i -k http://127.0.0.1/tester/test_redirect -X HEAD
Warning: Setting custom HTTP method to HEAD with -X/--request may not work the
Warning: way you want. Consider using -I/--head instead.
HTTP/1.1 307 Temporary Redirect
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Download-Options: noopen
X-Permitted-Cross-Domain-Policies: none
Referrer-Policy: strict-origin-when-cross-origin
Content-Type: text/html; charset=utf-8
Location: [http://127.0.0.1](http://127.0.0.1/)
Cache-Control: no-cache
X-Request-Id: 3b13e501-7c8b-45be-8e45-cb43e5fabbee
X-Runtime: 0.034709

routes.rb에는 get으로 제한했기 때문에 HEAD 또한 컨트롤러로 넘어갑니다. 이후 컨트롤러에서 request.method 로 검증을 했기 때문에 같은 GET 베이스의 요청이지만 실제로 처리하는 동작이 달라지게 됩니다. A test_redirect page is only use get method because routes.rb. but passed request with HEAD Method. In the controller, GET and HEAD method a diffrent. they are crossroads.

깃헙의 경우…

if request.get?
  # serve authorization page HTML
else
  # grant permissions to app
end

인증 뷰 페이지와 auth 플로우의 마지막 페이지의 주소가 같았고, 위 처럼 request.get? 으로 검증했기 때문에 authorize 요청을 CSRF 토큰 처리 없이 전달할 수 있게 됩니다. 결국 JS를 통해 HEAD 요청을 전달하는 CSRF 코드를 만들면 사용자의 동의 없이 3rd App을 연동할 수 있게 됩니다.

In github case, address of the last page of the auth flow was the same as the authentication view page, and because the address was verified as request.get?, the authoreize request can be send without CSRF token. Eventually, when you create a CSRF code that passes HEAD requests through JS, you can added the 3rd App without your consent.

HEAD login/oauth/authorize?client_id={CLIENT_ID}&scope=read:user&authorize=1
Host: github.com

(https://not-an-aardvark.github.io/oauth-bypass-poc-fbdf56605489c74b2951/ 실제 PoC)

Conclusion

HEAD에 대한 처리 방식을 Rails의 스펙적인 부분이기 때문에 (사실 당연한 이치) Rails 자체의 문제는 아닙니다. 결국은 개발자가 로직을 구성할 때 이러한 점들이 고려되지 않은 웹에서는 분명히 문제가 될 수 있는 부분이겠죠.

결국은 매번 분석할 때 마다 컨트롤러들의 동작과 분기를 잘 살펴봐야할 것 같네요.. HTTP Method 검증에 자주 사용되는 루비 메소드들입니다.

request.method
request.get?
request.post?
etc..

Reference

https://blog.teddykatz.com/2019/11/05/github-oauth-bypass.html https://developer.mozilla.org/ko/docs/Web/HTTP/Methods/HEAD