今天研究了一道Web题 --- Cat
本题来自 WHCTF 2017 , 大致流程是 : 输入一个域名 , 然后返回该域名是否存在
解题思路
- 然后我就一直以为这是一道命令执行题目 , 然后就想方设法绕过限制 , 所以思路都跑偏了2333 , 试到最后都没有能找到exp .
-
好了言归正传 , 遇到这道题首先要FUZZ一下 , 看一下URL到底能接收或过滤哪些字符 . 然后发现一共存在如下三种情况
-
发现Django报错 , 那么说明在处理请求时用的是 Python Django 框架 , 而由于DEBUG模式未被关闭 , 所以出错时指向了特定的错误页面 , 从而暴露了敏感文件内容
在通过
django-admin startproject ProjectName
创建Django项目时 , 会自动生成一个settings.py
文件settings.py
中可以设置是否开启DEBUG模式如果
DEBUG
参数被设置为True
, 那么 :- 所有的数据库查询将会以
django.db.connection.queries
的形式被保存在内存中 , 这会消耗大量的内存 -
任何404错误都会呈现django特殊的404页面 , 这个页面包含了潜在的敏感信息 , 但是该页面不会在互联网上暴露出来
-
任何没有被捕获的异常( 从基本的python语法错误到数据库错误以及模板语法错误 )都会返回漂亮的django页面( 在后面你能看到这个页面 ) , 这个页面包含了大量的敏感信息 , 甚至包含部分源代码 !
总的来说 , 当
DEBUG
被设置为True
时 , 该网站只能被可信任的开发人员使用 . 当你想要在互联网上部署该应用时 , 需要设置DEBUG
为False
.
- 所有的数据库查询将会以
-
回到Django报错本身上 , 为了便于研究 , 这里把报错进行HTML解码后转存查看
下面发现了三段代码
- 这段代码用于拼接 requests.FILES 和 requests.POST
这段代码很奇怪 , 因为在页面中是通过 GET 传递参数的 , 为什么会有 request.POST的值 ? 另外页面并没有任何文件上传点 , 为什么会有 request.FILES?
- 这里从POST中获取url参数的值 , 对该值转义后用一个正则表进行过滤 , 如果出现非法字符则返回 Invalid URL , 若通过过滤则将其放入
ping -c 1
命令中执行
这个正则表非常的严谨 , 命令执行漏洞中常用的 "& | ;" 都不在其中且好像无法绕过 , 所以这里应该不存在命令执行的可能
- 这段代码是escape()函数的实现 .
因为用户可能会输入一些特殊字符 , 而且这些特殊字符可能会被解释为正则运算符 . 所以通过对它们进行转义 . 转义后将数据进行GBK编码后返回
接下来是请求信息
如果你对HTTP请求很熟悉 , 你应该会发现有问题 , 一般来说正常网页提交表单时 , Content-Type应该为 : "application/x-www-form-urlencoded" , 但是这里用的却 "multipart/form-data" , 这是用于上传文件的表单 . 看来表单中拼接的那个requests.FILES肯定有问题 !
-
现在这个网站就比较奇怪了
- 唯一的页面是一个
php
页面( index.php ) , 而页面的报错是Python Django
的报错 , 为什么PHP页面会出现Python的报错? -
页面是通过GET请求获取数据 , 为什么后台会拼接POST请求数据?这个request.FILES请求是怎么来的?
-
这个Django报错并没有通过HTML渲染 , 而是是通过类似于PHP的
show_source
显示输出的.
其实这里已经不难猜出 , 本题后台可能不止一个应用
- 通过PHP将界面呈现给用户 , 通过GET请求获取参数 .
-
将POST参数与request.FILES拼接处理后通过POST请求传递给Django搭建的API上 .
-
Django API 解析收到的数据 , 对其进行特殊字符转义并编码为GBK , 然后将数据通过正则判断 , 如果未通过则返回错误信息
Invalid URL
给PHP , 如果通过则将其放入ping -c 1
执行系统命令
- 唯一的页面是一个
-
接下来怎么想?
现在上面很多疑问已经能解释清楚了!
-
为什么会出现POST请求?
这个POST请求是PHP构造好后发送给Python Django的 , 与用户操作无关
-
为什么会输出Django报错信息?
因为当Django正则不匹配时 , 会将报错信息返回给PHP , PHP本身只作为传递数据的媒介而不解析数据 , 所以仅输出Django的报错信息
-
为什么用户输入 " %A0 " 等特殊字符会出现Django Debug , 而不是写好的
Invalid URL
报错?因为在正则匹配之前 , 先将数据转义并进行GBK编码 . 因为像 "%A0" , "%B0" 等并不在GBK编码表中 , 所以这些Unicode字符不能被解码为GBK , 引发报错并终止程序 , 没有执行后面的正则匹配 . 所以在DEBUG界面会输出对应的报错信息
那么漏洞到底在哪里呢? 其实整个流程大致可以分成两个阶段.
- PHP处理阶段
这个过程感觉是可以利用的 , 因为到现在我都还不清楚那个request.FILES
到底有什么用 , 因为正常情况下没必要把一个无关的文件拼接进用户的请求啊~ -
Django处理阶段
Django处理阶段需要通过那个正则表的过滤 , 而且这个正则表非常的严谨 , 暂时没有什么很好的方式绕过它 . 所以这个阶段看上去难以利用
- PHP处理阶段
- PHP处理阶段解析
-
通过PHP获取数据 , 再传递数据给Django , 那么究竟是如何传递数据的呢?
可以是配置Django主动请求PHP数据 , 也可能是PHP再构造表单发送给Django . 很明显这里是 PHP重新构造表单给Django
如何通过PHP构造POST请求? 你肯定会想到
libcurl
库 , 这个库在SSRF中常常被使用这里就要用到几个小知识点~
-
还有个疑问就是这个
requests.FILES
到底有什么用 .HttpRequests.FILES
是Django处理文件上传时的类字典对象 , 表单上传的文件对象就存储在其中 , 在使用时表单格式需要为multipart/form-data
! , 现在你应该明白为什么要用这个 Content-Type 了!而下面又有这么一段代码
if isinstance(v, InMemoryUploadedFile): v = v.read()
这里将文件v进行类型比较 , 如果v的类型为
InMemoryUploadedFile
, 那么就读取它这个InMemoryUploadedFile 是一个特殊的类型 , 它表示通过表单上传的文件对象的类型 . 该对象有一个read()方法 , 用于从文件对象中访问文件的内容
那么哪些文件可以通过这个条件呢?
InMemoryUploadedFile
译为内存中的上传文件 , 你是否还记得前文我提到开启DEBUG模式结果中的第一条内容?看来我们似乎有目标了 , 找一找前面的DEBUG页面有没有透露什么数据库文件吧~
绝对路径都给你了~ , 搭配上面这两个利用点 , 是否可以读取该文件中的内容呢?
总结
其实这道题利用的条件真的非常苛刻 , 通过这道题我有了不小的收获
利用条件 :
- Django开启了DEBUG模式
-
PHP需要开启 CURLOPT_POSTFILEDS , 且设置 CURLOPT_SAFE_UPLOAD = False
-
读取的文件中存在GBK的无法编码的特殊字符 , 且服务没有捕获到这个异常
利用原理 :
请求中通过 " @ + 文件绝对路径 " 发送文件 , GBK无法对文件中的特殊字符编码导致GBK抛出未捕获的错误 , 把POST请求中的内容爆出 , 而这个POST请求恰好拼接了敏感文件的内容 , 所以可以拿到敏感文件中的内容