前言
这周末有点不舒服 , 在宿舍躺了两天 , 啥也看不进去 , 整个人都傻了. 这两天稍微好点 , 看了道攻防世界的题目 : Web_php_wrong_nginx_config
. 解题路线比较简单也很容易找 , 但是写利用脚本时却遇到了比较大的困难 . 还是我太菜了哈哈 .
下面总结一下解题过程 , 以及构造利用脚本的思路 .
Web_php_wrong_nginx_config
拿到敏感文件
打开环境会看到一个弹窗 , 提示请登录 , 然后会跳转到登录界面
没有找到注册按钮 , 随便输入用户名密码登录后会有提示弹框 : 网站建设中 !
看到弹窗就觉得这里登录并不是关键 , 应该有其他的利用点 . 于是拿 dirsearch
扫描一下后台文件
-
/admin
|/admin/
|/admin/?/login
|/admin/index.php
四个页面都提示
please continue
-
/admin/admin.php
弹窗显示
you need to log in
, 然后跳转到login.php
页面 -
/images
/images
会跳转到/images/
并返回 403 , 没有任何利用点 . -
robots.txt
发现了两个敏感文件
hint.php
和Hack.php
, 分别查看他们 . -
hint.php
给了个提示 ,
/etc/nginx/sites-enabled/site.conf
文件的配置可能有问题 . 该文件应该是 Nginx 的站点核心配置文件之一 . -
Hack.php
访问该文件得到一个空白页面 , 猜测该文件没有直接输出任何语句 .
基本的信息搜集的差不多了 , 我们现在有几个可用的点 .
-
/admin/admin.php
页面该页面需要登录后才能访问 , 但是我们没有帐号密码 , 也没有拿到注册页面 . 所以帐号密码登录是不太现实的 . 同样爆破帐号密码也不太可能 . 现在最理想的情况是在该站点在某个地方泄露了
Cookie
, 我们可以凭借这个Cookie
以某个用户的身份登录 . -
/etc/nginx/sites-enabled/site.conf
文件这个文件是题目中给出的提示 , 肯定有用 . 但现在没有地方去读取这个文件 . 最常用的读取文件漏洞有 " 文件包含 " 与 " 任意文件下载( 读取 ) " , 但要先找到利用点 .
-
Hack.php
页面暂时不知道该页面是干啥用的 , 不过看这文件名字肯定有利用点 .
整理思路后 , 拿 BurpSuite
代理走一遍刚才的流程 .
本地文件包含读取 Nginx 配置文件
BurpSuite 发现一个奇怪的 Cookie
在访问的该站点的每一个页面时 , 都会带上一个名为 isLogin
的 Cookie
, 这没有任何问题 , 但是 isLogin
字段的值却被定义为 " 0 " .
想到 Cookie 是用来识别用户 , 维持用户登录状态的 , 这里 " 0 " 可能代表布尔值 . 因此手动将它改为 " isLogin=1 "
然后再访问 /admin/admin.php
, 发现我们成功登录到了站点后台 .
题目环境可能有点问题 , 如果这里没有成功跳转 , 就再进一次题目环境 , 有了 Cookie 后页面会自动跳转到站点后台
控制台可能会抛出一堆错误 , 但这些错误都与做题无关 , 不去管它们
本地文件包含漏洞拿到 /etc/nginx/sites-enabled/site.conf
后台看起来有很多选项卡 , 其实大部分都是假的 , 即使有几个选项存在页面跳转 , 也都是指向 index.php
, 没有什么问题 .
真正的利用点在于 " 管理中心 " 选项卡 , 在访问它时会有如下这个 HTTP 请求
注意这个参数 : file=index?ext=php
, file
参数值是文件名 , ext
参数值是文件扩展名 . 那么这里是否存在LFI( 本地文件包含漏洞 ) , 可以包含其他文件呢 ?
-
./
测试Payload :
file=./index&ext=php
提示
please continue
. -
../
测试Payload :
file=../index&ext=php
响应结果与
./
完全相同 , 看起来../
并没有被解析处理 , 或者说是被过滤掉了 . -
....//
测试既然
../
可能被过滤了 , 那么就重叠写为....//
. 这样即使../
被过滤删除 , 剩下的内容也还是../
Payload :
file=....//index&ext=php
页面内容出现了变化 , 不再出现
please continue
, 这里可能因为过滤检测后../index.php
文件不存在 , 而导致站点自动跳转到./index.php
主页 .因此重写法绕过
../
过滤可能是成功的 , 我们继续测试 . -
测试去除 ext 参数值
如果 HTTP 请求中
ext=php
是必须存在且无法更改的 , 那么这里的利用会非常困难 . 因为我们的目标是/etc/nginx/sites-enabled/site.conf
, 而不是 PHP 文件 .Payload :
file=index&ext=
没有出现
please continue
, 因此这里看到的页面可能是因为当前目录下index
文件不存在而强制跳转到的index.php
. -
测试去除 ext 参数值 , 并在 file 参数中添加文件扩展名
那么如何验证上述
没有出现 please continue 字符串是因为站点没有找到文件而做的强制跳转
这个猜想是正确的呢 ?我们不在
ext
参数字段中添加任何值 , 而是在file
参数中添加文件扩展名 , 如果出现了please continue
则表示读取到了文件 .Payload :
file=index.php&ext=
出现了
please continue
, 说明读取到了当前目录下的index.php
. 而前面 Payload :file=index&ext=
因为站点没有读到index
这个文件 , 直接跳转到index.php
, 因此没有出现please continue
.这也说明了我上面所有的猜测都是正确的 . 我们可以利用重写
../
来绕过站点的过滤删除机制 .现在我们可以直接读取到
/etc/nginx/sites-enabled/site.conf
.Payload :
file=....//....//....//....//etc/nginx/sites-enabled/site.conf&ext=
成功读取到
/etc/nginx/sites-enabled/site.conf
配置文件
目录遍历漏洞拿到 WebShell
利用上面的本地文件包含漏洞 , 我们可以读取到所有已知文件名的文件 , 但是对于那些我们不知道的文件 , 就没有办法去读取 . 必须拿到其它的利用点 .
查看 /etc/nginx/sites-enabled/site.conf
, 很容易发现其中存在的问题 .
这段代码给 /web-img
目录设置了一个别名 /images/
, 并且开启了 autoindex
.
这里给不熟悉 Nginx 的同学说下 ,
alias 用于给 localtion 指定的路径设置别名 , 在路径匹配时 , alias 会把 location 后面配置的路径丢弃掉 , 并把当前匹配到的目录指向到 alias 指定的目录 .
注意 ! alias
会丢弃掉 location
的路径 , 因此 alias
后面的路径是从系统根目录开始的 , 然后直接跟指定的路径 . 它和 root
的用法不一样 . 可以参考这个链接 .
而 autoindex
是一个目录浏览功能 , 用于列出当前目录的所有文件及子目录 .
总之 , 这里在 URL 访问 /web-img
, 就会访问系统根目录下的 /images/
而如果在 URL 访问 /web-img../
, 则相当于访问 /images/../
, 即访问系统根目录 . 且由于开启了 autoindex
, 我们可以直接在浏览器里看到根目录下的所有内容 !
这里就是一个目录遍历漏洞 . 我们可以通过它查看系统中的所有文件~
遍历目录 , 在 /var/www
下能找到 hack.php.bak
这是 hack.php
的自动备份文件 . 点击即可下载到本地 .
打开后发现代码很乱 , 不清楚有什么用 .
但是这玩意怎么看怎么像用 weevely
生成的 WebShell 后门 .
weevely 生成的后门如下
所以猜测这也是一个 WebShell , 只不过经过了加密混淆 . 我们需要的做的是分析出该后门的构造思路 , 并且编写连接脚本 , 以连接这个 WebShell .
分析后门代码
hack.php.bak
里代码的思路是很简单的 , 先定义多个变量 , 然后通过 str_replace()
函数进行字符替换并拼接 , 接着通过 create_function()
创建了一个匿名函数 , 最后执行该函数 .
str_replace()
函数替换拼接后的代码就是匿名函数$g
的内容 . 因此这里输出$g
, 看看该函数到底在做什么 .
还是很乱 , 需要稍微整理下 , 这里增加了一些注释 .
-
先是预定义阶段 , 定义了两个字符串和一个
x()
函数 , 后面会用到 -
然后就需要获取攻击者发送的数据了 , 这里攻击代码是通过
Referer
字段传输的需要注意这个正则函数
preg_match_all()
, 该函数从Accept-Language
取值 , 然后通过正则匹配后输出到 $m 数组中 . 单独拿出来看 , $m 数组的输出内容是如下这样的 .$m[0] : 所有可选语言及其权重系数 $m[1] : 所有可选语言的首字母 $m[2] : 所有可选语言的权重值( 不清楚该怎么说 , 反正你能明白 )
简单的说下这个
Accept-Language
( 参考链接 )举个例子 : Accept-Language: zh-cn,zh;q=0.5 1. Accept-Language表示浏览器所支持的语言类型 . 2. zh-CN 表示简体中文 , zh 表示中文 , 不同语言之间用逗号分割 . 3. q 是权重系数 , 范围为 [0,1] . q 值越大 , 请求越倾向于获得其对应语言表示的内容 . 若没有指定 q 值,则默认为1 . 若被赋值为0 , 则用于提醒服务器该语言是浏览器不接受的内容类型 .
然后拼接了前两种可选语言的首字母 , 和预定义的字符串拼接并进行 md5 校验 , 截取等操作 . 然后赋值给
$h
和$f
两个变量 . -
拼接 Payload
循环中的
$p .= $q[$m[2][$z]]
会不断从$q
中提取数据 . 结合之前的代码 , 攻击代码是放在Referer
中的( 最后会放在$q
中 ) , 因此这里可以看作是拼接攻击代码 , 组合成 Payload . '然后判断 $h 是否出现在 Payload 的开头 , 若是则设置
$_SESSION['$i'] = ""
, 同时删除 Payload 的 $h 部分 .接着判断
$_SESSION
中那个是否存在$i
这个键名 , 若是则将 Payload 赋值给$_SESSION[$i]
, 然后查找$_SESSION[$i]
( 也就是 Payload ) 中$f
第一次出现的位置 . -
解密并执行 Payload
紧跟上面的代码 , 若在 Payload 中找到了
$f
第一次出现的位置( 也就是说明$f
存在于 Payload 中 ) , 就会继续执行如下过程 .-
生成密钥
$k
, 该值由预定义的两个字符串拼接而成 , 然后打开输出控制缓冲区 . -
截取 Payload 中从开头到
$f
出现位置的这部分字符串( 由此可以判断$f
应该是出现在 Payload 的末尾 , 这里删去 $f ) -
利用
preg_replace()
函数 , 正则替换字符串中的 "_
" 和 "-
" 为 "/
" 和 "+
" . -
对替换后的字符串进行
Base64_decode
解码操作 -
对解码后的字符串进行循环异或运算( 也就是调用
x()
函数 ) -
对计算后的字符串调用
gzuncompress()
函数进行解压 . -
通过
eval()
执行解压后的字符串 . -
返回输出到缓冲区的内容 , 然后清空并关闭输出缓冲区 .
-
对缓冲区输出的内容通过
gzcompress()
函数压缩 , 再通过x()
函数循环异或计算 , 最后通过Base64_encode
编码并输出 .
-
整个后门代码的思路应该就是如上这样的 , 攻击者通过 Referer
传输攻击代码 , 这段攻击代码的格式应该为 填充字符串 + 加密混淆后的Payload + 填充字符串
该后门脚本接收到攻击者传送的数据包 , 先按照一定顺序取出攻击代码 , 然后把前后两侧的填充字符串去除 , 拿到 Payload , 然后对 Payload 进行解密反混淆操作 , 接着通过 eval()
执行攻击者指定的命令 , 最后将命令执行结果加密编码呈现给攻击者 .
整体的解密流程还是非常清晰的 , 我们知道了后门是如何处理攻击者发送的恶意数据包 . 但现在我们没有连接脚本 , 无法构造出后门能处理的请求 . 因此这里需要逆向整个解密过程 , 以便构造出请求数据包 .
逆向分析后门解密过程
正如上所说的 , 我们需要根据解密流程构造出加密流程 , 其主要过程如下所示
-
定义一些变量与函数
这里主要提一下
x()
函数 , 正如注释里写的 , 由于A ^ B ^ B = A
这个特性 , 所以加密过程和解密过程的x()
是完全相同的 . -
构造 Payload
结合加密过程的内容 , 构造 Payload 的过程不多说了 , 看注释应该很容易明白 .
-
对收到数据包的处理
因为后门会在
eval()
处理完毕后返回输出信息 , 且输出信息是被加密过的 . 因此还需要一个解密过程 .
以上就是整个加密构造 Payload 并接收响应数据的流程 , 思路还是比较简单的 .
现在我们仅需要构造出最终的连接脚本就可以了 .
构造连接脚本
由于我自己的代码能力有限 , 搞了很久也没有弄出来连接脚本 . Google后发现了这篇文章 : 一个PHP混淆后门的分析 , 该文章讲的非常清楚 , 可以作为参考分析 , 且有一个已经写好的 Python 连接脚本 .
分析一下这个连接脚本是如何构造的吧 , 原理还是挺简单的 .
-
因为从 Referer 中提取 Payload 的顺序依赖于
Accept-Language
, 而不同客户端发送的Accept-Language
可能不一样 , 因此这里需要一个函数来辅助生成随机的可选语言及权重值 .先判断输入的序列串是否为空 , 不为空则建立
result
数组 , 并循环将序列串中的值添加到result
数组中 , 这里的序列串中的值可以是可选语言 , 也可以是权重值 . 最后返回result
数组 -
因为实际攻击代码的组成为
随机填充数据 + Payload + 随机填充数据
, 并且构造 " 交互式Shell " 也需要随机填充数据 , 因此这里需要创建函数来生成随机填充数据 .这里创建了两个生成随机填充数据的函数 , 分别对应不同的情况 .
-
循环异或函数
PHP代码中的
x()
函数 -
开启 Debug
这个可开可不开 , 开启 Debug 有助于代码分析 .
-
定义基本的变量
这个没啥好说的 , 注释写的很清楚
-
生成完整的
Accept-Language
和 Payload 两侧的随机填充空白看注释~~~
-
构造 Payload
这里的代码用于构造 Payload , 包括添加 Payload 两侧的随机填充数据 , Payload 本身的加密 , 最后把加密混淆后的 Payload 作为参数值放入到
Referer
中 . -
发送请求数据包 , 接收并处理响应数据
攻击代码构造好了 , 下面仅需要发送请求并接收数据 , 解密后即可看到攻击者命令的执行结果 .
这里也是对应前面 PHP 代码的 , 配合注释应该非常容易理解 .
这里的攻击代码我会放在文末
利用攻击脚本拿到 Flag
攻击脚本构造完毕 , 下面仅需要修改一些配置( 比如 url
, keyh
, keyf
) , 就可以利用成功拿到 Flag
了 .
搞定 , 成功拿到 Flag !
总结
本题还是非常有意思的 , 难点主要在于构造连接后门的脚本 , 分析代码的过程非常有趣 .
学到了不少东西 , 自己代码能力还是太差了 , 明明思路很清晰 , 但就是搞不出利用脚本 , 比较郁闷 .
如果您觉得本文有哪些不正确的地方 , 或者您有什么更好的想法 , 欢迎您的留言 .
poc.py
文章中使用的 poc.py 完整脚本的链接如下 :
非常详细的wp,博主一丝不苟的态度激励了我:在学习的路上不可做懒汉,亲自搭环境调试和翻阅手册才能理解看似轻松的知识点。