Home CVE-2022-25765 Vulnerability
Post
Cancel

CVE-2022-25765 Vulnerability

Hack the Box Precious 문제를 풀면서 발견된 취약점
CVE-2022-25765 분석을 해봅시다! ruby 언어를 잘몰라 정확하지 않을수 있음


CVE-2022-25765

CVE mitre에 따르면 해당 취약점은 아래와 같습니다.

The package pdfkit from 0.0.0 are vulnerable to Command Injection where the URL is not properly sanitized.

pdk 0.0.0 ~ 0.8.7.2 패키지 에서 발견되었고 URL is not properly sanitized 되는 곳에서 Command Injection 취약점이 존재합니다.


Vulnerable code

precious에서 pdf로 변환 하는 서버의 코드는 다음과 같습니다.

코드를 보면 PDFKit.new(url).to_file(path) PDFKit객체를 생성하는데 to_file(path)함수를 이용하여 URL을 렌더링합니다. 만약에 url에 Query String Parameter를 포함하여 렌더링을 할 경우 Command Injection 취약점이 발생합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class PdfControllers < Sinatra::Base

  configure do
    set :views, "app/views"
    set :public_dir, "public"
  end

  get '/' do
    erb :'index'
  end

  post '/' do
    url = ERB.new(params[:url]).result(binding)
    if url =~ /^https?:\/\//i
      filename = Array.new(32){rand(36).to_s(36)}.join + '.pdf'
      path = 'pdf/' + filename

      begin
           PDFKit.new(url).to_file(path)
          cmd = `exiftool -overwrite_original -all= -creator="Generated by pdfkit v0.8.6" -xmptoolkit= #{path}`
          send_file path, :disposition => 'attachment'
      rescue
           @msg = 'Cannot load remote URL!'
      end

    else
        @msg = 'You should provide a valid URL!'
    end
    erb :'index'
  end
end

PoC Code

PoC 코드는 아래와 같습니다.

1
2
PDFKit.new("http://example.com/?name=#{'%20`sleep 5`'}").to_pdf 
# 5 seconds wait...

to_file(path) Code

to_file 함수의 코드를 보면 to_pdf 함수를 실행하고 File 객체를 생성합니다.

1
2
3
4
  def to_file(path)
    self.to_pdf(path)
    File.new(path)
  end

to_pdf(path) Code

HAHWUL 루비에서 Process/command 실행하기를 보면 IO.popen() 함수에서 command 명령이 실행되는 것을 알 수 있습니다.

to_pdf 함수는 command(path) 함수를 실행하는데 command(path) 함수에서 경로를 이스케이프 처리를 합니다.

즉 해당 문제는 이스케이프 처리를 제대로 하지 못하고 쉘 명령어가 포함된 URL이 IO.popen()함수에 들어가게되며, Command Injection이 발생하는 것으로 확인할 수 있습니다.❗️❗️❗️

1
2
3
4
5
6
7
8
9
10
11
def to_pdf(path=nil)
    preprocess_html
    append_stylesheets

    invoke = command(path)

    result = IO.popen(invoke, "wb+") do |pdf|
      pdf.puts(@source.to_s) if @source.html?
      pdf.close_write
      pdf.gets(nil) if path.nil?
    end

command(path) Code

코드를 보면 to_input_for_command 함수를 실행합니다.

1
2
3
4
5
6
7
8
9
10
11
12
 def command(path = nil)
    args = @renderer.options_for_command
    shell_escaped_command = [executable, OS::shell_escape_for_os(args)].join ' '

    # In order to allow for URL parameters (e.g. https://www.google.com/search?q=pdfkit) we do
    # not escape the source. The user is responsible for ensuring that no vulnerabilities exist
    # in the source. Please see https://github.com/pdfkit/pdfkit/issues/164.
    input_for_command = @source.to_input_for_command
    output_for_command = path ? Shellwords.shellescape(path) : '-'

    "#{shell_escaped_command} #{input_for_command} #{output_for_command}"
  end

to_input_for_command Code

to_input_for_command 함수를 보면 url이면 shell_safe_url함수를 실행합니다.

1
2
3
4
5
6
7
8
9
    def to_input_for_command
      if file?
        @source.path
      elsif url?
        %{"#{shell_safe_url}"}
      else
        SOURCE_FROM_STDIN
      end
    end

shell_safe_url & url_needs_escaping Code

pdfkit command Injection을 보면 아래의 코드가 취약한 코드라고 합니다. shell_safe_url함수는 url_needs_escaping의 값이 참인지 거짓인지 확인합니다. 참이면 escape를 하고, 거짓이면 @source 변수 그대로 반환합니다.

url_needs_escaping 함수는 @source 변수를 unescape한 결과가 @source와 동일한지 확인합니다.

해당 코드가 취약한 이유는 @sourceunescape가 될 때 입력값에 대한 아무런 유효성 검증이 없기 때문입니다. 인코딩된 공격 페이로드가 포함됬을 때 unescape가 되면서 공격이 가능하기 때문입니다.

1
2
3
4
5
6
 def shell_safe_url 
   url_needs_escaping? ? URI::DEFAULT_PARSER.escape(@source) : @source 
 end 
  
 def url_needs_escaping? 
   URI::DEFAULT_PARSER.unescape(@source) == @source 

Safe Code

마지막으로 취약점이 수정된 코드입니다. 취약한 코드에는 unescape 방식만 사용하여 비교했다면 escapeunescape 방식을 사용하여 변수를 검증합니다.

@source변수를 unescape방식을 적용후 다시 escape한 결과가 원본 @source와 다른지 확인합니다.다르면 처음부터 인코딩 되지 않았으며 이스케이프 처리할 필요가 없음을 의미합니다.

1
2
3
4
5
6
7
8
9
def escaped_url
      url_needs_escaping? ? URI::DEFAULT_PARSER.escape(@source) : @source
end

def url_needs_escaping?
      URI::DEFAULT_PARSER.escape(URI::DEFAULT_PARSER.unescape(@source)) != @source
end


escape & unescape

  • escape는 %인코딩을 사용하여 URL을 인코딩 하는 방식입니다.
  • unescape %인코딩된 URL을 원래 형식으로 변환하는데 사용하는 방식입니다.

Reference

https://github.com/pdfkit/pdfkit/issues/507 https://www.hahwul.com/2016/06/13/ruby-processcommand-execute-process-and/


후기

URL escape & unescape 처리를 잘못하면 발생하는 문제에 대해서 알게되는 계기가되었다. PoC와 여려글들을 참조하여 글을 썼지만 아직도 정확하게는 잘모르겠다…

PoC를 보면 %20된 페이로드가 url_needs_escaping 함수를 만나면서 unescape가 되고 페이로드가 IO.popen()함수에 들어가면서 터진다고 이해했다.

저런 취약점을 아무런 정보없이 찾는 사람들은 대단한거같다!! 무서운사람들…