分析
分析源码可知,后端对文件的关键处理如下依次进行:
- 判断这个文件是否已经被上到服务传器的临时文件存储目录
- 设置上传文件的目标目录,并检查该目录是否存在和可写
- 检查上传文件的拓展名是否在允许范围内
- 检查上传文件的大小是否超过限制
- 检查服务器上是否已经存在相同名称的文件
- 文件移动到目标路径
- 重命名
文件上传upload-labs 第18关 条件竞争-CSDN博客
上一关突破点是有这么一个逻辑顺序不严谨,在文件移动之后才进行的拓展名验证,所以可以在文件移动之后检查删除之前这个时间窗口动手。这一关同理可以在文件移动之后重命名之前这一个时间窗口动手。
难点就是这一关拓展名检查在前是没有问题的,它先检查了文件后缀等很多信息判断是否合法才决定是否移动到目标路径,检查文件信息不符合会直接结束上传过程。而且它是一个白名单,只能上传那几种文件。
但是它是先文件移动到目标路径才重命名的,我们就有可能可访问到上传的原始的文件名,只能不能直接上传.php后缀的文件,必须是白名单规定的后缀。
观察这个白名单数组发现还允许了一些html、doc、xls、7z等后缀格式,这与任务给出的“请选择要上传的图片:“根本没关系。由此就只能想到中间件解析漏洞了。
由于代码或者系统原因,在windows下部署和Linux下部署会有一点差别,以下用windows演示。
Apache的未知后缀解析漏洞
在mime.types这个文件中
mime.types文件包含文件扩展名与MIME类型之间的映射关系。这些映射告诉Apache服务器如何处理不同类型的文件。这种映射通常用于指定浏览器如何显示文件,而不是指定服务器是否执行文件。例如在mime.types文件中有text/plain----txt的映射,那么当服务器收到一个以.txt结尾的文件请求时,它会把文件的内容和类型发送给浏览器,浏览器会根据Content-Type直接显示文本内容。
在http.conf这个文件中
可以通过AddHandler指令将文件扩展名映射到指定的处理程序,告诉服务器如何处理文件类型,优先于mime.types文件中的映射 。例如使用 AddHandler application/x-httpd-php .php指令,只要文件名包含.php后缀,就会交给PHP模块执行其中的命令,无论mime.types这个文件中是否有映射。
AddType指令是将文件扩展名映射到指定的内容类型,告诉服务器要为客户端提供哪种 MIME 类型 。内容类型(也就是MIME类型)是用来告诉浏览器如何处理不同类型的文件的标准 。
如果只使用AddType指令,而没有使用AddHandler指令时。
假如在mime.types这个文件中有映射image/png-----png,那么即使使用AddType application/x-httpd-php .php指令,sc.php.png也不会交给PHP模块执行其中的命令,而是根据mime.types这个文件中映射内容image/png-----png当做png图片处理。
如果在mime.types这个文件中没有关于png的映射,那么sc.php.png文件的内容类型会被设置为application/x-httpd-php,只要文件名包含.php后缀(sc.php.png),并且服务器有相应的处理程序来处理PHP文件,就会交给PHP模块执行其中的命令。
两个文件的联系
Apache默认可以支持多个文件后缀名。当一个文件名包含多个以点为分隔的后缀时,Apache会从最右边开始识别其后缀名,如果遇到无法识别的后缀名,则会依次从右向左进行识别。如果上传了一个包含恶意PHP代码的muma.php.jpg文件,服务器就可能执行文件中的恶意PHP代码。
如果在mime.types文件中没有定义一个文件类型和后缀名的映射,那么在httpd.conf文件中使用AddType指令来添加新的后缀名和文件类型也是有效的。
AddType指令也可以覆盖mime.types文件中没有的内容类型映射,从而改变服务器对文件的处理方式。
演示
这个解析漏洞与Apache版本无关,只与配置有关。
操作前打开httpd.conf文件 AddType application/x-httpd-php .php .phtml确保前面没有井号 # ,表示文件名包含.php的被当做php代码解释。
演示的时候先用sc.php.zip作为试了试,访问发现不能解释里面代码而且是下载一个压缩包,但是sc.php.7z的代码是可以被解释的,加上注释#AddType application/x-httpd-php .php .phtml然后重启Apache,让sc.php.7z也不能解释里面代码,浏览器发现显示的是sc.php.7z里面的部分代码内容。
问题就在于mime.types和http.conf这两个文件。
由于搜索mime.types没有发现定义7z、php、phtml与MIME类型的映射关系,所以我们在http.conf这个文件中看到的AddType application/x-httpd-php .php .phtml相当于添加新的后缀名和文件类型。这使得文件名带.php和.phtml的文件都会被当做php文件解释然后结果发送到浏览器。
sc.php.7z可以正常解释代码而sc.php.zip不能是因为在mime.types这个文件中我们看到了zip拓展名和MIME的映射关系,Apache允许多个后缀名,由右开始向左解析,所以服务器就会根据mime.types的映射关系把zip当做一个压缩包处理,我们访问的时候就会下载。
sc.php.7z在mime.types中没有映射,7z在mime.types没有映射是未知拓展名,所以由右向左解析到php,而AddType application/x-httpd-php .php .phtml又指定了包含
.php和.phtml的文件名都被当做php代码解释。
当我们注释掉AddType application/x-httpd-php .php .phtml时,Apache对于在MIME中没有的7z就不知道怎么处理了,也不会认为是.php。
Apache对于未知类型的文件返回给浏览器什么内容,取决于它的配置文件中是否有指定这种文件的类型或处理器,以及浏览器如何处理没有Content-Type的文件,浏览器可能会把它当作普通的文本或HTML文件来显。因此我们看到的sc.php.7z只有部分代码内容。
如果使用AddHandler application/x-httpd-php .php指令的话,那么任何类型文件名包含.php的都可以被当做PHP执行。
再加上开始去掉的注释符 # 让7z也不能被解析成功,然后对比zip不能解析成功浏览器输出结果有什么不同
httpd.conf修改要重启Apache
问题(在windows系统下部署)
- 为什么前面BURP发包每次会有一个sc.php.jpg没有被重命名,并且2000个包成功被上传并重命名只有几个?
- 为什么快速点了三次发送,只保存了两个文件,一个被重命名了,另一个没有被重命名?
- 如果代码原因造成服务器存在一个没有被重命名为xxx.jpg的文件比如sc.php.jpg,配合解析漏洞就可以执行后面代码
原因
分析代码使用time() 函数返回的时间戳作为重命名后的文件名,time() 函数返回的时间戳通常只精确到秒级别,如果用户在同一秒内点击上传按钮,它们生成的时间戳将是相同的,进行重命名时就会冲突失败。
快速点了三次(一秒内)发送就可能导致以下情况:
- 第一个请求成功上传和重命名了一个文件。
- 第二个请求上传了一个文件,但因为重命名冲突失败。
- 第三个请求可能遇到相同的问题,因此也无法重命名文件。
- 每个请求原文件名都是一样的,所以重命名失败后第三个覆盖第二个
- 最后保存的文件就只有两个,分别是第一个请求和最后一个请求
因此可以知道BURP发包多少秒就有多少个被重命名的文件,还有一个原文件名的文件。
解决
为了让所有文件都会被重命名为xxx.jpg格式,不会保存一个未被重命名的原文件。
由于部署环境是windows的原因,会有一个命名失败的原文件保留。如果想在windows部署环境,又想当做条件竞争来处理也简单,可以把time()函数换成以下两个任意一个,或者可以对time()函数获取的时间戳和随机数拼接、time()作为随机数种子等方法,目的就是不能有重命名后相同文件名的文件。下面演示以下当做条件竞争怎么处理。
$imgFileName = uniqid(); //生成一个基于当前时间和随机数的唯一字符串
$time = microtime(true); //实现微秒级的时间戳生成
$imgFileName = str_replace('.', '', $time); // 去除小数点
python代码
import requests
import time
# 设置目标网站的URL
url = "xxx"
# 设置每秒的访问次数
rate = 5
# 设置持续的时间
duration = 60
# 计算每次访问的间隔
interval = 1 / rate
# 初始化开始时间
start_time = time.time()
# 初始化访问次数
count = 0
# 在持续时间内重复发送请求
while time.time() - start_time < duration:
try:
# 发送请求并获取响应
response = requests.get(url)
# 计数
count += 1
# 响应的文本内容里有"successfully"或者"exists"就停止循环
if "successfully" in response.text or "exists" in response.text:
print("ok" + "Status code - " + str(response.status_code) + "\n")
# 获取返回的文本内容
print(response.text)
break
time.sleep(interval)
except requests.RequestException as e:
# 输出异常信息并退出循环
print("Error: " + str(e))
break
print("总请求次数: " + str(count))
最后
如果是在Linux环境下部署的,使用time()函数也没关系,因为不会遗留下原文件名,因为一秒内上传多个在服务器只会保存重名后的一个文件。
抓包验证了没有返回重命名失败的消息,猜测应该是所有文件都重命名成功,名称相同覆盖了前面的,或者说有其他机制其他函数特性。