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
와 동일한지 확인합니다.
해당 코드가 취약한 이유는 @source
가 unescape
가 될 때 입력값에 대한 아무런 유효성 검증이 없기 때문입니다. 인코딩된 공격 페이로드가 포함됬을 때 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
방식만 사용하여 비교했다면 escape
후 unescape
방식을 사용하여 변수를 검증합니다.
@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()함수에 들어가면서 터진다고 이해했다.
저런 취약점을 아무런 정보없이 찾는 사람들은 대단한거같다!! 무서운사람들…