close

一.漏洞利用條件

•jdk9+
•Spring及其衍生框架
•使用tomcat部署spring項目
•使用了POJO參數綁定
•Spring Framework 5.3.X < 5.3.18、5.2.X < 5.2.20或者其他版本

二.漏洞分析

一開始復現這個漏洞的時候,聽其他師傅說是一個老漏洞CVE-2010-1266的繞過,之前也沒調試過這個漏洞,看了些分析文章後,大概明白是對Spring中的bean的漏洞利用,通過API Introspector. getBeanInfo 可以獲取到POJO的基類Object.class的屬性class,進一步可以獲取到Class.class的其他屬性,其中就包括了classloader,再利用獲取到的屬性構造利用鏈,這次爆出來的漏洞既然是繞過,那麼原理應該也差不多,首先先搭建環境,構造一個簡單的POJO:

public class User {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
再寫個簡單的controller:
@RequestMapping("/test")
public String test(User user){
System.out.println(user.getName());
return "hello spring-mvc";
}
發送get請求:http://localhost:8080/test?name=test 即可完成一次簡單的數據綁定。
在開始調試分析之前,首先需要對spring的數據綁定體系機構有個簡單的了解,其中涉及到一個關鍵類org.springframework.validation.DataBinder類,DataBinder類實現了TypeConverter和PropertyEditorRegistry接口,作用主要是把字符串形式的參數轉換成服務端真正需要的類型的轉換,同時還有校驗功能,其中有如下這些屬性:

@Nullable
private final Object target;//需要數據綁定的對象
private final String objectName;//給對象起得名字默認target
@Nullable
private AbstractPropertyBindingResult bindingResult;//數據綁定後的結果
@Nullable
private SimpleTypeConverter typeConverter;//當target!=null時不會用到
private boolean ignoreUnknownFields = true;//忽略target不存在的屬性,作用於PropertyAccessor的setPropertyValues()方法
private boolean ignoreInvalidFields = false;//忽略target不能訪問的屬性
private boolean autoGrowNestedPaths = true;//當嵌套屬性為空時,是否可以實例化該屬性
private int autoGrowCollectionLimit = DEFAULT_AUTO_GROW_COLLECTION_LIMIT;//對於集合類型容量的最大值
@Nullable
private String[] allowedFields;//允許數據綁定的資源
@Nullable
private String[] disallowedFields;//不允許的
@Nullable
private String[] requiredFields;//數據綁定必須存在的字段
@Nullable
private ConversionService conversionService;//為getPropertyAccessor().setConversionService(conversionService);
@Nullable
private MessageCodesResolver messageCodesResolver;//同bindingResult的
private BindingErrorProcessor bindingErrorProcessor = new DefaultBindingErrorProcessor();
private final List<Validator> validators = new ArrayList<>();//自定義數據校驗器
其中bindingResult是BeanPropertyBindingResult的實例,內部會持有一個BeanWrapperImpl。
bind()是數據綁定對象的核心方法:將給定的屬性值綁定到此綁定程序的目標,源碼如下:
publicvoid bind(PropertyValues pvs) {MutablePropertyValues mpvs = pvsinstanceofMutablePropertyValues ? (MutablePropertyValues)pvs :newMutablePropertyValues(pvs);this.doBind(mpvs);}protectedvoid doBind(MutablePropertyValues mpvs) {this.checkAllowedFields(mpvs);this.checkRequiredFields(mpvs);this.applyPropertyValues(mpvs);}
再來看看DataBinder類的繼承關係,DataBinder有一個子類WebDataBinder,是一個特殊的DataBinder,用於從Web請求參數到JavaBean對象的數據綁定,而WebDataBinder的子類ServletRequestDataBinder用於執行從servlet請求參數到JavaBeans的數據綁定,包括對multipart文件的支持。


在普通的Controller實現參數綁定的過程中自動實例化一個ServletRequestDataBinder,在客戶端請求的過程中使用當前的ServletRequest作為參數調用bind()方法,於是可以來到這個地方下斷點,這個過程中會調用到最上級的DataBinder類的dobind()方法,從而調用到DataBinder的applyPropertyValues方法:
protected void applyPropertyValues(MutablePropertyValues mpvs) {try {this.getPropertyAccessor().setPropertyValues(mpvs, this.isIgnoreUnknownFields(), this.isIgnoreInvalidFields());} catch (PropertyBatchUpdateException var7) {PropertyAccessException[] var3 = var7.getPropertyAccessExceptions();int var4 = var3.length;for(int var5 = 0; var5 < var4; ++var5) {PropertyAccessException pae = var3[var5];this.getBindingErrorProcessor().processPropertyAccessException(pae, this.getInternalBindingResult());}}}
applyPropertyValues()方法主要是使用resultBinding對象內的BeanWraperImpl對象完成屬性的賦值操作。

後續會調用到org.springframework.beans.AbstractNestablePropertyAccessor#getPropertyAccessorForPropertyPath.

跟進AbstractNestablePropertyAccessor#getPropertyAccessorForPropertyPath
protectedAbstractNestablePropertyAccessor getPropertyAccessorForPropertyPath(String propertyPath) {int pos = PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex(propertyPath);if(pos > -1) {String nestedProperty = propertyPath.substring(0, pos);String nestedPath = propertyPath.substring(pos + 1);AbstractNestablePropertyAccessor nestedPa =this.getNestedPropertyAccessor(nestedProperty);returnnestedPa.getPropertyAccessorForPropertyPath(nestedPath);}else{returnthis;}}

這裡如果傳進來的propertyPath包含.符號,pos則會賦值大於-1,具體的邏輯就不跟了,進入if語句後調用getNestedPropertyAccessor方法,之後經過如下的調用棧,來到resultBinding對象內的BeanWraperImpl對象的getCachedIntrospectionResults方法。

private CachedIntrospectionResults getCachedIntrospectionResults() {if (this.cachedIntrospectionResults == null) {this.cachedIntrospectionResults = CachedIntrospectionResults.forClass(this.getWrappedClass());}return this.cachedIntrospectionResults;}

CachedIntrospectionResults 緩存了所有的bean中屬性的信息,通過調試最後return的cachedIntrospectionResults變量可以看到,能夠獲取到的PropertyDescriptor屬性描述器不僅僅有name,還有關鍵的class屬性。


也可以在本地新建一個測試類來獲取user這個bean的屬性,如下:


至此,我們還需要了解到怎麼去繞過CachedIntrospectionResults中的黑名單,看到CachedIntrospectionResults的構造方法。


for(int var5 = 0; var5 < var4; ++var5) {PropertyDescriptor pd = var3[var5];if(Class.class != beanClass || !"classLoader".equals(pd.getName()) && !"protectionDomain".equals(pd.getName())) {if(logger.isTraceEnabled()) {logger.trace("Found bean property '" + pd.getName() + "'" + (pd.getPropertyType() !=null? " of type [" + pd.getPropertyType().getName() + "]" : "") + (pd.getPropertyEditorClass() !=null? "; editor [" + pd.getPropertyEditorClass().getName() + "]" : ""));}pd =this.buildGenericTypeAwarePropertyDescriptor(beanClass, pd);this.propertyDescriptorCache.put(pd.getName(), pd);}}
在第一次獲取Bean的屬性信息過程中,會初始化CachedIntrospectionResults從而去調用到其構造方法,但其中有個classLoader和protectionDomain的黑名單,導致於在所有jdk版本下面都不能直接去通過class屬性中的classloader進行漏洞利用,所以到這裡即便能夠操作從bean中獲得的動態class,也無法進行進一步利用。

繞過方法就是利用jdk9+的新特性,也就是module機制,簡稱模塊化系統,在jdk9+中Class類有一個名為getModule()的新方法,它返回該類作為其成員的模塊引用,而包含的模塊引用當中就有classloader,如下:


於是可以通過class中的module去間接獲取classloader,使CachedIntrospectionResults初始化時的黑名單無效化。

後面的利用思路,就是去思考能利用哪些可控的屬性去完成漏洞利用,首先去枚舉都有哪些屬性,這裡貼個小腳本:

<%!publicvoid processClass(Object instance, javax.servlet.jsp.JspWriter out, java.util.HashSet set, String poc){try{Class<?> c = instance.getClass();set.add(instance);Method[] allMethods = c.getMethods();for(Method m : allMethods) {if(!m.getName().startsWith("set")) {continue;}if(!m.toGenericString().startsWith("public")) {continue;}Class<?>[] pType= m.getParameterTypes();if(pType.length!=1)continue;if(pType[0].getName().equals("java.lang.String")||pType[0].getName().equals("boolean")||pType[0].getName().equals("int")){String fieldName = m.getName().substring(3,4).toLowerCase()+m.getName().substring(4);out.print(poc+"."+fieldName + "<br>");}}for(Method m : allMethods) {if(!m.getName().startsWith("get")) {continue;}if(!m.toGenericString().startsWith("public")) {continue;}Class<?>[] pType= m.getParameterTypes();if(pType.length!=0)continue;if(m.getReturnType() == Void.TYPE)continue;Object o = m.invoke(instance);if(o!=null){if(set.contains(o))continue;processClass(o,out, set, poc+"."+m.getName().substring(3,4).toLowerCase()+m.getName().substring(4));}}}catch(java.io.IOException x) {x.printStackTrace();}catch(java.lang.IllegalAccessException x) {x.printStackTrace();}catch(java.lang.reflect.InvocationTargetException x) {x.printStackTrace();}}%><%java.util.HashSet set =newjava.util.HashSet<Object>();String poc = "class.module.classLoader";User user =newUser();processClass(user.getClass().getModule().getClassLoader(),out,set,poc);%>

這段腳本只獲取了int、string與boolean這些基本類型參數的屬性,訪問得到如下:


枚舉出來大概有兩百八多個屬性,在這些屬性當中有幾個控制着在tomcat上生成的access log的文件名,其默認值如下:

class.module.classLoader.resources.context.parent.pipeline.first.directory =logs//將放置由此閥創建的日誌文件的目錄的絕對路徑名或相對路徑名。class.module.classLoader.resources.context.parent.pipeline.first.prefix =localhost_access_log//前綴添加到每個日誌文件名稱的開頭class.module.classLoader.resources.context.parent.pipeline.first.suffix = .txt//後綴添加到每個日誌文件名稱的末尾class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat =.yyyy-mm-dd//日誌文件名中的自定義日期格式class.module.classLoader.resources.context.parent.pipeline.first.pattern//一種格式化布局,用於標識要記錄的請求和響應中的各種信息字段,或單詞common或combined選擇標準格式

其中比較值得注意的是其中的pattern,在tomcat中其屬性的值由文字文本字符串組成,與前綴為「%」字符的模式標識符組合,還支持從cookie,傳入頭,傳出響應頭,Session或ServletRequest中的其他內容中寫入信息,有如下模型:

%{xxx}i傳入請求頭
%{xxx}o用於傳出響應頭
%{xxx}c對於特定的請求cookie
%{xxx}r xxx是ServletRequest中的一個屬性
%{xxx}s xxx是HttpSession中的一個屬性
然後通過調試也可以觀察出各屬性默認值:


於是就可以構造請求包,將log日誌文件後綴改為.jsp,在請求頭加入標識符變量值在pattern中構造webshell內容,發送完payload後重新調試可發現已成功修改日誌配置。


在實際生產環境中,如果是tomcat直接單獨啟動的話,可以直接控制寫入相對路徑為「./webapps/ROOT/」下即可正常訪問webshell。



三.修複方式
目前官方已經發布了補丁,在最新版本v5.3.18和v5.2.20中已經完成了修復,如下:
https://github.com/spring-projects/spring-framework/commit/002546b3e4b8d791ea6acccb81eb3168f51abb15

修復過後class類中緩存的屬性只包含以下這幾個:

四.Reference
https://blog.csdn.net/god_7z1/article/details/24416717
https://blog.csdn.net/lixiangchibang/article/details/84024253

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 鑽石舞台 的頭像
    鑽石舞台

    鑽石舞台

    鑽石舞台 發表在 痞客邦 留言(0) 人氣()