木匣子

Web/Game/Programming/Life etc.

从 Error 500 到企业云:一次入侵检测体验

最近全世界都有些疯狂,澳洲大火,2019-nCov。我一边围观疫情,一边刷着新闻,突然间网站卡住了。作为前端,职业病般地打开了开发者工具,在网络面板中看到一片红色的 Ajax 请求,全部都 Error: 50X 了。并且有些请求还返回了一段刺眼的 html 错误描述:

<div style="border:1px solid #990000;padding-left:20px;margin:0 0 10px 0;">
  <h4>An uncaught Exception was encountered</h4>

  <p>Type: Yar_Client_Transport_Exception</p>
  <p>Message: server responsed non-200 code '502'</p>
  <p>Filename: /home/wwwroot/web/models/News_model.php</p>
  <p>Line Number: 69</p>

  <p>Backtrace:</p>

  <p style="margin-left:10px">
    File: /home/wwwroot/web/libraries/REST_Controller.php<br />
    Line: 665<br />
    Function: call_user_func_array
  </p>

  <p style="margin-left:10px">
    File: /home/wwwroot/web/index.php<br />
    Line: 215<br />
    Function: require_once
  </p>
</div>

这样一个报错,立马提起了我的胃口,第一时间先把这个文件存档,保存现场。通常来说,这些路径并没有什么实际用处。因为一般来说,这些 PHP 脚本都会在服务端执行,在前端什么也看不到。但是在好奇心的驱使下,我还是没忍住将文件路径拼接到网址中,看看会发生什么。

没想到浏览器竟然直接弹出了下载进度条,把源文件给下载下来了!源文件大概长这样:

<?php
class News_model extends CI_Model
{

    public function __construct()
    {
        parent::__construct();
        $this->RPC_client = new Yar_Client(API_SERVER . "/news");
        $this->table = "news";
    }

    public function getOne($table, $where = [], $order = [], $field = [])
    {
        if (!$table) {
            $table = $this->table;
        }
        return msgpack_unpack($this->RPC_client->getOne($table, $where, $order, $field));
    }
    ...
}

可惜这只是一个 PHP 的类定义和一些基本的 Yar 的远程过程调用,而报错正是因为这个 Yar 请求超时所导致。似乎没什么研究价值。不过既然可以下载这个文件,那么理论上只要知道其它的文件路径,就可以把他们全部下载下来吧!?可是要怎么知道其它的文件路径?难道一个一个猜吗?

不!其实有更简单的方法。如果你仔细看的话,可以发现这个类定义有特殊之处 class __ extends CI_Model {} 显然这个网站用了一个叫 CI 的框架。也就是著名的 CodeIgniter 。于是我们只要下载 CI 并对照框架的结构,大概就可以知道其它文件的路径。而我们的主要目标是配置文件,因为那里面可能包括了重要的信息。

经过比对确认,该网站使用的是 CI 3.x 左右的版本,框架的目录结构如下 (略去非重要文件):

CodeIgniter-3.1.11
├── application
│   ├── cache/
│   ├── config
│   │   ├── autoload.php
│   │   ├── config.php
│   │   ├── constants.php
│   │   ├── database.php
│   │   ├── hooks.php
│   │   ├── index.html
│   │   ├── memcached.php
│   │   ├── migration.php
│   │   └── routes.php
│   ├── controllers/
│   ├── helpers/
│   ├── index.html
│   ├── language/
│   ├── libraries/
│   ├── models/
│   └── views/
├── composer.json
├── index.php
├── system
...
62 directories, 459 files

几乎每个目录里都有一个空的 index.html 文件,这个文件一般是 Web Server 的默认索引文件,直接访问目录的时候,会返回一个空白页,这是用来防止目录遍历的。

经过一番尝试,我下载到了一些文件和线索:

  • config/config.php - CI 配置文件,里面有一些 csrf-token、encryption-key 之类的,不过该站没有启用。
  • config/autoload.php - 自加载文件,提示了一些自定义的配置文件名、辅助函数(helper)。
  • config/constants.php - 全局常量,里面有一些重要的密钥,非常有价值。
  • config/database.php - 数据库配置,然而什么也没有。
  • config/jwt.php - 从 autoload 中发现,存有 jwt 加密用的私钥。
  • helpers/authorization_helper.php - 从 autload 中发现,存有 api token 的加密方式。
  • model/Users_model.php - 根据命名规则猜出的用户数据类,包含用户数据结构。
  • controllers/Users.php - 根据命名规则猜出的用户业务逻辑类,包含登录检测之类的业务逻辑。

这个网站大量使用了 Yar 框架,通过远程过程调用操作数据读写。所以数据库和主要的业务逻辑都不在上面。而服务器都是以 172.10.x.x 开头的内部地址。所以没办法从外部访问。这种项目拆分的方式使得业务逻辑可以重用,网络隔离也增加了安全性。但是!我们继续挖。

我尝试了使用密钥手动构建 jwt token 的方式进行 cookie 欺骗,但是该网站的登陆检测逻辑中,居然用 redis 检查这个 token 是否入库,没有就不放行。一般来说 redis 可以用来做 token 黑名单,而不是用来作白名单。这一神操作居然曲线救了国。虽然放弃了 jwt 本身的优点(无状态),但是意外地挡住了入侵者。

再看看刚才提到的那个 config/constants.php 文件,这里面有很多变量。在里面找到了一组 Aliyun OSS 的 AccessKeyId 和 AccessKeySecret 。而该网站的静态资源正是存放在阿里云对象存储服务(Object Storage Service)上。

如果这组密钥可用,应该可以通过脚本访问的方式,获取到一些内部文件。我对 Aliyun 不太熟悉,于是顺手下载了 Aliyun Cli 现学现用:

# 安装 aliyun cli
$ brew install aliyun-cli
# 配置 Access Key
$ aliyun configure --profile news
Configuring profile 'news' in 'AK' authenticate mode...
Access Key Id: *********************aBc
Access Key Secret: ***************************eFG
Default Region Id: ap-southeast-2
Default Output Format [json]: json (Only support json)
Default Language [zh|en]: en
Saving profile[news] ...Done.

Configure Done!!!
..............888888888888888888888 ........=8888888888888888888D=..............
...........88888888888888888888888 ..........D8888888888888888888888I...........
.........,8888888888888ZI: ...........................=Z88D8888888888D..........
.........+88888888 ..........................................88888888D..........
.........+88888888 .......Welcome to use Alibaba Cloud.......O8888888D..........
.........+88888888 ............. ************* ..............O8888888D..........
.........+88888888 .... Command Line Interface(Reloaded) ....O8888888D..........
.........+88888888...........................................88888888D..........
..........D888888888888DO+. ..........................?ND888888888888D..........
...........O8888888888888888888888...........D8888888888888888888888=...........
............ .:D8888888888888888888.........78888888888888888888O ..............

配置完 Aliyun Cli 之后,之后就可以用这个 profile 进行 API 调用了:

# 查看手册
$ aliyun --profile news help
# 为了简化输入,这里使用 alias 为这个命令取个别名
$ alias news="aliyun --profile news"
# 之后只要这样即可
$ news help
# 查看 OSS 服务手册
$ news oss help
# 列出所有 OSS Buckets
$ news oss ls
# 列出所有子目录
$ news oss ls oss://web-static/ --directory
# 下载所有文件
$ news oss cp oss://web-static/dist --recursive

此外阿里云还提供了一些不错的工具供探索:

OSS 上只有一些前端的静态文件,似乎没有什么收获,可以通过篡改脚本文件的方式植入前端代码。另外 Aliyun Cli 还挺有意思,我几乎把所有命令都玩了一遍,结果发现了一个惊人的秘密!这个 OSS Access Key 居然有管理员权限。它并不是一个普通的帐号,而是 Aliyun 的超级管理员帐号。我想这个开发组的心也真大,居然把这么重要的密钥用在项目里,只是为了管理图片上传。万一遇到坏人怎么办!

Update: 2020-02-12

经过一段时间的非侵入式勘察,确认的漏洞的原因是网站的 nginx 服务器配置出现破绽,原本的 nginx 中有一段默认的 php 文件处理配置被无意间删去。最终导致除了 index.php 之外的 php 文件都不被执行,而是当作普通文件处理:

location ~ [^/]\.php(/|$) {
    fastcgi_split_path_info ^(.+?\.php)(/.*)$;
    if (!-f $document_root$fastcgi_script_name) {
        return 404;
    }

    # Mitigate https://httpoxy.org/ vulnerabilities
    fastcgi_param HTTP_PROXY "";

    fastcgi_pass unix:/var/run/php-fpm.sock;
    fastcgi_index index.php;

    # include the fastcgi_param setting
    include fastcgi_params;

    # SCRIPT_FILENAME parameter is used for PHP FPM determining
    #  the script name. If it is not set in fastcgi_params file,
    # i.e. /etc/nginx/fastcgi_params or in the parent contexts,
    # please comment off following line:
    # fastcgi_param  SCRIPT_FILENAME   $document_root$fastcgi_script_name;
}

所谓非侵入式勘察,是我自造的词。经过一段时间我对 Aliyun 的探索,我发现可以在不知道实例的密码和密钥的情况下,直接复制未加密的云盘,来获取里面的源文件。从而探索漏洞形成的原因。

虽然补上这个配置就可以防止源文件被下载。但是作为企业,更应该制定安全规范的密钥使用准则,避免随意使用超大权限的密钥。另外为了安全起见,拥有重要源码的实例都应该使用加密盘,防止被复制挂载。